diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 51428930..4ba8ab43 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -65,7 +65,7 @@ repos:
# Run main application tests
- id: pytest-main
name: pytest (main app)
- entry: uv run pytest tests/ --ignore=tests/alembic/
+ entry: uv run pytest -x tests/ --ignore=tests/alembic/
language: system
files: ^(src/|tests/).*\.py$
stages: [pre-push]
diff --git a/docs/changelog/compatibility-guide.mdx b/docs/changelog/compatibility-guide.mdx
index 33e01f7f..719ea184 100644
--- a/docs/changelog/compatibility-guide.mdx
+++ b/docs/changelog/compatibility-guide.mdx
@@ -4,27 +4,23 @@ description: "Compatibility guide for Honcho's SDKs and API"
icon: "shield-check"
---
-This guide helps you understand which versions of Honcho's API are compatible with which SDK versions.
+This guide helps you match the right SDK version to your Honcho API version. Newer SDK patch versions are always backward-compatible within the same major version — install the latest patch for your range.
-## Version Compatibility
-
-### Honcho API v3.0.2 (Current)
+## Current Versions
- **Compatible Version:** v2.0.0
+ **Latest:** v2.0.1
- Install with:
```bash
- npm install @honcho-ai/sdk@2.0.0
+ npm install @honcho-ai/sdk
```
- **Compatible Version:** v2.0.0
+ **Latest:** v2.0.1
- Install with:
```bash
- pip install honcho-ai==2.0.0
+ pip install honcho-ai
```
@@ -34,9 +30,9 @@ This guide helps you understand which versions of Honcho's API are compatible wi
| Honcho API Version | TypeScript SDK | Python SDK |
|-------------------|---------------|------------|
-| v3.0.2 (Current) | v2.0.0 | v2.0.0 |
-| v3.0.1 | v2.0.0 | v2.0.0 |
-| v3.0.0 | v2.0.0 | v2.0.0 |
+| v3.0.2 (Current) | v2.0.0+ | v2.0.0+ |
+| v3.0.1 | v2.0.0+ | v2.0.0+ |
+| v3.0.0 | v2.0.0+ | v2.0.0+ |
| v2.5.1 | v1.6.0 | v1.6.0 |
| v2.5.0 | v1.6.0 | v1.6.0 |
| v2.4.3 | v1.5.0 | v1.5.0 |
diff --git a/docs/v3/documentation/reference/sdk.mdx b/docs/v3/documentation/reference/sdk.mdx
index 967f39ff..9aa0f509 100644
--- a/docs/v3/documentation/reference/sdk.mdx
+++ b/docs/v3/documentation/reference/sdk.mdx
@@ -280,6 +280,12 @@ metadata = alice.get_metadata()
metadata["location"] = "Paris"
alice.set_metadata(metadata)
+# Peer card management
+card = alice.get_card() # Get peer card
+card = alice.get_card(target="bob") # Get card about another peer
+updated = alice.set_card(["Likes Python", "Lives in NYC"]) # Set peer card
+updated = alice.set_card(["Works at Acme"], target="bob") # Set card about another peer
+
# Get peer context (representation + peer card in one call)
context = alice.context()
context = alice.context(target="bob") # What alice knows about bob
@@ -334,6 +340,12 @@ await alice.setMetadata({
location: "Paris"
});
+// Peer card management
+const card = await alice.getCard(); // Get peer card
+const targetCard = await alice.getCard("bob"); // Get card about another peer
+const updated = await alice.setCard(["Likes TypeScript", "Lives in NYC"]); // Set peer card
+const updatedTarget = await alice.setCard(["Works at Acme"], "bob"); // Set card about another peer
+
// Get peer context (representation + peer card in one call)
const context = await alice.context();
const targetContext = await alice.context({ target: "bob" }); // What alice knows about bob
@@ -396,6 +408,46 @@ const searchedContext = await alice.context({
```
+### Peer Card
+
+The peer card contains stable biographical facts about a peer (name, preferences, background). Use `get_card()` / `getCard()` to retrieve it and `set_card()` / `setCard()` to overwrite it:
+
+
+```python Python
+# Get peer's own card
+card = alice.get_card()
+print(card) # ["Likes Python", "Lives in NYC", ...]
+
+# Get card about another peer (local representation)
+bob_card = alice.get_card(target="bob")
+
+# Set peer's own card
+updated = alice.set_card(["Likes Python", "Lives in NYC"])
+
+# Set card about another peer
+updated = alice.set_card(["Works at Acme", "Enjoys hiking"], target="bob")
+```
+
+```typescript TypeScript
+// Get peer's own card
+const card = await alice.getCard();
+console.log(card); // ["Likes TypeScript", "Lives in NYC", ...]
+
+// Get card about another peer (local representation)
+const bobCard = await alice.getCard("bob");
+
+// Set peer's own card
+const updated = await alice.setCard(["Likes TypeScript", "Lives in NYC"]);
+
+// Set card about another peer
+const updatedBob = await alice.setCard(["Works at Acme", "Enjoys hiking"], "bob");
+```
+
+
+
+Peer cards are automatically maintained by the dreaming agent during message processing. Use `set_card()` / `setCard()` when you need to manually override or seed the card — the peer will be created automatically if it doesn't already exist.
+
+
### Conclusions
Peers can access their conclusions (facts derived from messages) through the `conclusions` property and `conclusions_of()` method:
diff --git a/docs/v3/guides/migrations/mem0.mdx b/docs/v3/guides/migrations/mem0.mdx
index 6a2bbd22..1d56bde6 100644
--- a/docs/v3/guides/migrations/mem0.mdx
+++ b/docs/v3/guides/migrations/mem0.mdx
@@ -274,7 +274,7 @@ Additional features with **no Mem0 equivalent**:
| Honcho Method | Description | Use Case |
|---------------|-------------|----------|
-| `peer.card()` | Stable biographical facts (name, preferences, background) | User profiles, personalization |
+| `peer.get_card()` / `peer.set_card()` | Stable biographical facts (name, preferences, background) | User profiles, personalization |
| `session.representation(peer)` | Cached psychological analysis (mental state, intentions) | Real-time adaptation |
| `session.summaries()` | Auto-generated short/long session summaries | Conversation continuity |
| `SessionPeerConfig` | Configure observation settings (who learns about whom) | Privacy controls, role-based learning |
diff --git a/docs/v3/migrations/from-mem0.mdx b/docs/v3/migrations/from-mem0.mdx
index 6a2bbd22..1d56bde6 100644
--- a/docs/v3/migrations/from-mem0.mdx
+++ b/docs/v3/migrations/from-mem0.mdx
@@ -274,7 +274,7 @@ Additional features with **no Mem0 equivalent**:
| Honcho Method | Description | Use Case |
|---------------|-------------|----------|
-| `peer.card()` | Stable biographical facts (name, preferences, background) | User profiles, personalization |
+| `peer.get_card()` / `peer.set_card()` | Stable biographical facts (name, preferences, background) | User profiles, personalization |
| `session.representation(peer)` | Cached psychological analysis (mental state, intentions) | Real-time adaptation |
| `session.summaries()` | Auto-generated short/long session summaries | Conversation continuity |
| `SessionPeerConfig` | Configure observation settings (who learns about whom) | Privacy controls, role-based learning |
diff --git a/sdks/python/pyproject.toml b/sdks/python/pyproject.toml
index 20204d04..dd389d96 100644
--- a/sdks/python/pyproject.toml
+++ b/sdks/python/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "honcho-ai"
-version = "2.0.0"
+version = "2.0.1"
description = "Official DX Optimized Python SDK for Honcho"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/sdks/python/src/honcho/__init__.py b/sdks/python/src/honcho/__init__.py
index cf8d8bb5..35f42dce 100644
--- a/sdks/python/src/honcho/__init__.py
+++ b/sdks/python/src/honcho/__init__.py
@@ -66,7 +66,7 @@ from .types import (
DialecticStreamResponse,
)
-__version__ = "2.0.0"
+__version__ = "2.0.1"
__author__ = "Plastic Labs"
__email__ = "hello@plasticlabs.ai"
diff --git a/sdks/python/src/honcho/aio.py b/sdks/python/src/honcho/aio.py
index 1d72946a..aa851889 100644
--- a/sdks/python/src/honcho/aio.py
+++ b/sdks/python/src/honcho/aio.py
@@ -23,6 +23,7 @@ from __future__ import annotations
import json
import logging
+import warnings
from collections.abc import AsyncGenerator
from datetime import datetime
from typing import TYPE_CHECKING, Any, ClassVar, Literal
@@ -588,7 +589,7 @@ class PeerAio(AsyncMetadataConfigMixin):
]
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
- async def card(
+ async def get_card(
self,
target: str | PeerBase | None = None,
) -> list[str] | None:
@@ -604,6 +605,51 @@ class PeerAio(AsyncMetadataConfigMixin):
response = PeerCardResponse.model_validate(data)
return response.peer_card
+ async def card(
+ self,
+ target: str | PeerBase | None = None,
+ ) -> list[str] | None:
+ """Deprecated: use get_card() instead."""
+
+ warnings.warn(
+ "card() is deprecated, use get_card() instead",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return await self.get_card(target=target)
+
+ @validate_call(config=ConfigDict(arbitrary_types_allowed=True))
+ async def set_card(
+ self,
+ peer_card: list[str],
+ target: str | PeerBase | None = None,
+ ) -> list[str] | None:
+ """
+ Set the peer card for this peer.
+
+ Makes an API call to set the peer card. If a target is provided, sets this
+ peer's local card of the target peer.
+
+ Args:
+ peer_card: A list of strings to set as the peer card.
+ target: Optional target peer for local card. If provided, sets this
+ peer's card of the target peer. Can be a Peer object or peer ID string.
+
+ Returns:
+ A list of strings representing the updated peer card, or None if none is available
+ """
+ await self._peer._honcho._ensure_workspace_async()
+ target_id = resolve_id(target)
+
+ query = {"target": target_id} if target_id else None
+ data = await self._peer._honcho._async_http_client.put(
+ routes.peer_card(self._peer.workspace_id, self._peer.id),
+ body={"peer_card": peer_card},
+ query=query,
+ )
+ response = PeerCardResponse.model_validate(data)
+ return response.peer_card
+
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
async def representation(
self,
diff --git a/sdks/python/src/honcho/peer.py b/sdks/python/src/honcho/peer.py
index ea71fc79..5984737b 100644
--- a/sdks/python/src/honcho/peer.py
+++ b/sdks/python/src/honcho/peer.py
@@ -5,6 +5,7 @@ from __future__ import annotations
import datetime
import logging
+import warnings
from collections.abc import Generator
from typing import TYPE_CHECKING, Any, Literal
@@ -446,7 +447,7 @@ class Peer(PeerBase, MetadataConfigMixin):
]
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
- def card(
+ def get_card(
self,
target: str | PeerBase | None = None,
) -> list[str] | None:
@@ -476,6 +477,50 @@ class Peer(PeerBase, MetadataConfigMixin):
return response.peer_card
+ def card(
+ self,
+ target: str | PeerBase | None = None,
+ ) -> list[str] | None:
+ """Deprecated: use get_card() instead."""
+ warnings.warn(
+ "card() is deprecated, use get_card() instead",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self.get_card(target=target)
+
+ @validate_call(config=ConfigDict(arbitrary_types_allowed=True))
+ def set_card(
+ self,
+ peer_card: list[str],
+ target: str | PeerBase | None = None,
+ ) -> list[str] | None:
+ """
+ Set the peer card for this peer.
+
+ Makes an API call to set the peer card. If a target is provided, sets this
+ peer's local card of the target peer.
+
+ Args:
+ peer_card: A list of strings to set as the peer card.
+ target: Optional target peer for local card. If provided, sets this
+ peer's card of the target peer. Can be a Peer object or peer ID string.
+
+ Returns:
+ A list of strings representing the updated peer card, or None if none is available
+ """
+ self._honcho._ensure_workspace()
+ target_id = resolve_id(target)
+
+ query = {"target": target_id} if target_id else None
+ data = self._honcho._http.put(
+ routes.peer_card(self.workspace_id, self.id),
+ body={"peer_card": peer_card},
+ query=query,
+ )
+ response = PeerCardResponse.model_validate(data)
+ return response.peer_card
+
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
def representation(
self,
diff --git a/sdks/typescript/__tests__/peer.test.ts b/sdks/typescript/__tests__/peer.test.ts
index 9a3b3b61..77953168 100644
--- a/sdks/typescript/__tests__/peer.test.ts
+++ b/sdks/typescript/__tests__/peer.test.ts
@@ -389,6 +389,9 @@ describe('Peer', () => {
describe('POST /peers/:id/card', () => {
test('card returns string array or null', async () => {
const peer = await client.peer('card-peer')
+ const session = await client.session('card-session')
+ await session.addPeers([peer.id])
+ await session.addMessages([peer.message('hello')])
const card = await peer.card()
@@ -399,6 +402,9 @@ describe('Peer', () => {
test('card with target peer', async () => {
const observer = await client.peer('card-observer')
const observed = await client.peer('card-observed')
+ const session = await client.session('card-target-session')
+ await session.addPeers([observer.id, observed.id])
+ await session.addMessages([observer.message('hello'), observed.message('hi')])
const card = await observer.card(observed)
@@ -408,6 +414,10 @@ describe('Peer', () => {
test('card with target ID string', async () => {
const peer = await client.peer('card-string-peer')
+ const target = await client.peer('some-target-id')
+ const session = await client.session('card-string-session')
+ await session.addPeers([peer.id, target.id])
+ await session.addMessages([peer.message('hello'), target.message('hi')])
const card = await peer.card('some-target-id')
@@ -430,6 +440,48 @@ describe('Peer', () => {
})
})
+ // ===========================================================================
+ // Set Peer Card (PUT /peers/:id/card)
+ // ===========================================================================
+
+ describe('PUT /peers/:id/card', () => {
+ test('setCard sets and returns peer card', async () => {
+ const peer = await client.peer('setcard-peer')
+
+ const cardData = ['fact one', 'fact two']
+ const result = await peer.setCard(cardData)
+
+ expect(result).toEqual(cardData)
+
+ // Verify with get
+ const card = await peer.getCard()
+ expect(card).toEqual(cardData)
+ })
+
+ test('setCard with target peer', async () => {
+ const observer = await client.peer('setcard-observer')
+ const observed = await client.peer('setcard-observed')
+
+ const cardData = ['target likes TypeScript', 'target is clever']
+ const result = await observer.setCard(cardData, observed)
+
+ expect(result).toEqual(cardData)
+
+ // Verify with get
+ const card = await observer.getCard(observed)
+ expect(card).toEqual(cardData)
+ })
+
+ test('setCard with target ID string', async () => {
+ const peer = await client.peer('setcard-string-peer')
+
+ const cardData = ['some fact']
+ const result = await peer.setCard(cardData, 'setcard-string-target')
+
+ expect(result).toEqual(cardData)
+ })
+ })
+
// ===========================================================================
// Peer Context (POST /peers/:id/context)
// ===========================================================================
@@ -437,6 +489,9 @@ describe('Peer', () => {
describe('POST /peers/:id/context', () => {
test('context returns representation and card', async () => {
const peer = await client.peer('context-peer')
+ const session = await client.session('context-session')
+ await session.addPeers([peer.id])
+ await session.addMessages([peer.message('hello')])
const context = await peer.context()
@@ -450,6 +505,9 @@ describe('Peer', () => {
test('context with target peer', async () => {
const observer = await client.peer('context-observer')
const observed = await client.peer('context-observed')
+ const session = await client.session('context-target-session')
+ await session.addPeers([observer.id, observed.id])
+ await session.addMessages([observer.message('hello'), observed.message('hi')])
const context = await observer.context({ target: observed })
@@ -459,6 +517,9 @@ describe('Peer', () => {
test('context with options', async () => {
const peer = await client.peer('context-options-peer')
+ const session = await client.session('context-options-session')
+ await session.addPeers([peer.id])
+ await session.addMessages([peer.message('hello')])
const context = await peer.context({
searchQuery: 'interests',
diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json
index 956066ed..618a0acf 100644
--- a/sdks/typescript/package.json
+++ b/sdks/typescript/package.json
@@ -1,6 +1,6 @@
{
"name": "@honcho-ai/sdk",
- "version": "2.0.0",
+ "version": "2.0.1",
"description": "Official DX Optimized TypeScript SDK for Honcho",
"author": "Plastic Labs ",
"license": "Apache-2.0",
diff --git a/sdks/typescript/src/peer.ts b/sdks/typescript/src/peer.ts
index 2ce07468..65567ad5 100644
--- a/sdks/typescript/src/peer.ts
+++ b/sdks/typescript/src/peer.ts
@@ -28,6 +28,7 @@ import {
MessageConfigurationSchema,
MessageContentSchema,
MessageMetadataSchema,
+ PeerCardContentSchema,
type PeerConfig,
PeerConfigSchema,
PeerGetRepresentationParamsSchema,
@@ -295,6 +296,18 @@ export class Peer {
)
}
+ private async _setCard(params: {
+ peer_card: string[]
+ target?: string
+ }): Promise {
+ await this._ensureWorkspace()
+ const { peer_card, ...query } = params
+ return this._http.put(
+ `/${API_VERSION}/workspaces/${this.workspaceId}/peers/${this.id}/card`,
+ { body: { peer_card }, query }
+ )
+ }
+
// ===========================================================================
// Public Methods
// ===========================================================================
@@ -652,7 +665,7 @@ export class Peer {
* @returns Promise resolving to an array of strings containing the peer card items,
* or null if no peer card exists
*/
- async card(target?: string | Peer): Promise {
+ async getCard(target?: string | Peer): Promise {
const validatedTarget = CardTargetSchema.parse(target)
const response = await this._getCard({
@@ -662,6 +675,38 @@ export class Peer {
return response.peer_card
}
+ /**
+ * @deprecated Use {@link getCard} instead.
+ */
+ async card(target?: string | Peer): Promise {
+ return this.getCard(target)
+ }
+
+ /**
+ * Set the peer card for this peer.
+ *
+ * Makes an API call to set the peer card. If a target is provided, sets this
+ * peer's local card of the target peer.
+ *
+ * @param peerCard - An array of strings to set as the peer card.
+ * @param target - Optional target peer for local card. If provided, sets this
+ * peer's card of the target peer. Can be a Peer object or peer ID string.
+ * @returns Promise resolving to an array of strings containing the updated peer card items,
+ * or null if no peer card exists
+ */
+ async setCard(
+ peerCard: string[],
+ target?: string | Peer
+ ): Promise {
+ const validatedPeerCard = PeerCardContentSchema.parse(peerCard)
+ const validatedTarget = CardTargetSchema.parse(target)
+ const response = await this._setCard({
+ peer_card: validatedPeerCard,
+ target: validatedTarget,
+ })
+ return response.peer_card
+ }
+
/**
* Get a subset of Honcho's Representation of a peer.
*
diff --git a/sdks/typescript/src/validation.ts b/sdks/typescript/src/validation.ts
index 3a5e8584..829699ca 100644
--- a/sdks/typescript/src/validation.ts
+++ b/sdks/typescript/src/validation.ts
@@ -376,6 +376,11 @@ export const CardTargetSchema = z
val ? (typeof val === 'string' ? val : val.id) : undefined
)
+/**
+ * Schema for peer card content (array of strings).
+ */
+export const PeerCardContentSchema = z.array(z.string())
+
/**
* Schema for peer addition to session.
*/
diff --git a/src/crud/peer_card.py b/src/crud/peer_card.py
index f04205ed..37c2749c 100644
--- a/src/crud/peer_card.py
+++ b/src/crud/peer_card.py
@@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from src import exceptions, models, schemas
from src.cache.client import cache
-from src.crud.peer import get_peer, peer_cache_key
+from src.crud.peer import get_or_create_peers, get_peer, peer_cache_key
logger = logging.getLogger(__name__)
@@ -32,18 +32,18 @@ async def get_peer_card(
observer: Peer name of the observer
Returns:
- The peer's card text if present, otherwise None (also None if peer not found).
+ The peer's card text if present, otherwise None.
+
+ Raises:
+ ResourceNotFoundException: If the peer does not exist.
"""
- try:
- peer = await get_peer(db, workspace_name, schemas.PeerCreate(name=observer))
- return cast(
- list[str] | None,
- peer.internal_metadata.get(
- construct_peer_card_label(observer=observer, observed=observed)
- ),
- )
- except exceptions.ResourceNotFoundException:
- return None
+ peer = await get_peer(db, workspace_name, schemas.PeerCreate(name=observer))
+ return cast(
+ list[str] | None,
+ peer.internal_metadata.get(
+ construct_peer_card_label(observer=observer, observed=observed)
+ ),
+ )
async def set_peer_card(
@@ -66,9 +66,10 @@ async def set_peer_card(
observed: Peer name of the peer described in the peer card
observer: Peer name of the observer
- Raises:
- ResourceNotFoundException: If the peer does not exist
"""
+ # Ensure the peer exists (get-or-create)
+ await get_or_create_peers(db, workspace_name, [schemas.PeerCreate(name=observer)])
+
stmt = (
update(models.Peer)
.where(models.Peer.workspace_name == workspace_name)
@@ -82,11 +83,9 @@ async def set_peer_card(
}
)
)
- .returning(models.Peer)
)
result = await db.execute(stmt)
- updated_peer = result.scalar_one_or_none()
- if updated_peer is None:
+ if result.rowcount == 0:
raise exceptions.ResourceNotFoundException(
f"Peer {observer} not found in workspace {workspace_name}"
)
diff --git a/src/dialectic/chat.py b/src/dialectic/chat.py
index e2783db8..5ea0e11f 100644
--- a/src/dialectic/chat.py
+++ b/src/dialectic/chat.py
@@ -8,7 +8,7 @@ using the DialecticAgent.
import logging
from collections.abc import AsyncIterator
-from src import crud
+from src import crud, schemas
from src.config import ReasoningLevel
from src.dependencies import tracked_db
from src.dialectic.core import DialecticAgent
@@ -40,6 +40,11 @@ async def agentic_chat(
The synthesized answer string
"""
async with tracked_db("dialectic.agentic_chat") as db:
+ # Validate that the peers exist before proceeding
+ await crud.get_peer(db, workspace_name, schemas.PeerCreate(name=observer))
+ if observer != observed:
+ await crud.get_peer(db, workspace_name, schemas.PeerCreate(name=observed))
+
# Resolve configuration to check if peer cards should be used
session = None
if session_name:
@@ -101,6 +106,11 @@ async def agentic_chat_stream(
Chunks of the response text as they are generated
"""
async with tracked_db("dialectic.agentic_chat_stream") as db:
+ # Validate that the peers exist before proceeding
+ await crud.get_peer(db, workspace_name, schemas.PeerCreate(name=observer))
+ if observer != observed:
+ await crud.get_peer(db, workspace_name, schemas.PeerCreate(name=observed))
+
# Resolve configuration to check if peer cards should be used
session = None
if session_name:
diff --git a/src/dreamer/specialists.py b/src/dreamer/specialists.py
index 0c499594..cb690420 100644
--- a/src/dreamer/specialists.py
+++ b/src/dreamer/specialists.py
@@ -20,7 +20,7 @@ from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
-from src import crud
+from src import crud, schemas
from src.config import settings
from src.schemas import ResolvedConfiguration
from src.telemetry import prometheus_metrics
@@ -142,6 +142,11 @@ class BaseSpecialist(ABC):
task_name = f"dreamer_{self.name}_{run_id}"
start_time = time.perf_counter()
+ # Validate that the peers exist before proceeding
+ await crud.get_peer(db, workspace_name, schemas.PeerCreate(name=observer))
+ if observer != observed:
+ await crud.get_peer(db, workspace_name, schemas.PeerCreate(name=observed))
+
# Determine if peer card tools should be included
peer_card_enabled = configuration is None or configuration.peer_card.create
diff --git a/src/routers/peers.py b/src/routers/peers.py
index 3bff98a3..e9eea331 100644
--- a/src/routers/peers.py
+++ b/src/routers/peers.py
@@ -338,11 +338,7 @@ async def set_peer_card(
observed=observed,
)
- # Return the updated peer card
- peer_card = await crud.get_peer_card(
- db, workspace_id, observer=peer_id, observed=observed
- )
- return schemas.PeerCardResponse(peer_card=peer_card)
+ return schemas.PeerCardResponse(peer_card=peer_card_data.peer_card)
@router.get(
diff --git a/tests/crud/test_peer_card.py b/tests/crud/test_peer_card.py
index c057323b..ada64bd9 100644
--- a/tests/crud/test_peer_card.py
+++ b/tests/crud/test_peer_card.py
@@ -51,62 +51,30 @@ async def test_peer_card_get_set_roundtrip(
@pytest.mark.asyncio
-async def test_set_peer_card_missing_peer_raises(
+async def test_get_peer_card_missing_peer_raises(
db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer]
):
- """Setting a peer card for a non-existent peer should raise ResourceNotFoundException."""
+ """Getting a peer card for a non-existent peer should raise ResourceNotFoundException."""
workspace, _existing_peer = sample_data
with pytest.raises(ResourceNotFoundException):
- await set_peer_card(
+ await get_peer_card(
db_session,
workspace.name,
- ["card"],
observer="missing-peer",
observed="missing-peer",
)
@pytest.mark.asyncio
-async def test_get_peer_card_missing_peer_returns_none(
+async def test_get_peer_card_missing_workspace_raises(
db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer]
):
- """Getting a peer card for a non-existent peer should return None."""
- workspace, _existing_peer = sample_data
- result = await get_peer_card(
- db_session,
- workspace.name,
- observer="missing-peer",
- observed="missing-peer",
- )
- assert result is None
-
-
-@pytest.mark.asyncio
-async def test_get_peer_card_missing_workspace_returns_none(
- db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer]
-):
- """Getting a peer card for a non-existent workspace should return None."""
- _workspace, peer = sample_data
- result = await get_peer_card(
- db_session,
- "missing-workspace",
- observer=peer.name,
- observed=peer.name,
- )
- assert result is None
-
-
-@pytest.mark.asyncio
-async def test_set_peer_card_missing_workspace_raises(
- db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer]
-):
- """Setting a peer card for a non-existent workspace should raise ResourceNotFoundException."""
+ """Getting a peer card for a non-existent workspace should raise ResourceNotFoundException."""
_workspace, peer = sample_data
with pytest.raises(ResourceNotFoundException):
- await set_peer_card(
+ await get_peer_card(
db_session,
"missing-workspace",
- ["card"],
observer=peer.name,
observed=peer.name,
)
diff --git a/tests/routes/test_peers.py b/tests/routes/test_peers.py
index 84f23887..c8047f02 100644
--- a/tests/routes/test_peers.py
+++ b/tests/routes/test_peers.py
@@ -909,3 +909,52 @@ async def test_get_peer_card_with_data(
assert response.status_code == 200
data = response.json()
assert data["peer_card"] == target_card_content
+
+
+def test_set_peer_card(client: TestClient, sample_data: tuple[Workspace, Peer]):
+ """Test setting peer cards via the PUT endpoint."""
+ test_workspace, observer_peer = sample_data
+
+ # Create a target peer
+ target_peer_name = str(generate_nanoid())
+ response = client.post(
+ f"/v3/workspaces/{test_workspace.name}/peers",
+ json={"name": target_peer_name},
+ )
+ assert response.status_code in [200, 201]
+
+ # Set the observer's own card
+ self_card = ["I am a test peer", "I like writing tests"]
+ response = client.put(
+ f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card",
+ json={"peer_card": self_card},
+ )
+ assert response.status_code == 200
+ data = response.json()
+ assert data["peer_card"] == self_card
+
+ # Verify with GET
+ response = client.get(
+ f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card"
+ )
+ assert response.status_code == 200
+ assert response.json()["peer_card"] == self_card
+
+ # Set a card for the target peer
+ target_card = ["Target is helpful", "Target knows Python"]
+ response = client.put(
+ f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card",
+ params={"target": target_peer_name},
+ json={"peer_card": target_card},
+ )
+ assert response.status_code == 200
+ data = response.json()
+ assert data["peer_card"] == target_card
+
+ # Verify with GET
+ response = client.get(
+ f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card",
+ params={"target": target_peer_name},
+ )
+ assert response.status_code == 200
+ assert response.json()["peer_card"] == target_card
diff --git a/tests/sdk/sdk_integration_test.py b/tests/sdk/sdk_integration_test.py
index f5a14fe8..a8a90203 100644
--- a/tests/sdk/sdk_integration_test.py
+++ b/tests/sdk/sdk_integration_test.py
@@ -104,3 +104,41 @@ def test_message_and_chat_operations(honcho_test_client: Honcho):
# This is a mock response from the agent
_response = user.chat("What did I ask about?")
+
+
+def test_peer_card_operations(honcho_test_client: Honcho):
+ """
+ Tests setting and getting peer cards.
+ """
+ peer = honcho_test_client.peer(id="card-test-peer")
+ target = honcho_test_client.peer(id="card-test-target")
+
+ # Create the peers on the server by adding a message
+ session = honcho_test_client.session(id="card-test-session")
+ session.add_messages([peer.message("hello"), target.message("hi")])
+
+ # Initially card should be None
+ card = peer.get_card()
+ assert card is None
+
+ # Set own card
+ own_card = ["I am a helpful assistant", "I enjoy learning"]
+ result = peer.set_card(own_card)
+ assert result == own_card
+
+ # Verify with get
+ card = peer.get_card()
+ assert card == own_card
+
+ # Set card for target
+ target_card = ["Target likes Python", "Target is friendly"]
+ result = peer.set_card(target_card, target=target)
+ assert result == target_card
+
+ # Verify with get
+ card = peer.get_card(target=target)
+ assert card == target_card
+
+ # Own card should still be unchanged
+ card = peer.get_card()
+ assert card == own_card
diff --git a/tests/sdk/test_peer.py b/tests/sdk/test_peer.py
index dae19398..258fb989 100644
--- a/tests/sdk/test_peer.py
+++ b/tests/sdk/test_peer.py
@@ -93,7 +93,7 @@ async def test_peer_card_global(client_fixture: tuple[Honcho, str]):
await session.aio.add_messages([peer.message("I like pizza")])
# Get global peer card
- card_response = await peer.aio.card()
+ card_response = await peer.aio.get_card()
assert card_response is None or isinstance(card_response, list)
else:
peer = honcho_client.peer(id="test-card-global-peer")
@@ -103,7 +103,7 @@ async def test_peer_card_global(client_fixture: tuple[Honcho, str]):
session.add_messages([peer.message("I like pizza")])
# Get global peer card
- card_response = peer.card()
+ card_response = peer.get_card()
assert card_response is None or isinstance(card_response, list)
@@ -125,11 +125,11 @@ async def test_peer_card_local(client_fixture: tuple[Honcho, str]):
)
# Get local peer card with target as Peer object
- card_response = await observer.aio.card(target=target)
+ card_response = await observer.aio.get_card(target=target)
assert card_response is None or isinstance(card_response, list)
# Get local peer card with target as string
- card_response = await observer.aio.card(target=target.id)
+ card_response = await observer.aio.get_card(target=target.id)
assert card_response is None or isinstance(card_response, list)
else:
observer = honcho_client.peer(id="test-card-local-observer")
@@ -140,11 +140,11 @@ async def test_peer_card_local(client_fixture: tuple[Honcho, str]):
session.add_messages([observer.message("Hello"), target.message("Hi there")])
# Get local peer card with target as Peer object
- card_response = observer.card(target=target)
+ card_response = observer.get_card(target=target)
assert card_response is None or isinstance(card_response, list)
# Get local peer card with target as string
- card_response = observer.card(target=target.id)
+ card_response = observer.get_card(target=target.id)
assert card_response is None or isinstance(card_response, list)
@@ -159,15 +159,23 @@ async def test_peer_card_with_empty_target(client_fixture: tuple[Honcho, str]):
if client_type == "async":
peer = await honcho_client.aio.peer(id="test-card-validation-peer")
+ session = await honcho_client.aio.session(id="test-card-validation-session")
+
+ # Create the peer on the server by adding a message
+ await session.aio.add_messages([peer.message("hello")])
# Empty target is treated as no target (same as None)
- result = await peer.aio.card(target="")
+ result = await peer.aio.get_card(target="")
assert result is None or isinstance(result, list)
else:
peer = honcho_client.peer(id="test-card-validation-peer")
+ session = honcho_client.session(id="test-card-validation-session")
+
+ # Create the peer on the server by adding a message
+ session.add_messages([peer.message("hello")])
# Empty target is treated as no target (same as None)
- result = peer.card(target="")
+ result = peer.get_card(target="")
assert result is None or isinstance(result, list)
diff --git a/tests/unified/runner.py b/tests/unified/runner.py
index 2168d1c4..0d07d5fe 100644
--- a/tests/unified/runner.py
+++ b/tests/unified/runner.py
@@ -345,7 +345,7 @@ class UnifiedTestExecutor:
raise ValueError("peer_id required for get_peer_card")
peer = await self.client.aio.peer(id=step.observer_peer_id)
- card = await peer.aio.card(
+ card = await peer.aio.get_card(
step.observed_peer_id
if step.observed_peer_id
else step.observer_peer_id
diff --git a/uv.lock b/uv.lock
index 8817b2e7..ec6c0ab0 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1147,7 +1147,7 @@ dev = [
[[package]]
name = "honcho-ai"
-version = "2.0.0"
+version = "2.0.1"
source = { editable = "sdks/python" }
dependencies = [
{ name = "httpx" },