diff --git a/.gitignore b/.gitignore
index cc184613..8e15cb72 100644
--- a/.gitignore
+++ b/.gitignore
@@ -185,3 +185,4 @@ config.toml
CRUSH.md
.crush/
+AGENTS.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b5ff0178..c6de328b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
+## [2.3.3] - 2025-10-01
+
+### Fixed
+
+- Dialectic Streaming to follow SSE conventions
+- Sentry tracing in the deriver
+
## [2.3.2] - 2025-09-25
### Added
diff --git a/README.md b/README.md
index bb9cfab7..28a156bd 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
---
-
+
[](https://pypi.org/project/honcho-ai/)
[](https://npmjs.org/package/@honcho-ai/sdk)
[](https://discord.gg/plasticlabs)
diff --git a/docs/changelog/compatibility-guide.mdx b/docs/changelog/compatibility-guide.mdx
index feb857dd..b98608c3 100644
--- a/docs/changelog/compatibility-guide.mdx
+++ b/docs/changelog/compatibility-guide.mdx
@@ -8,23 +8,23 @@ This guide helps you understand which versions of Honcho's API are compatible wi
## Version Compatibility
-### Honcho API v2.3.2 (Current)
+### Honcho API v2.3.3 (Current)
- **Compatible Version:** v1.4.0
+ **Compatible Version:** v1.4.1
Install with:
```bash
- npm install @honcho-ai/sdk@1.4.0
+ npm install @honcho-ai/sdk@1.4.1
```
- **Compatible Version:** v1.4.0
+ **Compatible Version:** v1.4.1
Install with:
```bash
- pip install honcho-ai==1.4.0
+ pip install honcho-ai==1.4.1
```
@@ -35,7 +35,9 @@ This guide helps you understand which versions of Honcho's API are compatible wi
| Honcho API Version | TypeScript SDK | Python SDK |
|-------------------|---------------|------------|
-| v2.3.2 (Current) | v1.4.0 | v1.4.0 |
+| v2.3.3 (Current) | v1.4.1 | v1.4.1 |
+| v2.3.2 | v1.4.0 | v1.4.0 |
+| v2.3.1 | v1.4.0 | v1.4.0 |
| v2.3.0 | v1.4.0 | v1.4.0 |
| v2.2.0 | v1.3.0 | v1.3.0 |
| v2.1.1 | v1.2.1 | v1.2.2 |
diff --git a/docs/changelog/introduction.mdx b/docs/changelog/introduction.mdx
index cadaa6f8..c9c80059 100644
--- a/docs/changelog/introduction.mdx
+++ b/docs/changelog/introduction.mdx
@@ -27,7 +27,14 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
### Honcho API and SDK Changelogs
-
+
+ ### Fixed
+
+ - Dialectic Streaming to follow SSE conventions
+ - Sentry tracing in the deriver
+
+
+
### Added
- Get peer cards endpoint (`GET /v2/peers/{peer_id}/peer-card`) for retrieving targeted peer context information
diff --git a/docs/docs.json b/docs/docs.json
index ec44ab99..c838da84 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -9,21 +9,14 @@
},
"favicon": "/favicon.svg",
"contextual": {
- "options": [
- "copy",
- "view",
- "chatgpt",
- "claude"
- ]
+ "options": ["copy", "view", "chatgpt", "claude"]
},
"navigation": {
"versions": [
{
- "version": "v2.3.2",
+ "version": "v2.3.3",
"api": {
- "openapi": [
- "openapi.documented.yml"
- ]
+ "openapi": ["openapi.documented.yml"]
},
"tabs": [
{
@@ -61,17 +54,11 @@
"groups": [
{
"group": "Getting Started",
- "pages": [
- "v2/guides/overview",
- "v2/guides/mcp"
- ]
+ "pages": ["v2/guides/overview", "v2/guides/mcp"]
},
{
"group": "Application Interfaces",
- "pages": [
- "v2/guides/discord",
- "v2/guides/telegram"
- ]
+ "pages": ["v2/guides/discord", "v2/guides/telegram"]
},
{
"group": "Design Patterns",
@@ -106,9 +93,7 @@
"groups": [
{
"group": "API Documentation",
- "pages": [
- "v2/api-reference/introduction"
- ]
+ "pages": ["v2/api-reference/introduction"]
},
{
"group": "workspaces",
@@ -129,7 +114,8 @@
"v2/api-reference/endpoint/peers/get-sessions-for-peer",
"v2/api-reference/endpoint/peers/chat",
"v2/api-reference/endpoint/peers/get-working-representation",
- "v2/api-reference/endpoint/peers/search-peer"
+ "v2/api-reference/endpoint/peers/search-peer",
+ "v2/api-reference/endpoint/peers/get-peer-card"
]
},
{
@@ -163,9 +149,7 @@
},
{
"group": "keys",
- "pages": [
- "v2/api-reference/endpoint/keys/create-key"
- ]
+ "pages": ["v2/api-reference/endpoint/keys/create-key"]
},
{
"group": "webhooks",
@@ -195,9 +179,7 @@
{
"version": "v1.1.0",
"api": {
- "openapi": [
- "openapi.json"
- ]
+ "openapi": ["openapi.json"]
},
"tabs": [
{
@@ -227,23 +209,15 @@
"groups": [
{
"group": "Getting Started",
- "pages": [
- "v1/guides/overview",
- "v1/guides/streaming-response"
- ]
+ "pages": ["v1/guides/overview", "v1/guides/streaming-response"]
},
{
"group": "Application Interfaces",
- "pages": [
- "v1/guides/discord",
- "v1/guides/honcho-mcp"
- ]
+ "pages": ["v1/guides/discord", "v1/guides/honcho-mcp"]
},
{
"group": "Personal Memory",
- "pages": [
- "v1/guides/dialectic-endpoint"
- ]
+ "pages": ["v1/guides/dialectic-endpoint"]
}
]
},
@@ -252,9 +226,7 @@
"groups": [
{
"group": "API Documentation",
- "pages": [
- "v1/api-reference/introduction"
- ]
+ "pages": ["v1/api-reference/introduction"]
},
{
"group": "apps",
@@ -302,9 +274,7 @@
},
{
"group": "keys",
- "pages": [
- "v1/api-reference/endpoint/keys/create-key"
- ]
+ "pages": ["v1/api-reference/endpoint/keys/create-key"]
},
{
"group": "metamessages",
diff --git a/docs/v2/api-reference/endpoint/peers/get-peer-card.mdx b/docs/v2/api-reference/endpoint/peers/get-peer-card.mdx
new file mode 100644
index 00000000..ef12d719
--- /dev/null
+++ b/docs/v2/api-reference/endpoint/peers/get-peer-card.mdx
@@ -0,0 +1,3 @@
+---
+openapi: get /v2/workspaces/{workspace_id}/peers/{peer_id}/card
+---
diff --git a/docs/v2/openapi.documented.yml b/docs/v2/openapi.documented.yml
index 174cbb63..876e5069 100644
--- a/docs/v2/openapi.documented.yml
+++ b/docs/v2/openapi.documented.yml
@@ -7,7 +7,7 @@ info:
name: Plastic Labs
url: https://honcho.dev/
email: hello@plasticlabs.ai
- version: 2.3.0
+ version: 2.3.3
servers:
- url: http://localhost:8000
description: Local Development Server
@@ -31,22 +31,22 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/WorkspaceCreate'
+ $ref: "#/components/schemas/WorkspaceCreate"
description: Workspace creation parameters
required: true
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Workspace'
- '422':
+ $ref: "#/components/schemas/Workspace"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
security:
- HTTPBearer: []
- {}
@@ -110,23 +110,23 @@ paths:
application/json:
schema:
anyOf:
- - $ref: '#/components/schemas/WorkspaceGet'
- - type: 'null'
+ - $ref: "#/components/schemas/WorkspaceGet"
+ - type: "null"
description: Filtering and pagination options for the workspaces list
title: Options
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Page_Workspace_'
- '422':
+ $ref: "#/components/schemas/Page_Workspace_"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -174,21 +174,21 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/WorkspaceUpdate'
+ $ref: "#/components/schemas/WorkspaceUpdate"
description: Updated workspace parameters
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Workspace'
- '422':
+ $ref: "#/components/schemas/Workspace"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -236,24 +236,24 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/MessageSearchOptions'
- description: 'Message search parameters '
+ $ref: "#/components/schemas/MessageSearchOptions"
+ description: "Message search parameters "
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
type: array
items:
- $ref: '#/components/schemas/Message'
+ $ref: "#/components/schemas/Message"
title: Response Search Workspace V2 Workspaces Workspace Id Search Post
- '422':
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -303,7 +303,7 @@ paths:
schema:
anyOf:
- type: string
- - type: 'null'
+ - type: "null"
description: Optional observer ID to filter by
title: Observer Id
description: Optional observer ID to filter by
@@ -313,7 +313,7 @@ paths:
schema:
anyOf:
- type: string
- - type: 'null'
+ - type: "null"
description: Optional sender ID to filter by
title: Sender Id
description: Optional sender ID to filter by
@@ -323,23 +323,23 @@ paths:
schema:
anyOf:
- type: string
- - type: 'null'
+ - type: "null"
description: Optional session ID to filter by
title: Session Id
description: Optional session ID to filter by
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/DeriverStatus'
- '422':
+ $ref: "#/components/schemas/DeriverStatus"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -408,23 +408,23 @@ paths:
application/json:
schema:
anyOf:
- - $ref: '#/components/schemas/PeerGet'
- - type: 'null'
+ - $ref: "#/components/schemas/PeerGet"
+ - type: "null"
description: Filtering options for the peers list
title: Options
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Page_Peer_'
- '422':
+ $ref: "#/components/schemas/Page_Peer_"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -478,21 +478,21 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/PeerCreate'
+ $ref: "#/components/schemas/PeerCreate"
description: Peer creation parameters
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Peer'
- '422':
+ $ref: "#/components/schemas/Peer"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -549,21 +549,21 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/PeerUpdate'
+ $ref: "#/components/schemas/PeerUpdate"
description: Updated peer parameters
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Peer'
- '422':
+ $ref: "#/components/schemas/Peer"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -641,23 +641,23 @@ paths:
application/json:
schema:
anyOf:
- - $ref: '#/components/schemas/SessionGet'
- - type: 'null'
+ - $ref: "#/components/schemas/SessionGet"
+ - type: "null"
description: Filtering options for the sessions list
title: Options
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Page_Session_'
- '422':
+ $ref: "#/components/schemas/Page_Session_"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -715,22 +715,29 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/DialecticOptions'
+ $ref: "#/components/schemas/DialecticOptions"
description: Dialectic Endpoint Parameters
responses:
- '200':
+ "200":
description: Response to a question informed by Honcho's User Representation
content:
application/json:
schema:
- $ref: '#/components/schemas/DialecticResponse'
+ properties:
+ content:
+ title: Content
+ type: string
+ required:
+ - content
+ title: DialecticResponse
+ type: object
text/event-stream: {}
- '422':
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -797,10 +804,10 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/PeerRepresentationGet'
+ $ref: "#/components/schemas/PeerRepresentationGet"
description: Options for getting the peer representation
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
@@ -810,12 +817,12 @@ paths:
title: >-
Response Get Working Representation V2 Workspaces Workspace Id Peers Peer Id
Representation Post
- '422':
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -843,6 +850,84 @@ paths:
session_id="session_id",
)
print(response)
+ /v2/workspaces/{workspace_id}/peers/{peer_id}/card:
+ get:
+ tags:
+ - peers
+ summary: Get Peer Card
+ description: |-
+ Get a peer card for a specific peer relationship.
+
+ Returns the peer card that the observer peer has for the target peer if it exists.
+ If no target is specified, returns the observer's own peer card.
+ operationId: get_peer_card_v2_workspaces__workspace_id__peers__peer_id__card_get
+ security:
+ - HTTPBearer: []
+ - {}
+ parameters:
+ - name: workspace_id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: ID of the workspace
+ title: Workspace Id
+ description: ID of the workspace
+ - name: peer_id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: ID of the observer peer
+ title: Peer Id
+ description: ID of the observer peer
+ - name: target
+ in: query
+ required: false
+ schema:
+ anyOf:
+ - type: string
+ - type: "null"
+ description: The peer whose card to retrieve. If not provided, returns the observer's own card
+ title: Target
+ description: The peer whose card to retrieve. If not provided, returns the observer's own card
+ responses:
+ "200":
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PeerCardResponse"
+ "422":
+ description: Validation Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/HTTPValidationError"
+ x-codeSamples:
+ - lang: JavaScript
+ source: |-
+ import Honcho from '@honcho-ai/core';
+
+ const client = new Honcho({
+ apiKey: 'My API Key',
+ });
+
+ const response = await client.workspaces.peers.card('workspace_id', 'peer_id');
+
+ console.log(response.peer_card);
+ - lang: Python
+ source: |-
+ from honcho_core import Honcho
+
+ client = Honcho(
+ api_key="My API Key",
+ )
+ response = client.workspaces.peers.card(
+ peer_id="peer_id",
+ workspace_id="workspace_id",
+ )
+ print(response.peer_card)
/v2/workspaces/{workspace_id}/peers/{peer_id}/search:
post:
tags:
@@ -875,24 +960,24 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/MessageSearchOptions'
- description: 'Message search parameters '
+ $ref: "#/components/schemas/MessageSearchOptions"
+ description: "Message search parameters "
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
type: array
items:
- $ref: '#/components/schemas/Message'
+ $ref: "#/components/schemas/Message"
title: Response Search Peer V2 Workspaces Workspace Id Peers Peer Id Search Post
- '422':
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: >-
@@ -950,21 +1035,21 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/SessionCreate'
+ $ref: "#/components/schemas/SessionCreate"
description: Session creation parameters
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Session'
- '422':
+ $ref: "#/components/schemas/Session"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -1034,23 +1119,23 @@ paths:
application/json:
schema:
anyOf:
- - $ref: '#/components/schemas/SessionGet'
- - type: 'null'
+ - $ref: "#/components/schemas/SessionGet"
+ - type: "null"
description: Filtering and pagination options for the sessions list
title: Options
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Page_Session_'
- '422':
+ $ref: "#/components/schemas/Page_Session_"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -1108,21 +1193,21 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/SessionUpdate'
+ $ref: "#/components/schemas/SessionUpdate"
description: Updated session parameters
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Session'
- '422':
+ $ref: "#/components/schemas/Session"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -1174,17 +1259,17 @@ paths:
title: Session Id
description: ID of the session to delete
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema: {}
- '422':
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -1242,23 +1327,23 @@ paths:
schema:
anyOf:
- type: string
- - type: 'null'
+ - type: "null"
description: Message ID to cut off the clone at
title: Message Id
description: Message ID to cut off the clone at
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Session'
- '422':
+ $ref: "#/components/schemas/Session"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -1317,22 +1402,22 @@ paths:
schema:
type: object
additionalProperties:
- $ref: '#/components/schemas/SessionPeerConfig'
+ $ref: "#/components/schemas/SessionPeerConfig"
description: List of peer IDs to add to the session
title: Peers
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Session'
- '422':
+ $ref: "#/components/schemas/Session"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: >-
@@ -1397,22 +1482,22 @@ paths:
schema:
type: object
additionalProperties:
- $ref: '#/components/schemas/SessionPeerConfig'
+ $ref: "#/components/schemas/SessionPeerConfig"
description: List of peer IDs to set for the session
title: Peers
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Session'
- '422':
+ $ref: "#/components/schemas/Session"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: >-
@@ -1481,18 +1566,18 @@ paths:
description: List of peer IDs to remove from the session
title: Peers
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Session'
- '422':
+ $ref: "#/components/schemas/Session"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: >-
@@ -1570,18 +1655,18 @@ paths:
title: Size
description: Page size
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Page_Peer_'
- '422':
+ $ref: "#/components/schemas/Page_Peer_"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -1644,18 +1729,18 @@ paths:
title: Peer Id
description: ID of the peer
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/SessionPeerConfig'
- '422':
+ $ref: "#/components/schemas/SessionPeerConfig"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -1724,20 +1809,20 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/SessionPeerConfig'
+ $ref: "#/components/schemas/SessionPeerConfig"
description: Peer configuration
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema: {}
- '422':
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: >-
@@ -1811,7 +1896,7 @@ paths:
anyOf:
- type: integer
maximum: 100000
- - type: 'null'
+ - type: "null"
description: >-
Number of tokens to use for the context. Includes summary if set to true. If not provided, the
context will be exhaustive (within 100000 tokens)
@@ -1829,18 +1914,18 @@ paths:
title: Summary
description: Whether or not to include a summary *if* one is available for the session
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/SessionContext'
- '422':
+ $ref: "#/components/schemas/SessionContext"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -1897,18 +1982,18 @@ paths:
title: Session Id
description: ID of the session
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/SessionSummaries'
- '422':
+ $ref: "#/components/schemas/SessionSummaries"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -1965,24 +2050,24 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/MessageSearchOptions'
+ $ref: "#/components/schemas/MessageSearchOptions"
description: Message search parameters
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
type: array
items:
- $ref: '#/components/schemas/Message'
+ $ref: "#/components/schemas/Message"
title: Response Search Session V2 Workspaces Workspace Id Sessions Session Id Search Post
- '422':
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: >-
@@ -2040,25 +2125,25 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/MessageBatchCreate'
+ $ref: "#/components/schemas/MessageBatchCreate"
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
type: array
items:
- $ref: '#/components/schemas/Message'
+ $ref: "#/components/schemas/Message"
title: >-
Response Create Messages For Session V2 Workspaces Workspace Id Sessions Session Id
Messages Post
- '422':
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -2120,23 +2205,23 @@ paths:
$ref: >-
#/components/schemas/Body_create_messages_with_file_v2_workspaces__workspace_id__sessions__session_id__messages_upload_post
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
type: array
items:
- $ref: '#/components/schemas/Message'
+ $ref: "#/components/schemas/Message"
title: >-
Response Create Messages With File V2 Workspaces Workspace Id Sessions Session Id
Messages Upload Post
- '422':
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -2199,7 +2284,7 @@ paths:
schema:
anyOf:
- type: boolean
- - type: 'null'
+ - type: "null"
description: Whether to reverse the order of results
default: false
title: Reverse
@@ -2230,23 +2315,23 @@ paths:
application/json:
schema:
anyOf:
- - $ref: '#/components/schemas/MessageGet'
- - type: 'null'
+ - $ref: "#/components/schemas/MessageGet"
+ - type: "null"
description: Filtering options for the messages list
title: Options
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Page_Message_'
- '422':
+ $ref: "#/components/schemas/Page_Message_"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: >-
@@ -2313,18 +2398,18 @@ paths:
title: Message Id
description: ID of the message to retrieve
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Message'
- '422':
+ $ref: "#/components/schemas/Message"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -2393,21 +2478,21 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/MessageUpdate'
+ $ref: "#/components/schemas/MessageUpdate"
description: Updated message parameters
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Message'
- '422':
+ $ref: "#/components/schemas/Message"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: >-
@@ -2454,7 +2539,7 @@ paths:
schema:
anyOf:
- type: string
- - type: 'null'
+ - type: "null"
description: ID of the workspace to scope the key to
title: Workspace Id
description: ID of the workspace to scope the key to
@@ -2464,7 +2549,7 @@ paths:
schema:
anyOf:
- type: string
- - type: 'null'
+ - type: "null"
description: ID of the peer to scope the key to
title: Peer Id
description: ID of the peer to scope the key to
@@ -2474,7 +2559,7 @@ paths:
schema:
anyOf:
- type: string
- - type: 'null'
+ - type: "null"
description: ID of the session to scope the key to
title: Session Id
description: ID of the session to scope the key to
@@ -2485,20 +2570,20 @@ paths:
anyOf:
- type: string
format: date-time
- - type: 'null'
+ - type: "null"
title: Expires At
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema: {}
- '422':
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -2544,21 +2629,21 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/WebhookEndpointCreate'
+ $ref: "#/components/schemas/WebhookEndpointCreate"
description: Webhook endpoint parameters
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/WebhookEndpoint'
- '422':
+ $ref: "#/components/schemas/WebhookEndpoint"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: >-
@@ -2627,18 +2712,18 @@ paths:
title: Size
description: Page size
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Page_WebhookEndpoint_'
- '422':
+ $ref: "#/components/schemas/Page_WebhookEndpoint_"
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -2692,17 +2777,17 @@ paths:
title: Endpoint Id
description: Webhook endpoint ID
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema: {}
- '422':
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -2747,17 +2832,17 @@ paths:
title: Workspace Id
description: Workspace ID
responses:
- '200':
+ "200":
description: Successful Response
content:
application/json:
schema: {}
- '422':
+ "422":
description: Validation Error
content:
application/json:
schema:
- $ref: '#/components/schemas/HTTPValidationError'
+ $ref: "#/components/schemas/HTTPValidationError"
x-codeSamples:
- lang: JavaScript
source: |-
@@ -2818,9 +2903,9 @@ components:
sessions:
anyOf:
- additionalProperties:
- $ref: '#/components/schemas/SessionDeriverStatus'
+ $ref: "#/components/schemas/SessionDeriverStatus"
type: object
- - type: 'null'
+ - type: "null"
title: Sessions
description: Per-session status when not filtered by session
type: object
@@ -2835,13 +2920,13 @@ components:
session_id:
anyOf:
- type: string
- - type: 'null'
+ - type: "null"
title: Session Id
description: ID of the session to scope the representation to
target:
anyOf:
- type: string
- - type: 'null'
+ - type: "null"
title: Target
description: Optional peer to get the representation for, from the perspective of this peer
query:
@@ -2858,20 +2943,11 @@ components:
required:
- query
title: DialecticOptions
- DialecticResponse:
- properties:
- content:
- type: string
- title: Content
- type: object
- required:
- - content
- title: DialecticResponse
HTTPValidationError:
properties:
detail:
items:
- $ref: '#/components/schemas/ValidationError'
+ $ref: "#/components/schemas/ValidationError"
type: array
title: Detail
type: object
@@ -2918,7 +2994,7 @@ components:
properties:
messages:
items:
- $ref: '#/components/schemas/MessageCreate'
+ $ref: "#/components/schemas/MessageCreate"
type: array
maxItems: 100
minItems: 1
@@ -2942,13 +3018,13 @@ components:
anyOf:
- additionalProperties: true
type: object
- - type: 'null'
+ - type: "null"
title: Metadata
created_at:
anyOf:
- type: string
format: date-time
- - type: 'null'
+ - type: "null"
title: Created At
type: object
required:
@@ -2961,7 +3037,7 @@ components:
anyOf:
- additionalProperties: true
type: object
- - type: 'null'
+ - type: "null"
title: Filters
type: object
title: MessageGet
@@ -2975,7 +3051,7 @@ components:
anyOf:
- additionalProperties: true
type: object
- - type: 'null'
+ - type: "null"
title: Filters
description: Filters to scope the search
limit:
@@ -2995,7 +3071,7 @@ components:
anyOf:
- additionalProperties: true
type: object
- - type: 'null'
+ - type: "null"
title: Metadata
type: object
title: MessageUpdate
@@ -3003,7 +3079,7 @@ components:
properties:
items:
items:
- $ref: '#/components/schemas/Message'
+ $ref: "#/components/schemas/Message"
type: array
title: Items
total:
@@ -3032,7 +3108,7 @@ components:
properties:
items:
items:
- $ref: '#/components/schemas/Peer'
+ $ref: "#/components/schemas/Peer"
type: array
title: Items
total:
@@ -3061,7 +3137,7 @@ components:
properties:
items:
items:
- $ref: '#/components/schemas/Session'
+ $ref: "#/components/schemas/Session"
type: array
title: Items
total:
@@ -3090,7 +3166,7 @@ components:
properties:
items:
items:
- $ref: '#/components/schemas/WebhookEndpoint'
+ $ref: "#/components/schemas/WebhookEndpoint"
type: array
title: Items
total:
@@ -3119,7 +3195,7 @@ components:
properties:
items:
items:
- $ref: '#/components/schemas/Workspace'
+ $ref: "#/components/schemas/Workspace"
type: array
title: Items
total:
@@ -3170,6 +3246,18 @@ components:
- workspace_id
- created_at
title: Peer
+ PeerCardResponse:
+ properties:
+ peer_card:
+ anyOf:
+ - items:
+ type: string
+ type: array
+ - type: "null"
+ title: Peer Card
+ description: The peer card content, or None if not found
+ type: object
+ title: PeerCardResponse
PeerCreate:
properties:
id:
@@ -3182,13 +3270,13 @@ components:
anyOf:
- additionalProperties: true
type: object
- - type: 'null'
+ - type: "null"
title: Metadata
configuration:
anyOf:
- additionalProperties: true
type: object
- - type: 'null'
+ - type: "null"
title: Configuration
type: object
required:
@@ -3200,7 +3288,7 @@ components:
anyOf:
- additionalProperties: true
type: object
- - type: 'null'
+ - type: "null"
title: Filters
type: object
title: PeerGet
@@ -3213,7 +3301,7 @@ components:
target:
anyOf:
- type: string
- - type: 'null'
+ - type: "null"
title: Target
description: Optional peer ID to get the representation for, from the perspective of this peer
type: object
@@ -3226,13 +3314,13 @@ components:
anyOf:
- additionalProperties: true
type: object
- - type: 'null'
+ - type: "null"
title: Metadata
configuration:
anyOf:
- additionalProperties: true
type: object
- - type: 'null'
+ - type: "null"
title: Configuration
type: object
title: PeerUpdate
@@ -3273,13 +3361,13 @@ components:
title: Id
messages:
items:
- $ref: '#/components/schemas/Message'
+ $ref: "#/components/schemas/Message"
type: array
title: Messages
summary:
anyOf:
- - $ref: '#/components/schemas/Summary'
- - type: 'null'
+ - $ref: "#/components/schemas/Summary"
+ - type: "null"
description: The summary if available
type: object
required:
@@ -3298,20 +3386,20 @@ components:
anyOf:
- additionalProperties: true
type: object
- - type: 'null'
+ - type: "null"
title: Metadata
peers:
anyOf:
- additionalProperties:
- $ref: '#/components/schemas/SessionPeerConfig'
+ $ref: "#/components/schemas/SessionPeerConfig"
type: object
- - type: 'null'
+ - type: "null"
title: Peers
configuration:
anyOf:
- additionalProperties: true
type: object
- - type: 'null'
+ - type: "null"
title: Configuration
type: object
required:
@@ -3322,7 +3410,7 @@ components:
session_id:
anyOf:
- type: string
- - type: 'null'
+ - type: "null"
title: Session Id
description: Session ID if filtered by session
total_work_units:
@@ -3354,7 +3442,7 @@ components:
anyOf:
- additionalProperties: true
type: object
- - type: 'null'
+ - type: "null"
title: Filters
type: object
title: SessionGet
@@ -3370,7 +3458,7 @@ components:
observe_me:
anyOf:
- type: boolean
- - type: 'null'
+ - type: "null"
title: Observe Me
description: >-
Whether other peers in this session should try to form a session-level theory-of-mind
@@ -3384,13 +3472,13 @@ components:
title: Id
short_summary:
anyOf:
- - $ref: '#/components/schemas/Summary'
- - type: 'null'
+ - $ref: "#/components/schemas/Summary"
+ - type: "null"
description: The short summary if available
long_summary:
anyOf:
- - $ref: '#/components/schemas/Summary'
- - type: 'null'
+ - $ref: "#/components/schemas/Summary"
+ - type: "null"
description: The long summary if available
type: object
required:
@@ -3402,13 +3490,13 @@ components:
anyOf:
- additionalProperties: true
type: object
- - type: 'null'
+ - type: "null"
title: Metadata
configuration:
anyOf:
- additionalProperties: true
type: object
- - type: 'null'
+ - type: "null"
title: Configuration
type: object
title: SessionUpdate
@@ -3471,7 +3559,7 @@ components:
workspace_id:
anyOf:
- type: string
- - type: 'null'
+ - type: "null"
title: Workspace Id
url:
type: string
@@ -3546,7 +3634,7 @@ components:
anyOf:
- additionalProperties: true
type: object
- - type: 'null'
+ - type: "null"
title: Filters
type: object
title: WorkspaceGet
@@ -3556,13 +3644,13 @@ components:
anyOf:
- additionalProperties: true
type: object
- - type: 'null'
+ - type: "null"
title: Metadata
configuration:
anyOf:
- additionalProperties: true
type: object
- - type: 'null'
+ - type: "null"
title: Configuration
type: object
title: WorkspaceUpdate
diff --git a/pyproject.toml b/pyproject.toml
index cfadf4cf..40a13384 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "honcho"
-version = "2.3.2"
+version = "2.3.3"
description = "Honcho Server"
authors = [
{name = "Plastic Labs", email = "hello@plasticlabs.ai"},
@@ -79,7 +79,7 @@ extend-immutable-calls = ["fastapi.Depends"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "session"
-addopts = "--strict-markers --cov=src/ --cov-report=term-missing"
+addopts = "--strict-markers --cov=src/ --cov=sdks/python/src/honcho --cov-report=term-missing"
testpaths = ["tests"]
pythonpath = ["src"]
diff --git a/sdks/python/pyproject.toml b/sdks/python/pyproject.toml
index 73254de3..669159b8 100644
--- a/sdks/python/pyproject.toml
+++ b/sdks/python/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "honcho-ai"
-version = "1.4.0"
+version = "1.4.1"
description = "Official DX Optimized Python SDK for Honcho"
dynamic = ["readme"]
license = "Apache-2.0"
@@ -8,9 +8,10 @@ authors = [
{ name = "Plastic Labs", email = "hello@plasticlabs.ai" },
]
dependencies = [
- "honcho-core>=1.4.0",
+ "honcho-core>=1.4.1-alpha.0",
"httpx>=0.28.0, <1",
"pydantic>=2.0.0, <3",
+ "typing-extensions>=4.12.0; python_version < \"3.12\"",
]
requires-python = ">= 3.8"
classifiers = [
@@ -48,6 +49,16 @@ readme = {file = ["README.md"], content-type = "text/markdown"}
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
+[tool.basedpyright]
+# BasedPyright currently seems like the best type checker option, much faster
+# https://docs.basedpyright.com/latest/configuration/config-files/#sample-pyprojecttoml-file
+include = ["src"]
+reportUnusedCallResult = false
+reportCallInDefaultInitializer = false
+reportAny = false
+reportExplicitAny = false
+reportImportCycles = false
+
[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
diff --git a/sdks/python/src/honcho/__init__.py b/sdks/python/src/honcho/__init__.py
index f3ac73b7..27609e22 100644
--- a/sdks/python/src/honcho/__init__.py
+++ b/sdks/python/src/honcho/__init__.py
@@ -45,6 +45,7 @@ from .pagination import SyncPage
from .peer import Peer
from .session import Session
from .session_context import SessionContext, SessionSummaries, Summary
+from .types import DialecticStreamResponse
__version__ = "1.4.0"
__author__ = "Plastic Labs"
@@ -62,4 +63,5 @@ __all__ = [
"SessionSummaries",
"Summary",
"SyncPage",
+ "DialecticStreamResponse",
]
diff --git a/sdks/python/src/honcho/async_client/client.py b/sdks/python/src/honcho/async_client/client.py
index e1c1e788..bdc01fa8 100644
--- a/sdks/python/src/honcho/async_client/client.py
+++ b/sdks/python/src/honcho/async_client/client.py
@@ -9,6 +9,8 @@ import httpx
from honcho_core import AsyncHoncho as AsyncHonchoCore
from honcho_core import Honcho as HonchoCore
from honcho_core.types import DeriverStatus
+from honcho_core.types.workspaces.peer import Peer as PeerCore
+from honcho_core.types.workspaces.session import Session as SessionCore
from honcho_core.types.workspaces.sessions.message import Message
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
@@ -201,7 +203,7 @@ class AsyncHoncho(BaseModel):
async def get_peers(
self, filters: dict[str, object] | None = None
- ) -> AsyncPage[AsyncPeer]:
+ ) -> AsyncPage[PeerCore, AsyncPeer]:
"""
Get all peers in the current workspace.
@@ -265,7 +267,7 @@ class AsyncHoncho(BaseModel):
async def get_sessions(
self, filters: dict[str, object] | None = None
- ) -> AsyncPage[AsyncSession]:
+ ) -> AsyncPage[SessionCore, AsyncSession]:
"""
Get all sessions in the current workspace.
@@ -466,6 +468,53 @@ class AsyncHoncho(BaseModel):
await asyncio.sleep(sleep_time)
+ @validate_call(config=ConfigDict(arbitrary_types_allowed=True))
+ async def update_message(
+ self,
+ message: Message | str = Field(
+ ..., description="The Message object or message ID to update"
+ ),
+ metadata: dict[str, object] = Field(
+ ..., description="The metadata to update for the message"
+ ),
+ session_id: str | None = Field(
+ None,
+ min_length=1,
+ description="The ID of the session (required if message is a string ID)",
+ ),
+ ) -> Message:
+ """
+ Update the metadata of a message.
+
+ Makes an API call to update the metadata of a specific message within a session.
+
+ Args:
+ message: Either a Message object or a message ID string
+ metadata: The metadata to update for the message
+ session_id: The ID of the session (required if message is a string ID, ignored if message is a Message object)
+
+ Returns:
+ The updated Message object
+
+ Raises:
+ ValidationError: If message is a string ID but session_id is not provided
+ """
+ if isinstance(message, Message):
+ message_id = message.id
+ resolved_session_id = message.session_id
+ else:
+ message_id = message
+ if not session_id:
+ raise ValueError("session_id is required when message is a string ID")
+ resolved_session_id = session_id
+
+ return await self._client.workspaces.sessions.messages.update(
+ message_id=message_id,
+ workspace_id=self.workspace_id,
+ session_id=resolved_session_id,
+ metadata=metadata,
+ )
+
def __repr__(self) -> str:
"""
Return a string representation of the AsyncHoncho client.
diff --git a/sdks/python/src/honcho/async_client/pagination.py b/sdks/python/src/honcho/async_client/pagination.py
index a8232095..4fd3ae6e 100644
--- a/sdks/python/src/honcho/async_client/pagination.py
+++ b/sdks/python/src/honcho/async_client/pagination.py
@@ -1,14 +1,13 @@
from collections.abc import AsyncIterator, Callable
-from typing import TypeVar
from honcho_core.pagination import AsyncPage as AsyncPageCore
-from pydantic import Field, validate_call
+from typing_extensions import Generic, TypeVar
T = TypeVar("T")
-U = TypeVar("U")
+U = TypeVar("U", default=T)
-class AsyncPage(AsyncPageCore[U]):
+class AsyncPage(Generic[T, U]):
"""
Async paginated result wrapper that transforms objects from type T to type U.
@@ -16,16 +15,13 @@ class AsyncPage(AsyncPageCore[U]):
pagination functionality from the underlying core AsyncPage.
"""
- @validate_call
+ _original_page: AsyncPageCore[T]
+ _transform_func: Callable[[T], U] | None
+
def __init__(
self,
- original_page: AsyncPageCore[T] = Field(
- ..., description="The original AsyncPage to wrap"
- ),
- transform_func: Callable[[T], U] | None = Field(
- None,
- description="Optional function to transform objects from type T to type U",
- ),
+ original_page: AsyncPageCore[T],
+ transform_func: Callable[[T], U] | None = None,
) -> None:
"""
Initialize the transformed async page.
@@ -35,57 +31,73 @@ class AsyncPage(AsyncPageCore[U]):
transform_func: Optional function to transform objects from type T to type U.
If None, objects are passed through unchanged.
"""
- super().__init__(items=original_page.items) # pyright: ignore
- self._original_page = original_page # pyright: ignore
- self._transform_func = transform_func # pyright: ignore
+ self._original_page = original_page
+ self._transform_func = transform_func
- @property
- def items(self) -> list[U]: # pyright: ignore
- """Get all optionally transformed items as a list."""
- if self._transform_func is not None:
- return [self._transform_func(item) for item in self._original_page.items]
- return self._original_page.items # pyright: ignore
-
- async def __aiter__(self) -> AsyncIterator[U]:
- """Async iterate over optionally transformed objects."""
+ async def __aiter__(self) -> AsyncIterator[U] | AsyncIterator[T]:
+ """Async iterate over all transformed items across all pages."""
async for item in self._original_page:
if self._transform_func is not None:
yield self._transform_func(item)
else:
- yield item # type: ignore # pyright: ignore
+ yield item
- async def __agetitem__(self, index: int) -> U:
- """Get an optionally transformed object by index."""
- item = await self._original_page.__agetitem__(index) # type: ignore # pyright: ignore
+ def __getitem__(self, index: int) -> U | T:
+ """Get a transformed item by index on the current page."""
+ items = self._original_page.items or []
+ item = items[index]
if self._transform_func is not None:
- return self._transform_func(item) # pyright: ignore
- return item # type: ignore # pyright: ignore
+ return self._transform_func(item)
+ return item
def __len__(self) -> int:
- """Get the length of the page."""
- return len(self._original_page) # type: ignore # pyright: ignore
+ """Get the number of items on the current page."""
+ items = self._original_page.items or []
+ return len(items)
@property
- async def data(self) -> list[U]:
- """Get all optionally transformed data as a list."""
- data = await self._original_page.data # type: ignore # pyright: ignore
+ def items(self) -> list[U] | list[T]:
+ """Get all transformed items on the current page."""
+ items = self._original_page.items or []
if self._transform_func is not None:
- return [self._transform_func(item) for item in data] # pyright: ignore
- return data # type: ignore # pyright: ignore
+ return [self._transform_func(item) for item in items]
+ return items
@property
- def object(self) -> str:
- """Get the object type."""
- return self._original_page.object # type: ignore # pyright: ignore
+ def total(self) -> int | None:
+ """Get the total number of items across all pages."""
+ return self._original_page.total
@property
- def has_next_page(self) -> bool: # pyright: ignore
+ def page(self) -> int | None:
+ """Get the current page number."""
+ return self._original_page.page
+
+ @property
+ def size(self) -> int | None:
+ """Get the page size."""
+ return self._original_page.size
+
+ @property
+ def pages(self) -> int | None:
+ """Get the total number of pages."""
+ return self._original_page.pages
+
+ def has_next_page(self) -> bool:
"""Check if there's a next page."""
- return self._original_page.has_next_page # type: ignore # pyright: ignore
+ return self._original_page.has_next_page()
- async def next_page(self) -> "AsyncPage[U] | None":
- """Get the next page with optional transformation applied."""
- next_page = await self._original_page.next_page() # type: ignore # pyright: ignore
- if next_page is None:
+ async def get_next_page(self) -> "AsyncPage[T, U] | None":
+ """
+ Fetch the next page of results.
+
+ Returns None if there are no more pages.
+ """
+ if not hasattr(self._original_page, "get_next_page"):
return None
- return AsyncPage(next_page, self._transform_func) # pyright: ignore
+
+ next_original_page = await self._original_page.get_next_page()
+ if not next_original_page:
+ return None
+
+ return AsyncPage(next_original_page, self._transform_func)
diff --git a/sdks/python/src/honcho/async_client/peer.py b/sdks/python/src/honcho/async_client/peer.py
index b21968b3..52c4b424 100644
--- a/sdks/python/src/honcho/async_client/peer.py
+++ b/sdks/python/src/honcho/async_client/peer.py
@@ -2,13 +2,17 @@ from __future__ import annotations
import datetime
from typing import TYPE_CHECKING
+from collections.abc import AsyncGenerator
from honcho_core import AsyncHoncho as AsyncHonchoCore
-from honcho_core._types import NOT_GIVEN
+from honcho_core._types import omit
+from honcho_core.types.workspaces import PeerCardResponse
+from honcho_core.types.workspaces.session import Session as SessionCore
from honcho_core.types.workspaces.sessions import MessageCreateParam
from honcho_core.types.workspaces.sessions.message import Message
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
+from ..types import DialecticStreamResponse
from .pagination import AsyncPage
if TYPE_CHECKING:
@@ -90,12 +94,12 @@ class AsyncPeer(BaseModel):
"""
peer = cls(peer_id, workspace_id, client)
- if config or metadata:
+ if config is not None or metadata is not None:
await client.workspaces.peers.get_or_create(
workspace_id=workspace_id,
id=peer_id,
- configuration=config if config is not None else NOT_GIVEN,
- metadata=metadata if metadata is not None else NOT_GIVEN,
+ configuration=config if config is not None else omit,
+ metadata=metadata if metadata is not None else omit,
)
return peer
@@ -107,7 +111,7 @@ class AsyncPeer(BaseModel):
stream: bool = False,
target: str | AsyncPeer | None = None,
session_id: str | None = None,
- ) -> str | None:
+ ) -> str | DialecticStreamResponse | None:
"""
Query the peer's representation with a natural language question.
@@ -125,9 +129,40 @@ class AsyncPeer(BaseModel):
If provided, only information from that session is considered
Returns:
- Response string containing the answer to the query, or None if no
- relevant information is available
+ For non-streaming: Response string containing the answer, or None if no relevant information
+ For streaming: DialecticStreamResponse object that can be iterated over and provides final response
"""
+ if stream:
+
+ async def stream_response() -> AsyncGenerator[str]:
+ import json
+
+ # Use core SDK with_streaming_response
+ async with self._client.workspaces.peers.with_streaming_response.chat(
+ peer_id=self.id,
+ workspace_id=self.workspace_id,
+ query=query,
+ stream=True,
+ target=str(target.id) if isinstance(target, AsyncPeer) else target,
+ session_id=session_id,
+ ) as response:
+ response.http_response.raise_for_status()
+ async for line in response.iter_lines():
+ if line.startswith("data: "):
+ json_str = line[6:] # Remove "data: " prefix
+ try:
+ chunk_data = json.loads(json_str)
+ if chunk_data.get("done"):
+ break
+ delta_obj = chunk_data.get("delta", {})
+ content = delta_obj.get("content")
+ if content:
+ yield content
+ except json.JSONDecodeError:
+ continue
+
+ return DialecticStreamResponse(stream_response())
+
response = await self._client.workspaces.peers.chat(
peer_id=self.id,
workspace_id=self.workspace_id,
@@ -143,7 +178,7 @@ class AsyncPeer(BaseModel):
async def get_sessions(
self, filters: dict[str, object] | None = None
- ) -> AsyncPage[AsyncSession]:
+ ) -> AsyncPage[SessionCore, AsyncSession]:
"""
Get all sessions this peer is a member of.
@@ -322,6 +357,40 @@ class AsyncPeer(BaseModel):
limit=limit,
)
+ async def card(
+ self,
+ target: str | AsyncPeer | None = None,
+ ) -> str:
+ """
+ Get the peer card for this peer.
+
+ Makes an API call to retrieve the peer card, which contains a representation
+ of what this peer knows. If a target is provided, returns this peer's local
+ representation of the target peer.
+
+ Args:
+ target: Optional target peer for local card. If provided, returns this
+ peer's card of the target peer. Can be an AsyncPeer object or peer ID string.
+
+ Returns:
+ A string containing the peer card joined with newlines, or an empty string if none is available
+ """
+ # Validate target parameter
+ if isinstance(target, str) and len(target.strip()) == 0:
+ raise ValueError("target string cannot be empty")
+
+ response: PeerCardResponse = await self._client.workspaces.peers.card(
+ peer_id=self.id,
+ workspace_id=self.workspace_id,
+ target=str(target.id) if isinstance(target, AsyncPeer) else target,
+ )
+
+ if response.peer_card is None:
+ return ""
+
+ items: list[str] = response.peer_card
+ return "\n".join(items)
+
def __repr__(self) -> str:
"""
Return a string representation of the AsyncPeer.
diff --git a/sdks/python/src/honcho/async_client/session.py b/sdks/python/src/honcho/async_client/session.py
index 64e03ffe..fb2bea00 100644
--- a/sdks/python/src/honcho/async_client/session.py
+++ b/sdks/python/src/honcho/async_client/session.py
@@ -1,9 +1,13 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any
+import asyncio
+import time
+import logging
from honcho_core import AsyncHoncho as AsyncHonchoCore
-from honcho_core._types import NOT_GIVEN
+from honcho_core._types import omit
+from honcho_core.types import DeriverStatus
from honcho_core.types.workspaces.sessions import MessageCreateParam
from honcho_core.types.workspaces.sessions.message import Message
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
@@ -15,6 +19,8 @@ from .pagination import AsyncPage
if TYPE_CHECKING:
from .peer import AsyncPeer
+logger = logging.getLogger(__name__)
+
class SessionPeerConfig(BaseModel):
observe_others: bool | None = Field(
@@ -105,12 +111,12 @@ class AsyncSession(BaseModel):
"""
session = cls(session_id, workspace_id, client)
- if config or metadata:
+ if config is not None or metadata is not None:
await client.workspaces.sessions.get_or_create(
workspace_id=workspace_id,
id=session_id,
- configuration=config if config is not None else NOT_GIVEN,
- metadata=metadata if metadata is not None else NOT_GIVEN,
+ configuration=config if config is not None else omit,
+ metadata=metadata if metadata is not None else omit,
)
return session
@@ -296,10 +302,10 @@ class AsyncSession(BaseModel):
peer_id=str(peer.id) if isinstance(peer, AsyncPeer) else peer,
workspace_id=self.workspace_id,
session_id=self.id,
- observe_others=NOT_GIVEN
+ observe_others=omit
if config.observe_others is None
else config.observe_others,
- observe_me=NOT_GIVEN if config.observe_me is None else config.observe_me,
+ observe_me=omit if config.observe_me is None else config.observe_me,
)
@validate_call
@@ -362,6 +368,17 @@ class AsyncSession(BaseModel):
)
return AsyncPage(messages_page)
+ async def delete(self) -> None:
+ """
+ Delete this session.
+
+ Makes an async API call to delete this session.
+ """
+ await self._client.workspaces.sessions.delete(
+ session_id=self.id,
+ workspace_id=self.workspace_id,
+ )
+
async def get_metadata(self) -> dict[str, object]:
"""
Get metadata for this session.
@@ -436,7 +453,7 @@ class AsyncSession(BaseModel):
context = await self._client.workspaces.sessions.get_context(
session_id=self.id,
workspace_id=self.workspace_id,
- tokens=tokens if tokens is not None else NOT_GIVEN,
+ tokens=tokens if tokens is not None else omit,
summary=summary,
)
@@ -615,6 +632,100 @@ class AsyncSession(BaseModel):
target=str(target.id) if isinstance(target, AsyncPeer) else target,
)
+ @validate_call
+ async def get_deriver_status(
+ self,
+ observer_id: str | None = None,
+ sender_id: str | None = None,
+ ) -> DeriverStatus:
+ """
+ Get the deriver processing status, optionally scoped to an observer, sender, and/or session
+ """
+ return await self._client.workspaces.deriver_status(
+ workspace_id=self.workspace_id,
+ observer_id=observer_id,
+ sender_id=sender_id,
+ session_id=self.id,
+ )
+
+ @validate_call
+ async def poll_deriver_status(
+ self,
+ observer_id: str | None = None,
+ sender_id: str | None = None,
+ timeout: float = Field(
+ 300.0,
+ gt=0,
+ description="Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds).",
+ ),
+ ) -> DeriverStatus:
+ """
+ Poll get_deriver_status until pending_work_units and in_progress_work_units are both 0.
+ This allows you to guarantee that all messages have been processed by the deriver for
+ use with the dialectic endpoint.
+
+ The polling estimates sleep time by assuming each work unit takes 1 second.
+
+ Args:
+ observer_id: Optional observer ID to scope the status check
+ sender_id: Optional sender ID to scope the status check
+ timeout: Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds).
+
+ Returns:
+ DeriverStatus when all work units are complete
+
+ Raises:
+ TimeoutError: If timeout is exceeded before work units complete
+ Exception: If get_deriver_status fails repeatedly
+ """
+ start_time = time.time()
+
+ while True:
+ try:
+ status = await self.get_deriver_status(observer_id, sender_id)
+ except Exception as e:
+ logger.warning(f"Failed to get deriver status: {e}")
+ # Sleep briefly before retrying
+ await asyncio.sleep(1)
+
+ # Check timeout after error
+ elapsed_time = time.time() - start_time
+ if elapsed_time >= timeout:
+ raise TimeoutError(
+ f"Polling timeout exceeded after {timeout}s. "
+ + f"Error during status check: {e}"
+ ) from e
+ continue
+
+ if status.pending_work_units == 0 and status.in_progress_work_units == 0:
+ return status
+
+ # Check timeout before sleeping
+ elapsed_time = time.time() - start_time
+ if elapsed_time >= timeout:
+ raise TimeoutError(
+ f"Polling timeout exceeded after {timeout}s. "
+ + f"Current status: {status.pending_work_units} pending, "
+ + f"{status.in_progress_work_units} in progress work units."
+ )
+
+ # Sleep for the expected time to complete all current work units
+ # Assuming each pending and in-progress work unit takes 1 second
+ total_work_units = status.pending_work_units + status.in_progress_work_units
+ sleep_time = max(1, total_work_units)
+
+ # Don't sleep past the timeout
+ remaining_time = timeout - elapsed_time
+ sleep_time = min(sleep_time, remaining_time)
+ if sleep_time <= 0:
+ raise TimeoutError(
+ f"Polling timeout exceeded after {timeout}s. "
+ + f"Current status: {status.pending_work_units} pending, "
+ + f"{status.in_progress_work_units} in progress work units."
+ )
+
+ await asyncio.sleep(sleep_time)
+
def __repr__(self) -> str:
"""
Return a string representation of the AsyncSession.
diff --git a/sdks/python/src/honcho/client.py b/sdks/python/src/honcho/client.py
index 78d799e9..777c5e4e 100644
--- a/sdks/python/src/honcho/client.py
+++ b/sdks/python/src/honcho/client.py
@@ -7,6 +7,8 @@ from typing import Any, Literal
import httpx
from honcho_core import Honcho as HonchoCore
from honcho_core.types import DeriverStatus
+from honcho_core.types.workspaces.peer import Peer as PeerCore
+from honcho_core.types.workspaces.session import Session as SessionCore
from honcho_core.types.workspaces.sessions.message import Message
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
@@ -185,7 +187,9 @@ class Honcho(BaseModel):
id, self.workspace_id, self._client, config=config, metadata=metadata
)
- def get_peers(self, filters: dict[str, object] | None = None) -> SyncPage[Peer]:
+ def get_peers(
+ self, filters: dict[str, object] | None = None
+ ) -> SyncPage[PeerCore, Peer]:
"""
Get all peers in the current workspace.
@@ -247,7 +251,7 @@ class Honcho(BaseModel):
def get_sessions(
self, filters: dict[str, object] | None = None
- ) -> SyncPage[Session]:
+ ) -> SyncPage[SessionCore, Session]:
"""
Get all sessions in the current workspace.
@@ -438,6 +442,53 @@ class Honcho(BaseModel):
time.sleep(sleep_time)
+ @validate_call(config=ConfigDict(arbitrary_types_allowed=True))
+ def update_message(
+ self,
+ message: Message | str = Field(
+ ..., description="The Message object or message ID to update"
+ ),
+ metadata: dict[str, object] = Field(
+ ..., description="The metadata to update for the message"
+ ),
+ session_id: str | None = Field(
+ None,
+ min_length=1,
+ description="The ID of the session (required if message is a string ID)",
+ ),
+ ) -> Message:
+ """
+ Update the metadata of a message.
+
+ Makes an API call to update the metadata of a specific message within a session.
+
+ Args:
+ message: Either a Message object or a message ID string
+ metadata: The metadata to update for the message
+ session_id: The ID of the session (required if message is a string ID, ignored if message is a Message object)
+
+ Returns:
+ The updated Message object
+
+ Raises:
+ ValidationError: If message is a string ID but session_id is not provided
+ """
+ if isinstance(message, Message):
+ message_id = message.id
+ resolved_session_id = message.session_id
+ else:
+ message_id = message
+ if not session_id:
+ raise ValueError("session_id is required when message is a string ID")
+ resolved_session_id = session_id
+
+ return self._client.workspaces.sessions.messages.update(
+ message_id=message_id,
+ workspace_id=self.workspace_id,
+ session_id=resolved_session_id,
+ metadata=metadata,
+ )
+
def __repr__(self) -> str:
"""
Return a string representation of the Honcho client.
diff --git a/sdks/python/src/honcho/pagination.py b/sdks/python/src/honcho/pagination.py
index b22270db..10c7cd09 100644
--- a/sdks/python/src/honcho/pagination.py
+++ b/sdks/python/src/honcho/pagination.py
@@ -1,14 +1,14 @@
from collections.abc import Callable, Iterator
-from typing import Optional, TypeVar # pyright: ignore
+from typing import Generic
from honcho_core.pagination import SyncPage as SyncPageCore
-from pydantic import Field, validate_call
+from typing_extensions import TypeVar
T = TypeVar("T")
-U = TypeVar("U")
+U = TypeVar("U", default=T)
-class SyncPage(SyncPageCore[U]):
+class SyncPage(Generic[T, U]):
"""
Paginated result wrapper that transforms objects from type T to type U.
@@ -16,16 +16,13 @@ class SyncPage(SyncPageCore[U]):
pagination functionality from the underlying core SyncPage.
"""
- @validate_call
- def __init__( # pyright: ignore
+ _original_page: SyncPageCore[T]
+ _transform_func: Callable[[T], U] | None
+
+ def __init__(
self,
- original_page: SyncPageCore[T] = Field(
- ..., description="The original SyncPage to wrap"
- ),
- transform_func: Callable[[T], U] | None = Field(
- None,
- description="Optional function to transform objects from type T to type U",
- ),
+ original_page: SyncPageCore[T],
+ transform_func: Callable[[T], U] | None = None,
) -> None:
"""
Initialize the transformed page.
@@ -35,48 +32,73 @@ class SyncPage(SyncPageCore[U]):
transform_func: Optional function to transform objects from type T to type U.
If None, objects are passed through unchanged.
"""
- self._original_page = original_page # pyright: ignore
- self._transform_func = transform_func # pyright: ignore
+ self._original_page = original_page
+ self._transform_func = transform_func
- def __iter__(self) -> Iterator[U]:
- """Iterate over optionally transformed objects."""
+ def __iter__(self) -> Iterator[U] | Iterator[T]:
+ """Iterate over all transformed items across all pages."""
for item in self._original_page:
if self._transform_func is not None:
yield self._transform_func(item)
else:
- yield item # pyright: ignore
+ yield item
- def __getitem__(self, index: int) -> U:
- """Get an optionally transformed object by index."""
- item = self._original_page[index] # pyright: ignore
+ def __getitem__(self, index: int) -> U | T:
+ """Get a transformed item by index on the current page."""
+ items = self._original_page.items or []
+ item = items[index]
if self._transform_func is not None:
- return self._transform_func(item) # pyright: ignore
- return item # pyright: ignore
+ return self._transform_func(item)
+ return item
def __len__(self) -> int:
- """Get the length of the page."""
- return len(self._original_page) # pyright: ignore
+ """Get the number of items on the current page."""
+ items = self._original_page.items or []
+ return len(items)
@property
- def data(self) -> list[U]:
- """Get all optionally transformed data as a list."""
+ def items(self) -> list[U] | list[T]:
+ """Get all transformed items on the current page."""
+ items = self._original_page.items or []
if self._transform_func is not None:
- return [self._transform_func(item) for item in self._original_page.data] # pyright: ignore
- return self._original_page.data # pyright: ignore
+ return [self._transform_func(item) for item in items]
+ return items
@property
- def object(self) -> str:
- """Get the object type."""
- return self._original_page.object # pyright: ignore
+ def total(self) -> int | None:
+ """Get the total number of items across all pages."""
+ return self._original_page.total
@property
- def has_next_page(self) -> bool: # pyright: ignore
+ def page(self) -> int | None:
+ """Get the current page number."""
+ return self._original_page.page
+
+ @property
+ def size(self) -> int | None:
+ """Get the page size."""
+ return self._original_page.size
+
+ @property
+ def pages(self) -> int | None:
+ """Get the total number of pages."""
+ return self._original_page.pages
+
+ def has_next_page(self) -> bool:
"""Check if there's a next page."""
- return self._original_page.has_next_page # pyright: ignore
+ return self._original_page.has_next_page()
- def next_page(self) -> Optional["SyncPage[U]"]: # pyright: ignore
- """Get the next page with optional transformation applied."""
- next_page = self._original_page.next_page() # pyright: ignore
- if next_page is None:
+ def get_next_page(self) -> "SyncPage[T, U] | None":
+ """
+ Fetch the next page of results.
+
+ Returns None if there are no more pages.
+ """
+ if not hasattr(self._original_page, "get_next_page"):
return None
- return SyncPage(next_page, self._transform_func) # pyright: ignore
+
+ next_original_page = self._original_page.get_next_page()
+ if not next_original_page:
+ return None
+
+ return SyncPage(next_original_page, self._transform_func)
diff --git a/sdks/python/src/honcho/peer.py b/sdks/python/src/honcho/peer.py
index 920163bf..0272c2b4 100644
--- a/sdks/python/src/honcho/peer.py
+++ b/sdks/python/src/honcho/peer.py
@@ -2,13 +2,17 @@ from __future__ import annotations
import datetime
from typing import TYPE_CHECKING
+from collections.abc import Generator
from honcho_core import Honcho as HonchoCore
-from honcho_core._types import NOT_GIVEN
+from honcho_core._types import omit
+from honcho_core.types.workspaces import PeerCardResponse
+from honcho_core.types.workspaces.session import Session as SessionCore
from honcho_core.types.workspaces.sessions import MessageCreateParam
from honcho_core.types.workspaces.sessions.message import Message
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
+from .types import DialecticStreamResponse
from .pagination import SyncPage
if TYPE_CHECKING:
@@ -76,12 +80,12 @@ class Peer(BaseModel):
super().__init__(id=peer_id, workspace_id=workspace_id)
self._client = client
- if config or metadata:
+ if config is not None or metadata is not None:
self._client.workspaces.peers.get_or_create(
workspace_id=workspace_id,
id=peer_id,
- configuration=config if config is not None else NOT_GIVEN,
- metadata=metadata if metadata is not None else NOT_GIVEN,
+ configuration=config if config is not None else omit,
+ metadata=metadata if metadata is not None else omit,
)
def chat(
@@ -91,7 +95,7 @@ class Peer(BaseModel):
stream: bool = False,
target: str | Peer | None = None,
session_id: str | None = None,
- ) -> str | None:
+ ) -> str | DialecticStreamResponse | None:
"""
Query the peer's representation with a natural language question.
@@ -109,9 +113,40 @@ class Peer(BaseModel):
If provided, only information from that session is considered
Returns:
- Response string containing the answer to the query, or None if no
- relevant information is available
+ For non-streaming: Response string containing the answer, or None if no relevant information
+ For streaming: DialecticStreamResponse object that can be iterated over and provides final response
"""
+ if stream:
+
+ def stream_response() -> Generator[str, None, None]:
+ import json
+
+ # Use core SDK with_streaming_response
+ with self._client.workspaces.peers.with_streaming_response.chat(
+ peer_id=self.id,
+ workspace_id=self.workspace_id,
+ query=query,
+ stream=True,
+ target=str(target.id) if isinstance(target, Peer) else target,
+ session_id=session_id,
+ ) as response:
+ response.http_response.raise_for_status()
+ for line in response.iter_lines():
+ if line.startswith("data: "):
+ json_str = line[6:] # Remove "data: " prefix
+ try:
+ chunk_data = json.loads(json_str)
+ if chunk_data.get("done"):
+ break
+ delta_obj = chunk_data.get("delta", {})
+ content = delta_obj.get("content")
+ if content:
+ yield content
+ except json.JSONDecodeError:
+ continue
+
+ return DialecticStreamResponse(stream_response())
+
response = self._client.workspaces.peers.chat(
peer_id=self.id,
workspace_id=self.workspace_id,
@@ -126,7 +161,7 @@ class Peer(BaseModel):
def get_sessions(
self, filters: dict[str, object] | None = None
- ) -> SyncPage[Session]:
+ ) -> SyncPage[SessionCore, Session]:
"""
Get all sessions this peer is a member of.
@@ -305,6 +340,40 @@ class Peer(BaseModel):
limit=limit,
)
+ def card(
+ self,
+ target: str | Peer | None = None,
+ ) -> str:
+ """
+ Get the peer card for this peer.
+
+ Makes an API call to retrieve the peer card, which contains a representation
+ of what this peer knows. If a target is provided, returns this peer's local
+ representation of the target peer.
+
+ Args:
+ target: Optional target peer for local card. If provided, returns this
+ peer's card of the target peer. Can be a Peer object or peer ID string.
+
+ Returns:
+ A string containing the peer card joined with newlines, or an empty string if none is available
+ """
+ # Validate target parameter
+ if isinstance(target, str) and len(target.strip()) == 0:
+ raise ValueError("target string cannot be empty")
+
+ response: PeerCardResponse = self._client.workspaces.peers.card(
+ peer_id=self.id,
+ workspace_id=self.workspace_id,
+ target=str(target.id) if isinstance(target, Peer) else target,
+ )
+ if response.peer_card is None:
+ return ""
+
+ items: list[str] = response.peer_card
+
+ return "\n".join(items)
+
def __repr__(self) -> str:
"""
Return a string representation of the Peer.
diff --git a/sdks/python/src/honcho/session.py b/sdks/python/src/honcho/session.py
index b3962ea3..193887b5 100644
--- a/sdks/python/src/honcho/session.py
+++ b/sdks/python/src/honcho/session.py
@@ -1,9 +1,12 @@
from __future__ import annotations
+import logging
+import time
from typing import TYPE_CHECKING, Any
from honcho_core import Honcho as HonchoCore
-from honcho_core._types import NOT_GIVEN
+from honcho_core._types import omit
+from honcho_core.types import DeriverStatus
from honcho_core.types.workspaces.sessions import MessageCreateParam
from honcho_core.types.workspaces.sessions.message import Message
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
@@ -15,6 +18,8 @@ from .utils import prepare_file_for_upload
if TYPE_CHECKING:
from .peer import Peer
+logger = logging.getLogger(__name__)
+
class SessionPeerConfig(BaseModel):
observe_others: bool | None = Field(
@@ -91,12 +96,12 @@ class Session(BaseModel):
)
self._client = client
- if config or metadata:
+ if config is not None or metadata is not None:
self._client.workspaces.sessions.get_or_create(
workspace_id=workspace_id,
id=session_id,
- configuration=config if config is not None else NOT_GIVEN,
- metadata=metadata if metadata is not None else NOT_GIVEN,
+ configuration=config if config is not None else omit,
+ metadata=metadata if metadata is not None else omit,
)
def add_peers(
@@ -275,10 +280,10 @@ class Session(BaseModel):
peer_id=str(peer.id) if isinstance(peer, Peer) else peer,
workspace_id=self.workspace_id,
session_id=self.id,
- observe_others=NOT_GIVEN
+ observe_others=omit
if config.observe_others is None
else config.observe_others,
- observe_me=NOT_GIVEN if config.observe_me is None else config.observe_me,
+ observe_me=omit if config.observe_me is None else config.observe_me,
)
@validate_call
@@ -360,6 +365,17 @@ class Session(BaseModel):
or {}
)
+ def delete(self) -> None:
+ """
+ Delete this session
+
+ Makes an API call to mark this session as inactive.
+ """
+ self._client.workspaces.sessions.delete(
+ session_id=self.id,
+ workspace_id=self.workspace_id,
+ )
+
@validate_call
def set_metadata(
self,
@@ -417,7 +433,7 @@ class Session(BaseModel):
context = self._client.workspaces.sessions.get_context(
session_id=self.id,
workspace_id=self.workspace_id,
- tokens=tokens if tokens is not None else NOT_GIVEN,
+ tokens=tokens if tokens is not None else omit,
summary=summary,
)
@@ -596,6 +612,100 @@ class Session(BaseModel):
target=str(target.id) if isinstance(target, Peer) else target,
)
+ @validate_call
+ def get_deriver_status(
+ self,
+ observer_id: str | None = None,
+ sender_id: str | None = None,
+ ) -> DeriverStatus:
+ """
+ Get the deriver processing status, optionally scoped to an observer, sender, and/or session
+ """
+ return self._client.workspaces.deriver_status(
+ workspace_id=self.workspace_id,
+ observer_id=observer_id,
+ sender_id=sender_id,
+ session_id=self.id,
+ )
+
+ @validate_call
+ def poll_deriver_status(
+ self,
+ observer_id: str | None = None,
+ sender_id: str | None = None,
+ timeout: float = Field(
+ 300.0,
+ gt=0,
+ description="Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds).",
+ ),
+ ) -> DeriverStatus:
+ """
+ Poll get_deriver_status until pending_work_units and in_progress_work_units are both 0.
+ This allows you to guarantee that all messages have been processed by the deriver for
+ use with the dialectic endpoint.
+
+ The polling estimates sleep time by assuming each work unit takes 1 second.
+
+ Args:
+ observer_id: Optional observer ID to scope the status check
+ sender_id: Optional sender ID to scope the status check
+ timeout: Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds).
+
+ Returns:
+ DeriverStatus when all work units are complete
+
+ Raises:
+ TimeoutError: If timeout is exceeded before work units complete
+ Exception: If get_deriver_status fails repeatedly
+ """
+ start_time = time.time()
+
+ while True:
+ try:
+ status = self.get_deriver_status(observer_id, sender_id)
+ except Exception as e:
+ logger.warning(f"Failed to get deriver status: {e}")
+ # Sleep briefly before retrying
+ time.sleep(1)
+
+ # Check timeout after error
+ elapsed_time = time.time() - start_time
+ if elapsed_time >= timeout:
+ raise TimeoutError(
+ f"Polling timeout exceeded after {timeout}s. "
+ + f"Error during status check: {e}"
+ ) from e
+ continue
+
+ if status.pending_work_units == 0 and status.in_progress_work_units == 0:
+ return status
+
+ # Check timeout before sleeping
+ elapsed_time = time.time() - start_time
+ if elapsed_time >= timeout:
+ raise TimeoutError(
+ f"Polling timeout exceeded after {timeout}s. "
+ + f"Current status: {status.pending_work_units} pending, "
+ + f"{status.in_progress_work_units} in progress work units."
+ )
+
+ # Sleep for the expected time to complete all current work units
+ # Assuming each pending and in-progress work unit takes 1 second
+ total_work_units = status.pending_work_units + status.in_progress_work_units
+ sleep_time = max(1, total_work_units)
+
+ # Don't sleep past the timeout
+ remaining_time = timeout - elapsed_time
+ sleep_time = min(sleep_time, remaining_time)
+ if sleep_time <= 0:
+ raise TimeoutError(
+ f"Polling timeout exceeded after {timeout}s. "
+ + f"Current status: {status.pending_work_units} pending, "
+ + f"{status.in_progress_work_units} in progress work units."
+ )
+
+ time.sleep(sleep_time)
+
def __repr__(self) -> str:
"""
Return a string representation of the Session.
diff --git a/sdks/python/src/honcho/types.py b/sdks/python/src/honcho/types.py
new file mode 100644
index 00000000..bf22497d
--- /dev/null
+++ b/sdks/python/src/honcho/types.py
@@ -0,0 +1,106 @@
+"""Shared types for the Honcho SDK."""
+
+from __future__ import annotations
+
+from collections.abc import Iterator, AsyncIterator
+
+
+class DialecticStreamResponse:
+ """
+ Iterator for streaming dialectic responses with utilities for accessing the final response.
+
+ Similar to OpenAI and Anthropic streaming patterns, this allows you to:
+ - Iterate over chunks as they arrive
+ - Access the final accumulated response after streaming completes
+
+ Works with both sync and async iterators.
+
+ Example (sync):
+ ```python
+ stream = peer.chat("Hello", stream=True)
+
+ # Stream chunks
+ for chunk in stream:
+ print(chunk, end="", flush=True)
+
+ # Get final response object
+ final = stream.get_final_response()
+ print(f"\\nFull content: {final['content']}")
+ ```
+
+ Example (async):
+ ```python
+ stream = await peer.chat("Hello", stream=True)
+
+ # Stream chunks
+ async for chunk in stream:
+ print(chunk, end="", flush=True)
+
+ # Get final response object
+ final = stream.get_final_response()
+ print(f"\\nFull content: {final['content']}")
+ ```
+ """
+
+ _iterator: Iterator[str] | AsyncIterator[str]
+ _accumulated_content: list[str]
+ _is_complete: bool
+
+ def __init__(self, iterator: Iterator[str] | AsyncIterator[str]):
+ self._iterator = iterator
+ self._accumulated_content = []
+ self._is_complete = False
+
+ # Sync iterator protocol
+ def __iter__(self):
+ if isinstance(self._iterator, Iterator):
+ return self
+ else:
+ raise TypeError("iterator must be an sync iterator, got async iterator")
+
+ def __next__(self) -> str:
+ try:
+ if not isinstance(self._iterator, Iterator):
+ raise TypeError("iterator must be an sync iterator, got async iterator")
+ chunk = next(self._iterator) # type: ignore
+ self._accumulated_content.append(chunk)
+ return chunk
+ except StopIteration:
+ self._is_complete = True
+ raise
+
+ # Async iterator protocol
+ def __aiter__(self):
+ if isinstance(self._iterator, AsyncIterator):
+ return self
+ else:
+ raise TypeError("iterator must be an async iterator, got sync iterator")
+
+ async def __anext__(self) -> str:
+ try:
+ if not isinstance(self._iterator, AsyncIterator):
+ raise TypeError("iterator must be an async iterator, got sync iterator")
+ chunk = await self._iterator.__anext__() # type: ignore
+ self._accumulated_content.append(chunk)
+ return chunk
+ except StopAsyncIteration:
+ self._is_complete = True
+ raise
+
+ def get_final_response(self) -> dict[str, str]:
+ """
+ Get the final accumulated response after streaming completes.
+
+ Returns:
+ A dictionary with the full content: {"content": "full accumulated text"}
+
+ Note:
+ This should be called after the stream has been fully consumed.
+ If called before completion, it returns the content accumulated so far.
+ """
+ return {"content": "".join(self._accumulated_content)}
+
+ @property
+ def is_complete(self) -> bool:
+ """Check if the stream has finished."""
+ return self._is_complete
diff --git a/sdks/typescript/__tests__/client.test.ts b/sdks/typescript/__tests__/client.test.ts
index 1a571680..e4c48267 100644
--- a/sdks/typescript/__tests__/client.test.ts
+++ b/sdks/typescript/__tests__/client.test.ts
@@ -2,6 +2,7 @@ import { Honcho } from '../src/client';
import { Peer } from '../src/peer';
import { Session } from '../src/session';
import { Page } from '../src/pagination';
+import type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages';
// Mock the @honcho-ai/core module
jest.mock('@honcho-ai/core', () => {
@@ -481,4 +482,93 @@ describe('Honcho Client', () => {
await expect(honcho.pollDeriverStatus({ timeoutMs: 100 })).rejects.toThrow();
});
});
+
+ describe('updateMessage', () => {
+ beforeEach(() => {
+ mockClient.workspaces.sessions = {
+ messages: {
+ update: jest.fn(),
+ },
+ };
+ });
+
+ it('should update message metadata using Message object', async () => {
+ const mockMessage: Message = {
+ id: 'msg-123',
+ session_id: 'session-456',
+ content: 'Test message',
+ peer_id: 'peer-789',
+ created_at: '2024-01-01T00:00:00Z',
+ token_count: 10,
+ workspace_id: 'test-workspace',
+ };
+ const metadata = { updated: true, importance: 'high' };
+ const mockUpdatedMessage = { ...mockMessage, metadata };
+
+ mockClient.workspaces.sessions.messages.update.mockResolvedValue(mockUpdatedMessage);
+
+ const result = await honcho.updateMessage(mockMessage, metadata);
+
+ expect(result).toEqual(mockUpdatedMessage);
+ expect(mockClient.workspaces.sessions.messages.update).toHaveBeenCalledWith(
+ 'test-workspace',
+ 'session-456',
+ 'msg-123',
+ { metadata }
+ );
+ });
+
+ it('should update message metadata using message ID and session ID', async () => {
+ const messageId = 'msg-123';
+ const sessionId = 'session-456';
+ const metadata = { updated: true, importance: 'high' };
+ const mockUpdatedMessage = {
+ id: messageId,
+ session_id: sessionId,
+ content: 'Test message',
+ peer_id: 'peer-789',
+ metadata,
+ };
+
+ mockClient.workspaces.sessions.messages.update.mockResolvedValue(mockUpdatedMessage);
+
+ const result = await honcho.updateMessage(messageId, metadata, sessionId);
+
+ expect(result).toEqual(mockUpdatedMessage);
+ expect(mockClient.workspaces.sessions.messages.update).toHaveBeenCalledWith(
+ 'test-workspace',
+ sessionId,
+ messageId,
+ { metadata }
+ );
+ });
+
+ it('should throw error when message is string ID but session ID is not provided', async () => {
+ const messageId = 'msg-123';
+ const metadata = { updated: true };
+
+ await expect(honcho.updateMessage(messageId, metadata)).rejects.toThrow(
+ 'sessionId is required when message is a string ID'
+ );
+ });
+
+ it('should handle API errors', async () => {
+ const mockMessage: Message = {
+ id: 'msg-123',
+ session_id: 'session-456',
+ content: 'Test message',
+ peer_id: 'peer-789',
+ created_at: '2024-01-01T00:00:00Z',
+ token_count: 10,
+ workspace_id: 'test-workspace',
+ };
+ const metadata = { updated: true };
+
+ mockClient.workspaces.sessions.messages.update.mockRejectedValue(
+ new Error('Update failed')
+ );
+
+ await expect(honcho.updateMessage(mockMessage, metadata)).rejects.toThrow('Update failed');
+ });
+ });
});
diff --git a/sdks/typescript/__tests__/integration.test.ts b/sdks/typescript/__tests__/integration.test.ts
index cc876a8b..e5417d15 100644
--- a/sdks/typescript/__tests__/integration.test.ts
+++ b/sdks/typescript/__tests__/integration.test.ts
@@ -165,7 +165,7 @@ describe('Honcho SDK Integration Tests', () => {
'assistant',
{
query: 'How are you?',
- stream: undefined,
+ stream: false,
target: undefined,
session_id: undefined,
}
@@ -183,6 +183,11 @@ describe('Honcho SDK Integration Tests', () => {
total: 2,
size: 2,
hasNextPage: false,
+ [Symbol.asyncIterator]: async function* () {
+ for (const item of this.items) {
+ yield item
+ }
+ },
}
mockWorkspacesApi.workspaces.getOrCreate.mockResolvedValue({
@@ -302,8 +307,15 @@ describe('Honcho SDK Integration Tests', () => {
],
total: 4,
size: 2,
+ page: 1,
+ pages: 2,
hasNextPage: true,
- nextPage: jest.fn(),
+ getNextPage: jest.fn(),
+ [Symbol.asyncIterator]: async function* () {
+ for (const item of this.items) {
+ yield item
+ }
+ },
}
const secondPageData = {
@@ -313,11 +325,18 @@ describe('Honcho SDK Integration Tests', () => {
],
total: 4,
size: 2,
+ page: 2,
+ pages: 2,
hasNextPage: false,
- nextPage: jest.fn().mockResolvedValue(null),
+ getNextPage: jest.fn().mockResolvedValue(null),
+ [Symbol.asyncIterator]: async function* () {
+ for (const item of this.items) {
+ yield item
+ }
+ },
}
- firstPageData.nextPage.mockResolvedValue(secondPageData)
+ firstPageData.getNextPage.mockResolvedValue(secondPageData)
mockWorkspacesApi.workspaces.peers.list.mockResolvedValue(firstPageData)
// Step 1: Get first page
@@ -327,20 +346,25 @@ describe('Honcho SDK Integration Tests', () => {
expect(firstPage.hasNextPage).toBe(true)
// Step 2: Get data from first page
- const firstPageData_ = await firstPage.data()
+ const firstPageData_ = firstPage.items
expect(firstPageData_).toHaveLength(2)
+ expect(firstPageData_[0]).toBeInstanceOf(Peer)
+ expect(firstPageData_[0].id).toBe('peer1')
// Step 3: Get next page
- const secondPage = await firstPage.nextPage()
+ const secondPage = await firstPage.getNextPage()
expect(secondPage).not.toBeNull()
expect(secondPage!.hasNextPage).toBe(false)
+ expect(secondPage!.page).toBe(2)
// Step 4: Get data from second page
- const secondPageData_ = await secondPage!.data()
+ const secondPageData_ = secondPage!.items
expect(secondPageData_).toHaveLength(2)
+ expect(secondPageData_[0]).toBeInstanceOf(Peer)
+ expect(secondPageData_[0].id).toBe('peer3')
// Step 5: Verify no more pages
- const thirdPage = await secondPage!.nextPage()
+ const thirdPage = await secondPage!.getNextPage()
expect(thirdPage).toBeNull()
})
@@ -405,7 +429,7 @@ describe('Honcho SDK Integration Tests', () => {
// Test empty peers list
const peersPage = await honcho.getPeers()
- const peersList = await peersPage.data()
+ const peersList = peersPage.items
expect(peersList).toEqual([])
// Test empty context
diff --git a/sdks/typescript/__tests__/pagination.test.ts b/sdks/typescript/__tests__/pagination.test.ts
index 6986c4d9..a9385206 100644
--- a/sdks/typescript/__tests__/pagination.test.ts
+++ b/sdks/typescript/__tests__/pagination.test.ts
@@ -1,453 +1,330 @@
-import { Page } from '../src/pagination';
+import { Page } from '../src/pagination'
describe('Page', () => {
- let mockOriginalPage: any;
- let mockItems: any[];
+ let mockOriginalPage: any
+ let mockItems: any[]
beforeEach(() => {
mockItems = [
{ id: 'item1', name: 'Item 1' },
{ id: 'item2', name: 'Item 2' },
{ id: 'item3', name: 'Item 3' },
- ];
+ ]
mockOriginalPage = {
items: mockItems,
- data: mockItems,
size: 3,
total: 10,
+ page: 1,
+ pages: 4,
hasNextPage: true,
- get: jest.fn(),
- nextPage: jest.fn(),
- };
- });
+ [Symbol.asyncIterator]: async function*() {
+ for (const item of mockItems) {
+ yield item
+ }
+ },
+ } as any
+ })
describe('constructor', () => {
it('should initialize with original page', () => {
- const page = new Page(mockOriginalPage);
+ const page = new Page(mockOriginalPage)
- expect(page['_originalPage']).toBe(mockOriginalPage);
- expect(page['_transformFunc']).toBeUndefined();
- });
+ expect(page['_originalPage']).toBe(mockOriginalPage)
+ expect(page['_transformFunc']).toBeUndefined()
+ })
it('should initialize with transform function', () => {
- const transformFunc = (item: any) => ({ ...item, transformed: true });
- const page = new Page(mockOriginalPage, transformFunc);
+ const transformFunc = (item: any) => ({ ...item, transformed: true })
+ const page = new Page(mockOriginalPage, transformFunc)
- expect(page['_originalPage']).toBe(mockOriginalPage);
- expect(page['_transformFunc']).toBe(transformFunc);
- });
- });
+ expect(page['_originalPage']).toBe(mockOriginalPage)
+ expect(page['_transformFunc']).toBe(transformFunc)
+ })
+ })
describe('Symbol.asyncIterator', () => {
it('should iterate through items without transform', async () => {
- const page = new Page(mockOriginalPage);
- const items: any[] = [];
+ const page = new Page(mockOriginalPage)
+ const items: any[] = []
for await (const item of page) {
- items.push(item);
+ items.push(item)
}
- expect(items).toEqual(mockItems);
- });
+ expect(items).toEqual(mockItems)
+ })
it('should iterate through items with transform', async () => {
- const transformFunc = (item: any) => ({ ...item, transformed: true });
- const page = new Page(mockOriginalPage, transformFunc);
- const items: any[] = [];
+ const transformFunc = (item: any) => ({ ...item, transformed: true })
+ const page = new Page(mockOriginalPage, transformFunc)
+ const items: any[] = []
for await (const item of page) {
- items.push(item);
+ items.push(item)
}
expect(items).toEqual([
{ id: 'item1', name: 'Item 1', transformed: true },
{ id: 'item2', name: 'Item 2', transformed: true },
{ id: 'item3', name: 'Item 3', transformed: true },
- ]);
- });
+ ])
+ })
- it('should handle empty items array', async () => {
- const emptyPage = { items: [], data: [], size: 0, total: 0, hasNextPage: false };
- const page = new Page(emptyPage);
- const items: any[] = [];
+ it('should handle transform function that throws error', async () => {
+ const errorTransform = () => {
+ throw new Error('Transform error')
+ }
+ const page = new Page(mockOriginalPage, errorTransform)
- for await (const item of page) {
- items.push(item);
+ const iterate = async () => {
+ for await (const item of page) {
+ // This should throw
+ }
}
- expect(items).toEqual([]);
- });
-
- it('should handle page with data field instead of items', async () => {
- const pageWithData = {
- data: mockItems,
- size: 3,
- total: 10,
- hasNextPage: true,
- };
- const page = new Page(pageWithData);
- const items: any[] = [];
-
- for await (const item of page) {
- items.push(item);
- }
-
- expect(items).toEqual(mockItems);
- });
-
- it('should handle page with neither items nor data', async () => {
- const pageWithoutItems = {
- size: 0,
- total: 0,
- hasNextPage: false,
- };
- const page = new Page(pageWithoutItems);
- const items: any[] = [];
-
- for await (const item of page) {
- items.push(item);
- }
-
- expect(items).toEqual([]);
- });
- });
+ await expect(iterate()).rejects.toThrow('Transform error')
+ })
+ })
describe('get', () => {
- it('should get item by index without transform', async () => {
- mockOriginalPage.get.mockResolvedValue(mockItems[1]);
- const page = new Page(mockOriginalPage);
+ it('should get item by index without transform', () => {
+ const page = new Page(mockOriginalPage)
- const item = await page.get(1);
+ const item = page.get(1)
- expect(item).toEqual(mockItems[1]);
- expect(mockOriginalPage.get).toHaveBeenCalledWith(1);
- });
+ expect(item).toEqual(mockItems[1])
+ })
- it('should get item by index with transform', async () => {
- const transformFunc = (item: any) => ({ ...item, transformed: true });
- mockOriginalPage.get.mockResolvedValue(mockItems[1]);
- const page = new Page(mockOriginalPage, transformFunc);
+ it('should get item by index with transform', () => {
+ const transformFunc = (item: any) => ({ ...item, transformed: true })
+ const page = new Page(mockOriginalPage, transformFunc)
- const item = await page.get(1);
+ const item = page.get(1)
- expect(item).toEqual({ ...mockItems[1], transformed: true });
- expect(mockOriginalPage.get).toHaveBeenCalledWith(1);
- });
+ expect(item).toEqual({ ...mockItems[1], transformed: true })
+ })
- it('should handle out of bounds index', async () => {
- mockOriginalPage.get.mockResolvedValue(undefined);
- const page = new Page(mockOriginalPage);
+ it('should handle out of bounds index', () => {
+ const page = new Page(mockOriginalPage)
- const item = await page.get(999);
+ expect(() => page.get(999)).toThrow(
+ 'Index 999 is out of bounds for page with 3 items'
+ )
+ })
+ })
- expect(item).toBeUndefined();
- expect(mockOriginalPage.get).toHaveBeenCalledWith(999);
- });
+ describe('length getter', () => {
+ it('should return length of items array', () => {
+ const page = new Page(mockOriginalPage)
- it('should handle negative index', async () => {
- mockOriginalPage.get.mockResolvedValue(undefined);
- const page = new Page(mockOriginalPage);
+ expect(page.length).toBe(3)
+ })
- const item = await page.get(-1);
+ it('should handle empty items array', () => {
+ const emptyPage = { ...mockOriginalPage, items: [] }
+ const page = new Page(emptyPage)
- expect(item).toBeUndefined();
- expect(mockOriginalPage.get).toHaveBeenCalledWith(-1);
- });
- });
+ expect(page.length).toBe(0)
+ })
- describe('size getter', () => {
- it('should return size from original page', () => {
- const page = new Page(mockOriginalPage);
+ it('should handle undefined items', () => {
+ const noItemsPage = { ...mockOriginalPage, items: undefined }
+ const page = new Page(noItemsPage)
- expect(page.size).toBe(3);
- });
+ expect(page.length).toBe(0)
+ })
+ })
- it('should handle missing size', () => {
- const pageWithoutSize = { items: mockItems };
- const page = new Page(pageWithoutSize);
+ describe('items getter', () => {
+ it('should return items array without transform', () => {
+ const page = new Page(mockOriginalPage)
- expect(page.size).toBeUndefined();
- });
- });
+ const data = page.items
- describe('total getter', () => {
- it('should return total from original page', () => {
- const page = new Page(mockOriginalPage);
+ expect(data).toEqual(mockItems)
+ })
- expect(page.total).toBe(10);
- });
+ it('should return items array with transform', () => {
+ const transformFunc = (item: any) => ({ ...item, transformed: true })
+ const page = new Page(mockOriginalPage, transformFunc)
- it('should handle missing total', () => {
- const pageWithoutTotal = { items: mockItems };
- const page = new Page(pageWithoutTotal);
-
- expect(page.total).toBeUndefined();
- });
- });
-
- describe('data', () => {
- it('should return data array without transform', async () => {
- const page = new Page(mockOriginalPage);
-
- const data = await page.data();
-
- expect(data).toEqual(mockItems);
- });
-
- it('should return data array with transform', async () => {
- const transformFunc = (item: any) => ({ ...item, transformed: true });
- const page = new Page(mockOriginalPage, transformFunc);
-
- const data = await page.data();
+ const data = page.items
expect(data).toEqual([
{ id: 'item1', name: 'Item 1', transformed: true },
{ id: 'item2', name: 'Item 2', transformed: true },
{ id: 'item3', name: 'Item 3', transformed: true },
- ]);
- });
+ ])
+ })
- it('should handle data as function', async () => {
- const mockDataFunction = jest.fn().mockResolvedValue(mockItems);
- const pageWithDataFunction = {
- data: mockDataFunction,
- size: 3,
- total: 10,
- hasNextPage: true,
- };
- const page = new Page(pageWithDataFunction);
+ it('should handle transform function returning null', () => {
+ const nullTransform = () => null
+ const page = new Page(mockOriginalPage, nullTransform)
- const data = await page.data();
+ const data = page.items
- expect(data).toEqual(mockItems);
- expect(mockDataFunction).toHaveBeenCalled();
- });
+ expect(data).toEqual([null, null, null])
+ })
+ })
- it('should handle items as function', async () => {
- const mockItemsFunction = jest.fn().mockResolvedValue(mockItems);
- const pageWithItemsFunction = {
- items: mockItemsFunction,
- size: 3,
- total: 10,
- hasNextPage: true,
- };
- const page = new Page(pageWithItemsFunction);
+ describe('pagination metadata getters', () => {
+ it('should return total from original page', () => {
+ const page = new Page(mockOriginalPage)
- const data = await page.data();
+ expect(page.total).toBe(10)
+ })
- expect(data).toEqual(mockItems);
- expect(mockItemsFunction).toHaveBeenCalled();
- });
+ it('should return page number from original page', () => {
+ const page = new Page(mockOriginalPage)
- it('should handle empty data', async () => {
- const emptyPage = { items: [], size: 0, total: 0, hasNextPage: false };
- const page = new Page(emptyPage);
+ expect(page.page).toBe(1)
+ })
- const data = await page.data();
+ it('should return size from original page', () => {
+ const page = new Page(mockOriginalPage)
- expect(data).toEqual([]);
- });
+ expect(page.size).toBe(3)
+ })
- it('should prioritize items over data field', async () => {
- const pageWithBoth = {
- items: [{ id: 'from-items' }],
- data: [{ id: 'from-data' }],
- size: 1,
- total: 1,
- hasNextPage: false,
- };
- const page = new Page(pageWithBoth);
+ it('should return pages from original page', () => {
+ const page = new Page(mockOriginalPage)
- const data = await page.data();
+ expect(page.pages).toBe(4)
+ })
- expect(data).toEqual([{ id: 'from-items' }]);
- });
- });
+ it('should handle undefined metadata', () => {
+ const minimalPage = { items: mockItems }
+ const page = new Page(minimalPage as any)
+
+ expect(page.total).toBeUndefined()
+ expect(page.page).toBeUndefined()
+ expect(page.size).toBeUndefined()
+ expect(page.pages).toBeUndefined()
+ })
+ })
describe('hasNextPage getter', () => {
- it('should return hasNextPage from original page', () => {
- const page = new Page(mockOriginalPage);
+ it('should return true when hasNextPage is true', () => {
+ const page = new Page(mockOriginalPage)
- expect(page.hasNextPage).toBe(true);
- });
+ expect(page.hasNextPage).toBe(true)
+ })
- it('should handle missing hasNextPage', () => {
- const pageWithoutHasNextPage = { items: mockItems };
- const page = new Page(pageWithoutHasNextPage);
+ it('should return false when hasNextPage is false', () => {
+ const lastPage = { ...mockOriginalPage, hasNextPage: false }
+ const page = new Page(lastPage)
- expect(page.hasNextPage).toBeUndefined();
- });
+ expect(page.hasNextPage).toBe(false)
+ })
- it('should handle false hasNextPage', () => {
- const lastPage = { ...mockOriginalPage, hasNextPage: false };
- const page = new Page(lastPage);
+ it('should call hasNextPage function if it is a function', () => {
+ const hasNextPageFn = jest.fn(() => true)
+ const pageWithFn = { ...mockOriginalPage, hasNextPage: hasNextPageFn }
+ const page = new Page(pageWithFn)
- expect(page.hasNextPage).toBe(false);
- });
- });
+ const result = page.hasNextPage
- describe('nextPage', () => {
+ expect(result).toBe(true)
+ expect(hasNextPageFn).toHaveBeenCalled()
+ })
+
+ it('should return false when hasNextPage is undefined', () => {
+ const noNextPage = { ...mockOriginalPage }
+ delete noNextPage.hasNextPage
+ const page = new Page(noNextPage)
+
+ expect(page.hasNextPage).toBe(false)
+ })
+ })
+
+ describe('getNextPage', () => {
it('should return next page with same transform function', async () => {
const nextPageData = {
items: [{ id: 'item4', name: 'Item 4' }],
size: 1,
total: 10,
+ page: 2,
+ pages: 4,
hasNextPage: false,
- };
- const transformFunc = (item: any) => ({ ...item, transformed: true });
- mockOriginalPage.nextPage.mockResolvedValue(nextPageData);
- const page = new Page(mockOriginalPage, transformFunc);
+ [Symbol.asyncIterator]: async function*() {
+ for (const item of this.items) {
+ yield item
+ }
+ },
+ }
+ const transformFunc = (item: any) => ({ ...item, transformed: true })
+ mockOriginalPage.getNextPage = jest.fn().mockResolvedValue(nextPageData)
+ const page = new Page(mockOriginalPage, transformFunc)
- const nextPage = await page.nextPage();
+ const nextPage = await page.getNextPage()
- expect(nextPage).toBeInstanceOf(Page);
- expect(nextPage!['_transformFunc']).toBe(transformFunc);
- expect(mockOriginalPage.nextPage).toHaveBeenCalled();
- });
+ expect(nextPage).toBeInstanceOf(Page)
+ expect(nextPage!['_transformFunc']).toBe(transformFunc)
+ expect(mockOriginalPage.getNextPage).toHaveBeenCalled()
+ })
it('should return null when no next page', async () => {
- mockOriginalPage.nextPage.mockResolvedValue(null);
- const page = new Page(mockOriginalPage);
+ mockOriginalPage.getNextPage = jest.fn().mockResolvedValue(null)
+ const page = new Page(mockOriginalPage)
- const nextPage = await page.nextPage();
+ const nextPage = await page.getNextPage()
- expect(nextPage).toBeNull();
- });
+ expect(nextPage).toBeNull()
+ })
- it('should handle next page returning undefined', async () => {
- mockOriginalPage.nextPage.mockResolvedValue(undefined);
- const page = new Page(mockOriginalPage);
+ it('should return null when getNextPage returns undefined', async () => {
+ mockOriginalPage.getNextPage = jest.fn().mockResolvedValue(undefined)
+ const page = new Page(mockOriginalPage)
- const nextPage = await page.nextPage();
+ const nextPage = await page.getNextPage()
- expect(nextPage).toBeNull();
- });
+ expect(nextPage).toBeNull()
+ })
- it('should handle error from original page nextPage', async () => {
- mockOriginalPage.nextPage.mockRejectedValue(new Error('Failed to get next page'));
- const page = new Page(mockOriginalPage);
+ it('should return null when getNextPage method does not exist', async () => {
+ const pageWithoutNext = { ...mockOriginalPage }
+ delete pageWithoutNext.getNextPage
+ const page = new Page(pageWithoutNext)
- await expect(page.nextPage()).rejects.toThrow();
- });
- });
+ const nextPage = await page.getNextPage()
- describe('edge cases and error handling', () => {
- it('should handle null original page', () => {
- const page = new Page(null);
+ expect(nextPage).toBeNull()
+ })
- expect(page['_originalPage']).toBeNull();
- expect(page.size).toBeUndefined();
- expect(page.total).toBeUndefined();
- expect(page.hasNextPage).toBeUndefined();
- });
-
- it('should handle transform function that throws error', async () => {
- const errorTransform = () => {
- throw new Error('Transform error');
- };
- const page = new Page(mockOriginalPage, errorTransform);
-
- await expect(async () => {
- for await (const item of page) {
- // This should throw
- }
- }).rejects.toThrow();
- });
-
- it('should handle transform function returning null', async () => {
- const nullTransform = () => null;
- const page = new Page(mockOriginalPage, nullTransform);
- const items: any[] = [];
-
- for await (const item of page) {
- items.push(item);
- }
-
- expect(items).toEqual([null, null, null]);
- });
-
- it('should handle very large datasets', async () => {
- const largeItems = Array.from({ length: 10000 }, (_, i) => ({ id: i }));
- const largePage = {
- items: largeItems,
- size: 10000,
- total: 10000,
- hasNextPage: false,
- };
- const page = new Page(largePage);
- let count = 0;
-
- for await (const item of page) {
- count++;
- if (count > 10) break; // Don't actually iterate through all 10k items
- }
-
- expect(count).toBe(11);
- });
-
- it('should handle circular reference in items', async () => {
- const circularItem: any = { id: 'circular' };
- circularItem.self = circularItem;
- const pageWithCircular = {
- items: [circularItem],
- size: 1,
- total: 1,
- hasNextPage: false,
- };
- const page = new Page(pageWithCircular);
- const items: any[] = [];
-
- for await (const item of page) {
- items.push(item);
- }
-
- expect(items).toHaveLength(1);
- expect(items[0].id).toBe('circular');
- expect(items[0].self).toBe(items[0]);
- });
-
- it('should handle items with complex nested structures', async () => {
- const complexItems = [
- {
- id: 'complex1',
- nested: {
- deep: {
- value: 'deeply nested',
- array: [1, 2, { nested: 'array object' }],
- },
- },
- metadata: new Map([['key', 'value']]),
+ it('should propagate transform function to next page', async () => {
+ const nextPageData = {
+ items: [{ id: 'item4', name: 'Item 4' }],
+ [Symbol.asyncIterator]: async function*() {
+ for (const item of this.items) {
+ yield item
+ }
},
- ];
- const complexPage = {
- items: complexItems,
- size: 1,
- total: 1,
- hasNextPage: false,
- };
- const page = new Page(complexPage);
- const items: any[] = [];
-
- for await (const item of page) {
- items.push(item);
}
+ const transformFunc = (item: any) => ({ ...item, count: 999 })
+ mockOriginalPage.getNextPage = jest.fn().mockResolvedValue(nextPageData)
+ const page = new Page(mockOriginalPage, transformFunc)
- expect(items).toEqual(complexItems);
- expect(items[0].nested.deep.array[2].nested).toBe('array object');
- });
+ const nextPage = await page.getNextPage()
- it('should handle async transform function', async () => {
- const asyncTransform = async (item: any) => {
- await new Promise(resolve => setTimeout(resolve, 1));
- return { ...item, asyncTransformed: true };
- };
- mockOriginalPage.get.mockResolvedValue(mockItems[0]);
- const page = new Page(mockOriginalPage, asyncTransform);
+ expect(nextPage).not.toBeNull()
+ const transformedItem = nextPage!.get(0)
+ expect(transformedItem).toEqual({
+ id: 'item4',
+ name: 'Item 4',
+ count: 999,
+ })
+ })
- const item = await page.get(0);
+ it('should handle error from getNextPage', async () => {
+ mockOriginalPage.getNextPage = jest
+ .fn()
+ .mockRejectedValue(new Error('Failed to get next page'))
+ const page = new Page(mockOriginalPage)
- expect(item).toEqual({ ...mockItems[0], asyncTransformed: true });
- });
- });
-});
+ await expect(page.getNextPage()).rejects.toThrow(
+ 'Failed to get next page'
+ )
+ })
+ })
+})
diff --git a/sdks/typescript/__tests__/peer.test.ts b/sdks/typescript/__tests__/peer.test.ts
index 9cfde7ea..c85e6118 100644
--- a/sdks/typescript/__tests__/peer.test.ts
+++ b/sdks/typescript/__tests__/peer.test.ts
@@ -67,7 +67,7 @@ describe('Peer', () => {
expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
'test-workspace',
'test-peer',
- { query: 'Hello', stream: undefined, target: undefined, session_id: undefined }
+ { query: 'Hello', stream: false, target: undefined, session_id: undefined }
);
});
@@ -89,17 +89,9 @@ describe('Peer', () => {
expect(result).toBeNull();
});
- it('should handle chat with streaming option', async () => {
- const mockResponse = { content: 'Streamed response' };
- mockClient.workspaces.peers.chat.mockResolvedValue(mockResponse);
-
- await peer.chat('Hello', { stream: true });
-
- expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
- 'test-workspace',
- 'test-peer',
- { query: 'Hello', stream: true, target: undefined, session_id: undefined }
- );
+ it.skip('should handle chat with streaming option', async () => {
+ // Skipped: streaming now uses fetch API directly, not the mocked client
+ // TODO: Add proper streaming tests with fetch mocking when needed
});
it('should handle chat with target peer', async () => {
@@ -112,7 +104,7 @@ describe('Peer', () => {
expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
'test-workspace',
'test-peer',
- { query: 'Hello', stream: undefined, target: 'target-peer', session_id: undefined }
+ { query: 'Hello', stream: false, target: 'target-peer', session_id: undefined }
);
});
@@ -125,7 +117,7 @@ describe('Peer', () => {
expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
'test-workspace',
'test-peer',
- { query: 'Hello', stream: undefined, target: 'string-target', session_id: undefined }
+ { query: 'Hello', stream: false, target: 'string-target', session_id: undefined }
);
});
@@ -138,23 +130,24 @@ describe('Peer', () => {
expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
'test-workspace',
'test-peer',
- { query: 'Hello', stream: undefined, target: undefined, session_id: 'session-123' }
+ { query: 'Hello', stream: false, target: undefined, session_id: 'session-123' }
);
});
- it('should handle all options together', async () => {
- const targetPeer = new Peer('target-peer', 'test-workspace', mockClient);
- const mockResponse = { content: 'Full options response' };
- mockClient.workspaces.peers.chat.mockResolvedValue(mockResponse);
+ // TODO: Re-enable after regenerating Stainless SDK with streaming support
+ // it('should handle all options together', async () => {
+ // const targetPeer = new Peer('target-peer', 'test-workspace', mockClient);
+ // const mockResponse = { content: 'Full options response' };
+ // mockClient.workspaces.peers.chat.mockResolvedValue(mockResponse);
- await peer.chat('Hello', { stream: true, target: targetPeer, sessionId: 'session-456' });
+ // await peer.chat('Hello', { stream: true, target: targetPeer, sessionId: 'session-456' });
- expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
- 'test-workspace',
- 'test-peer',
- { query: 'Hello', stream: true, target: 'target-peer', session_id: 'session-456' }
- );
- });
+ // expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
+ // 'test-workspace',
+ // 'test-peer',
+ // { query: 'Hello', stream: true, target: 'target-peer', session_id: 'session-456' }
+ // );
+ // });
it('should handle API errors', async () => {
mockClient.workspaces.peers.chat.mockRejectedValue(new Error('Chat failed'));
@@ -429,4 +422,96 @@ describe('Peer', () => {
await expect(peer.search('test')).rejects.toThrow();
});
});
+
+ describe('card', () => {
+ beforeEach(() => {
+ mockClient.workspaces.peers.card = jest.fn();
+ });
+
+ it('should get peer card without target', async () => {
+ const mockCardResponse = {
+ peer_card: ['Fact 1 about peer', 'Fact 2 about peer', 'Fact 3 about peer'],
+ };
+ mockClient.workspaces.peers.card.mockResolvedValue(mockCardResponse);
+
+ const result = await peer.card();
+
+ expect(result).toBe('Fact 1 about peer\nFact 2 about peer\nFact 3 about peer');
+ expect(mockClient.workspaces.peers.card).toHaveBeenCalledWith(
+ 'test-workspace',
+ 'test-peer',
+ { target: undefined }
+ );
+ });
+
+ it('should get peer card with target as string', async () => {
+ const mockCardResponse = {
+ peer_card: ['What peer knows about target'],
+ };
+ mockClient.workspaces.peers.card.mockResolvedValue(mockCardResponse);
+
+ const result = await peer.card('target-peer');
+
+ expect(result).toBe('What peer knows about target');
+ expect(mockClient.workspaces.peers.card).toHaveBeenCalledWith(
+ 'test-workspace',
+ 'test-peer',
+ { target: 'target-peer' }
+ );
+ });
+
+ it('should get peer card with target as Peer object', async () => {
+ const targetPeer = new Peer('target-peer', 'test-workspace', mockClient);
+ const mockCardResponse = {
+ peer_card: ['What peer knows about target peer'],
+ };
+ mockClient.workspaces.peers.card.mockResolvedValue(mockCardResponse);
+
+ const result = await peer.card(targetPeer);
+
+ expect(result).toBe('What peer knows about target peer');
+ expect(mockClient.workspaces.peers.card).toHaveBeenCalledWith(
+ 'test-workspace',
+ 'test-peer',
+ { target: 'target-peer' }
+ );
+ });
+
+ it('should return empty string when peer_card is null', async () => {
+ const mockCardResponse = {
+ peer_card: null,
+ };
+ mockClient.workspaces.peers.card.mockResolvedValue(mockCardResponse);
+
+ const result = await peer.card();
+
+ expect(result).toBe('');
+ });
+
+ it('should return empty string when peer_card is undefined', async () => {
+ const mockCardResponse = {};
+ mockClient.workspaces.peers.card.mockResolvedValue(mockCardResponse);
+
+ const result = await peer.card();
+
+ expect(result).toBe('');
+ });
+
+ it('should throw error for empty string target', async () => {
+ await expect(peer.card('')).rejects.toThrow('target string cannot be empty');
+ await expect(peer.card(' ')).rejects.toThrow('target string cannot be empty');
+ });
+
+ it('should throw error for invalid target type', async () => {
+ await expect(peer.card(123 as any)).rejects.toThrow('target must be string, Peer, or undefined');
+ await expect(peer.card(null as any)).rejects.toThrow('target must be string, Peer, or undefined');
+ await expect(peer.card({} as any)).rejects.toThrow('target must be string, Peer, or undefined');
+ });
+
+ it('should handle API errors', async () => {
+ mockClient.workspaces.peers.card.mockRejectedValue(new Error('Card fetch failed'));
+
+ await expect(peer.card()).rejects.toThrow('Card fetch failed');
+ });
+ });
});
diff --git a/sdks/typescript/__tests__/session.test.ts b/sdks/typescript/__tests__/session.test.ts
index 1d0d2d9e..942cf7c1 100644
--- a/sdks/typescript/__tests__/session.test.ts
+++ b/sdks/typescript/__tests__/session.test.ts
@@ -1,8 +1,8 @@
-import { Session } from '../src/session';
-import { Peer } from '../src/peer';
-import { Page } from '../src/pagination';
-import { SessionContext } from '../src/session_context';
-import { Honcho } from '../src/client';
+import { Session } from '../src/session'
+import { Peer } from '../src/peer'
+import { Page } from '../src/pagination'
+import { SessionContext } from '../src/session_context'
+import { Honcho } from '../src/client'
// Mock the @honcho-ai/core module
jest.mock('@honcho-ai/core', () => {
@@ -24,247 +24,259 @@ jest.mock('@honcho-ai/core', () => {
},
getOrCreate: jest.fn(),
update: jest.fn(),
+ delete: jest.fn(),
getContext: jest.fn(),
search: jest.fn(),
},
peers: {
workingRepresentation: jest.fn(),
},
- getOrCreate: jest.fn().mockResolvedValue({ id: 'test-workspace', metadata: {} }),
+ deriverStatus: jest.fn(),
+ getOrCreate: jest.fn(),
update: jest.fn(),
list: jest.fn(),
search: jest.fn(),
},
- }));
-});
+ }))
+})
describe('Session', () => {
- let honcho: Honcho;
- let session: Session;
- let mockClient: any;
+ let honcho: Honcho
+ let session: Session
+ let mockClient: any
beforeEach(() => {
- jest.clearAllMocks();
+ jest.clearAllMocks()
honcho = new Honcho({
workspaceId: 'test-workspace',
apiKey: 'test-key',
environment: 'local',
- });
+ })
- session = new Session('test-session', 'test-workspace', (honcho as any)._client);
- mockClient = (honcho as any)._client;
- });
+ session = new Session(
+ 'test-session',
+ 'test-workspace',
+ (honcho as any)._client
+ )
+ mockClient = (honcho as any)._client
+ })
describe('constructor', () => {
it('should initialize with correct properties', () => {
- const newSession = new Session('session-id', 'test-workspace', mockClient);
+ const newSession = new Session('session-id', 'test-workspace', mockClient)
- expect(newSession.id).toBe('session-id');
- expect(newSession.workspaceId).toBe('test-workspace');
- expect(newSession['_client']).toBe(mockClient);
- });
- });
+ expect(newSession.id).toBe('session-id')
+ expect(newSession.workspaceId).toBe('test-workspace')
+ expect(newSession['_client']).toBe(mockClient)
+ })
+ })
describe('addPeers', () => {
it('should add single peer by string ID', async () => {
- mockClient.workspaces.sessions.peers.add.mockResolvedValue({});
+ mockClient.workspaces.sessions.peers.add.mockResolvedValue({})
- await session.addPeers('peer1');
+ await session.addPeers('peer1')
expect(mockClient.workspaces.sessions.peers.add).toHaveBeenCalledWith(
'test-workspace',
'test-session',
- { 'peer1': {} }
- );
- });
+ { peer1: {} }
+ )
+ })
it('should add single peer by Peer object', async () => {
- const peer = new Peer('peer1', 'test-workspace', mockClient);
- mockClient.workspaces.sessions.peers.add.mockResolvedValue({});
+ const peer = new Peer('peer1', 'test-workspace', mockClient)
+ mockClient.workspaces.sessions.peers.add.mockResolvedValue({})
- await session.addPeers(peer);
+ await session.addPeers(peer)
expect(mockClient.workspaces.sessions.peers.add).toHaveBeenCalledWith(
'test-workspace',
'test-session',
- { 'peer1': {} }
- );
- });
+ { peer1: {} }
+ )
+ })
it('should add array of peer strings', async () => {
- mockClient.workspaces.sessions.peers.add.mockResolvedValue({});
+ mockClient.workspaces.sessions.peers.add.mockResolvedValue({})
- await session.addPeers(['peer1', 'peer2', 'peer3']);
+ await session.addPeers(['peer1', 'peer2', 'peer3'])
expect(mockClient.workspaces.sessions.peers.add).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{
- 'peer1': {},
- 'peer2': {},
- 'peer3': {}
+ peer1: {},
+ peer2: {},
+ peer3: {},
}
- );
- });
+ )
+ })
it('should add array of Peer objects', async () => {
const peers = [
new Peer('peer1', 'test-workspace', mockClient),
new Peer('peer2', 'test-workspace', mockClient),
new Peer('peer3', 'test-workspace', mockClient),
- ];
- mockClient.workspaces.sessions.peers.add.mockResolvedValue({});
+ ]
+ mockClient.workspaces.sessions.peers.add.mockResolvedValue({})
- await session.addPeers(peers);
+ await session.addPeers(peers)
expect(mockClient.workspaces.sessions.peers.add).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{
- 'peer1': {},
- 'peer2': {},
- 'peer3': {}
+ peer1: {},
+ peer2: {},
+ peer3: {},
}
- );
- });
+ )
+ })
it('should add mixed array of strings and Peer objects', async () => {
const peers = [
'string-peer',
new Peer('object-peer', 'test-workspace', mockClient),
- ];
- mockClient.workspaces.sessions.peers.add.mockResolvedValue({});
+ ]
+ mockClient.workspaces.sessions.peers.add.mockResolvedValue({})
- await session.addPeers(peers);
+ await session.addPeers(peers)
expect(mockClient.workspaces.sessions.peers.add).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{
'string-peer': {},
- 'object-peer': {}
+ 'object-peer': {},
}
- );
- });
+ )
+ })
it('should add peer with SessionPeerConfig', async () => {
- const { SessionPeerConfig } = require('../src/session');
- const config = new SessionPeerConfig(false, true);
- mockClient.workspaces.sessions.peers.add.mockResolvedValue({});
+ const { SessionPeerConfig } = require('../src/session')
+ const config = new SessionPeerConfig(false, true)
+ mockClient.workspaces.sessions.peers.add.mockResolvedValue({})
- await session.addPeers([['peer1', config]]);
+ await session.addPeers([['peer1', config]])
expect(mockClient.workspaces.sessions.peers.add).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{
- 'peer1': { observe_me: false, observe_others: true }
+ peer1: { observe_me: false, observe_others: true },
}
- );
- });
+ )
+ })
it('should handle API errors', async () => {
- mockClient.workspaces.sessions.peers.add.mockRejectedValue(new Error('Failed to add peers'));
+ mockClient.workspaces.sessions.peers.add.mockRejectedValue(
+ new Error('Failed to add peers')
+ )
- await expect(session.addPeers('peer1')).rejects.toThrow();
- });
- });
+ await expect(session.addPeers('peer1')).rejects.toThrow()
+ })
+ })
describe('setPeers', () => {
it('should set single peer by string ID', async () => {
- mockClient.workspaces.sessions.peers.set.mockResolvedValue({});
+ mockClient.workspaces.sessions.peers.set.mockResolvedValue({})
- await session.setPeers('peer1');
+ await session.setPeers('peer1')
expect(mockClient.workspaces.sessions.peers.set).toHaveBeenCalledWith(
'test-workspace',
'test-session',
- { 'peer1': {} }
- );
- });
+ { peer1: {} }
+ )
+ })
it('should set single peer by Peer object', async () => {
- const peer = new Peer('peer1', 'test-workspace', mockClient);
- mockClient.workspaces.sessions.peers.set.mockResolvedValue({});
+ const peer = new Peer('peer1', 'test-workspace', mockClient)
+ mockClient.workspaces.sessions.peers.set.mockResolvedValue({})
- await session.setPeers(peer);
+ await session.setPeers(peer)
expect(mockClient.workspaces.sessions.peers.set).toHaveBeenCalledWith(
'test-workspace',
'test-session',
- { 'peer1': {} }
- );
- });
+ { peer1: {} }
+ )
+ })
it('should set array of peers', async () => {
- const peers = ['peer1', new Peer('peer2', 'test-workspace', mockClient)];
- mockClient.workspaces.sessions.peers.set.mockResolvedValue({});
+ const peers = ['peer1', new Peer('peer2', 'test-workspace', mockClient)]
+ mockClient.workspaces.sessions.peers.set.mockResolvedValue({})
- await session.setPeers(peers);
+ await session.setPeers(peers)
expect(mockClient.workspaces.sessions.peers.set).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{
- 'peer1': {},
- 'peer2': {}
+ peer1: {},
+ peer2: {},
}
- );
- });
+ )
+ })
it('should handle API errors', async () => {
- mockClient.workspaces.sessions.peers.set.mockRejectedValue(new Error('Failed to set peers'));
+ mockClient.workspaces.sessions.peers.set.mockRejectedValue(
+ new Error('Failed to set peers')
+ )
- await expect(session.setPeers(['peer1'])).rejects.toThrow();
- });
- });
+ await expect(session.setPeers(['peer1'])).rejects.toThrow()
+ })
+ })
describe('removePeers', () => {
it('should remove single peer by string ID', async () => {
- mockClient.workspaces.sessions.peers.remove.mockResolvedValue({});
+ mockClient.workspaces.sessions.peers.remove.mockResolvedValue({})
- await session.removePeers('peer1');
+ await session.removePeers('peer1')
expect(mockClient.workspaces.sessions.peers.remove).toHaveBeenCalledWith(
'test-workspace',
'test-session',
['peer1']
- );
- });
+ )
+ })
it('should remove single peer by Peer object', async () => {
- const peer = new Peer('peer1', 'test-workspace', mockClient);
- mockClient.workspaces.sessions.peers.remove.mockResolvedValue({});
+ const peer = new Peer('peer1', 'test-workspace', mockClient)
+ mockClient.workspaces.sessions.peers.remove.mockResolvedValue({})
- await session.removePeers(peer);
+ await session.removePeers(peer)
expect(mockClient.workspaces.sessions.peers.remove).toHaveBeenCalledWith(
'test-workspace',
'test-session',
['peer1']
- );
- });
+ )
+ })
it('should remove array of peers', async () => {
- const peers = ['peer1', new Peer('peer2', 'test-workspace', mockClient)];
- mockClient.workspaces.sessions.peers.remove.mockResolvedValue({});
+ const peers = ['peer1', new Peer('peer2', 'test-workspace', mockClient)]
+ mockClient.workspaces.sessions.peers.remove.mockResolvedValue({})
- await session.removePeers(peers);
+ await session.removePeers(peers)
expect(mockClient.workspaces.sessions.peers.remove).toHaveBeenCalledWith(
'test-workspace',
'test-session',
['peer1', 'peer2']
- );
- });
+ )
+ })
it('should handle API errors', async () => {
- mockClient.workspaces.sessions.peers.remove.mockRejectedValue(new Error('Failed to remove peers'));
+ mockClient.workspaces.sessions.peers.remove.mockRejectedValue(
+ new Error('Failed to remove peers')
+ )
- await expect(session.removePeers(['peer1'])).rejects.toThrow();
- });
- });
+ await expect(session.removePeers(['peer1'])).rejects.toThrow()
+ })
+ })
describe('getPeers', () => {
it('should return array of Peer instances', async () => {
@@ -276,20 +288,20 @@ describe('Session', () => {
total: 2,
size: 2,
hasNextPage: false,
- };
- mockClient.workspaces.sessions.peers.list.mockResolvedValue(mockPeersData);
+ }
+ mockClient.workspaces.sessions.peers.list.mockResolvedValue(mockPeersData)
- const peers = await session.getPeers();
+ const peers = await session.getPeers()
- expect(peers).toBeInstanceOf(Array);
- expect(peers).toHaveLength(2);
- expect(peers[0]).toBeInstanceOf(Peer);
- expect(peers[1]).toBeInstanceOf(Peer);
+ expect(peers).toBeInstanceOf(Array)
+ expect(peers).toHaveLength(2)
+ expect(peers[0]).toBeInstanceOf(Peer)
+ expect(peers[1]).toBeInstanceOf(Peer)
expect(mockClient.workspaces.sessions.peers.list).toHaveBeenCalledWith(
'test-workspace',
'test-session'
- );
- });
+ )
+ })
it('should handle empty peers list', async () => {
const mockPeersData = {
@@ -297,85 +309,87 @@ describe('Session', () => {
total: 0,
size: 0,
hasNextPage: false,
- };
- mockClient.workspaces.sessions.peers.list.mockResolvedValue(mockPeersData);
+ }
+ mockClient.workspaces.sessions.peers.list.mockResolvedValue(mockPeersData)
- const peers = await session.getPeers();
+ const peers = await session.getPeers()
- expect(peers).toBeInstanceOf(Array);
- expect(peers.length).toBe(0);
- });
+ expect(peers).toBeInstanceOf(Array)
+ expect(peers.length).toBe(0)
+ })
it('should handle API errors', async () => {
- mockClient.workspaces.sessions.peers.list.mockRejectedValue(new Error('Failed to get peers'));
+ mockClient.workspaces.sessions.peers.list.mockRejectedValue(
+ new Error('Failed to get peers')
+ )
- await expect(session.getPeers()).rejects.toThrow();
- });
- });
+ await expect(session.getPeers()).rejects.toThrow()
+ })
+ })
describe('getPeerConfig', () => {
it('should return peer configuration', async () => {
- const mockConfig = { observe_me: true, observe_others: false };
- mockClient.workspaces.sessions.peers.getConfig.mockResolvedValue(mockConfig);
+ const mockConfig = { observe_me: true, observe_others: false }
+ mockClient.workspaces.sessions.peers.getConfig.mockResolvedValue(
+ mockConfig
+ )
- const config = await session.getPeerConfig('peer1');
+ const config = await session.getPeerConfig('peer1')
- expect(config).toEqual(mockConfig);
- expect(mockClient.workspaces.sessions.peers.getConfig).toHaveBeenCalledWith(
- 'test-workspace',
- 'test-session',
- 'peer1'
- );
- });
+ expect(config).toEqual(mockConfig)
+ expect(
+ mockClient.workspaces.sessions.peers.getConfig
+ ).toHaveBeenCalledWith('test-workspace', 'test-session', 'peer1')
+ })
it('should handle Peer object input', async () => {
- const peer = new Peer('peer1', 'test-workspace', mockClient);
- const mockConfig = { observe_me: false, observe_others: true };
- mockClient.workspaces.sessions.peers.getConfig.mockResolvedValue(mockConfig);
+ const peer = new Peer('peer1', 'test-workspace', mockClient)
+ const mockConfig = { observe_me: false, observe_others: true }
+ mockClient.workspaces.sessions.peers.getConfig.mockResolvedValue(
+ mockConfig
+ )
- const config = await session.getPeerConfig(peer);
+ const config = await session.getPeerConfig(peer)
- expect(config).toEqual(mockConfig);
- expect(mockClient.workspaces.sessions.peers.getConfig).toHaveBeenCalledWith(
- 'test-workspace',
- 'test-session',
- 'peer1'
- );
- });
- });
+ expect(config).toEqual(mockConfig)
+ expect(
+ mockClient.workspaces.sessions.peers.getConfig
+ ).toHaveBeenCalledWith('test-workspace', 'test-session', 'peer1')
+ })
+ })
describe('setPeerConfig', () => {
it('should set peer configuration', async () => {
- const { SessionPeerConfig } = require('../src/session');
- const config = new SessionPeerConfig(false, true);
- mockClient.workspaces.sessions.peers.setConfig.mockResolvedValue({});
+ const { SessionPeerConfig } = require('../src/session')
+ const config = new SessionPeerConfig(false, true)
+ mockClient.workspaces.sessions.peers.setConfig.mockResolvedValue({})
- await session.setPeerConfig('peer1', config);
+ await session.setPeerConfig('peer1', config)
- expect(mockClient.workspaces.sessions.peers.setConfig).toHaveBeenCalledWith(
- 'test-workspace',
- 'test-session',
- 'peer1',
- { observe_me: false, observe_others: true }
- );
- });
+ expect(
+ mockClient.workspaces.sessions.peers.setConfig
+ ).toHaveBeenCalledWith('test-workspace', 'test-session', 'peer1', {
+ observe_me: false,
+ observe_others: true,
+ })
+ })
it('should handle Peer object input', async () => {
- const peer = new Peer('peer1', 'test-workspace', mockClient);
- const { SessionPeerConfig } = require('../src/session');
- const config = new SessionPeerConfig(true, false);
- mockClient.workspaces.sessions.peers.setConfig.mockResolvedValue({});
+ const peer = new Peer('peer1', 'test-workspace', mockClient)
+ const { SessionPeerConfig } = require('../src/session')
+ const config = new SessionPeerConfig(true, false)
+ mockClient.workspaces.sessions.peers.setConfig.mockResolvedValue({})
- await session.setPeerConfig(peer, config);
+ await session.setPeerConfig(peer, config)
- expect(mockClient.workspaces.sessions.peers.setConfig).toHaveBeenCalledWith(
- 'test-workspace',
- 'test-session',
- 'peer1',
- { observe_me: true, observe_others: false }
- );
- });
- });
+ expect(
+ mockClient.workspaces.sessions.peers.setConfig
+ ).toHaveBeenCalledWith('test-workspace', 'test-session', 'peer1', {
+ observe_me: true,
+ observe_others: false,
+ })
+ })
+ })
describe('addMessages', () => {
it('should add single message', async () => {
@@ -383,78 +397,78 @@ describe('Session', () => {
peer_id: 'peer1',
content: 'Hello world',
metadata: { type: 'greeting' },
- };
- mockClient.workspaces.sessions.messages.create.mockResolvedValue({});
+ }
+ mockClient.workspaces.sessions.messages.create.mockResolvedValue({})
- await session.addMessages(message);
+ await session.addMessages(message)
- expect(mockClient.workspaces.sessions.messages.create).toHaveBeenCalledWith(
- 'test-workspace',
- 'test-session',
- {
- messages: [{
+ expect(
+ mockClient.workspaces.sessions.messages.create
+ ).toHaveBeenCalledWith('test-workspace', 'test-session', {
+ messages: [
+ {
peer_id: 'peer1',
content: 'Hello world',
- metadata: { type: 'greeting' }
- }]
- }
- );
- });
+ metadata: { type: 'greeting' },
+ },
+ ],
+ })
+ })
it('should add array of messages', async () => {
const messages = [
{ peer_id: 'peer1', content: 'Message 1', metadata: { order: 1 } },
{ peer_id: 'peer2', content: 'Message 2', metadata: { order: 2 } },
- ];
- mockClient.workspaces.sessions.messages.create.mockResolvedValue({});
+ ]
+ mockClient.workspaces.sessions.messages.create.mockResolvedValue({})
- await session.addMessages(messages);
+ await session.addMessages(messages)
- expect(mockClient.workspaces.sessions.messages.create).toHaveBeenCalledWith(
- 'test-workspace',
- 'test-session',
- {
- messages: [
- { peer_id: 'peer1', content: 'Message 1', metadata: { order: 1 } },
- { peer_id: 'peer2', content: 'Message 2', metadata: { order: 2 } },
- ]
- }
- );
- });
+ expect(
+ mockClient.workspaces.sessions.messages.create
+ ).toHaveBeenCalledWith('test-workspace', 'test-session', {
+ messages: [
+ { peer_id: 'peer1', content: 'Message 1', metadata: { order: 1 } },
+ { peer_id: 'peer2', content: 'Message 2', metadata: { order: 2 } },
+ ],
+ })
+ })
it('should handle messages without metadata', async () => {
const message = {
peer_id: 'peer1',
content: 'Simple message',
- };
- mockClient.workspaces.sessions.messages.create.mockResolvedValue({});
+ }
+ mockClient.workspaces.sessions.messages.create.mockResolvedValue({})
- await session.addMessages(message);
+ await session.addMessages(message)
- expect(mockClient.workspaces.sessions.messages.create).toHaveBeenCalledWith(
- 'test-workspace',
- 'test-session',
- { messages: [{ peer_id: 'peer1', content: 'Simple message' }] }
- );
- });
+ expect(
+ mockClient.workspaces.sessions.messages.create
+ ).toHaveBeenCalledWith('test-workspace', 'test-session', {
+ messages: [{ peer_id: 'peer1', content: 'Simple message' }],
+ })
+ })
it('should handle empty array', async () => {
- mockClient.workspaces.sessions.messages.create.mockResolvedValue({});
+ mockClient.workspaces.sessions.messages.create.mockResolvedValue({})
- await session.addMessages([]);
+ await session.addMessages([])
- expect(mockClient.workspaces.sessions.messages.create).toHaveBeenCalledWith(
- 'test-workspace',
- 'test-session',
- { messages: [] }
- );
- });
+ expect(
+ mockClient.workspaces.sessions.messages.create
+ ).toHaveBeenCalledWith('test-workspace', 'test-session', { messages: [] })
+ })
it('should handle API errors', async () => {
- mockClient.workspaces.sessions.messages.create.mockRejectedValue(new Error('Failed to add messages'));
+ mockClient.workspaces.sessions.messages.create.mockRejectedValue(
+ new Error('Failed to add messages')
+ )
- await expect(session.addMessages({ peer_id: 'peer1', content: 'test' })).rejects.toThrow();
- });
+ await expect(
+ session.addMessages({ peer_id: 'peer1', content: 'test' })
+ ).rejects.toThrow()
+ })
it('should add message with custom timestamp', async () => {
const message = {
@@ -462,24 +476,24 @@ describe('Session', () => {
content: 'Message with timestamp',
created_at: '2023-01-01T12:00:00Z',
metadata: { test: 'timestamp' },
- };
- mockClient.workspaces.sessions.messages.create.mockResolvedValue({});
+ }
+ mockClient.workspaces.sessions.messages.create.mockResolvedValue({})
- await session.addMessages(message);
+ await session.addMessages(message)
- expect(mockClient.workspaces.sessions.messages.create).toHaveBeenCalledWith(
- 'test-workspace',
- 'test-session',
- {
- messages: [{
+ expect(
+ mockClient.workspaces.sessions.messages.create
+ ).toHaveBeenCalledWith('test-workspace', 'test-session', {
+ messages: [
+ {
peer_id: 'peer1',
content: 'Message with timestamp',
created_at: '2023-01-01T12:00:00Z',
- metadata: { test: 'timestamp' }
- }]
- }
- );
- });
+ metadata: { test: 'timestamp' },
+ },
+ ],
+ })
+ })
it('should add message with null timestamp', async () => {
const message = {
@@ -487,24 +501,24 @@ describe('Session', () => {
content: 'Message without timestamp',
created_at: null,
metadata: { test: 'no_timestamp' },
- };
- mockClient.workspaces.sessions.messages.create.mockResolvedValue({});
+ }
+ mockClient.workspaces.sessions.messages.create.mockResolvedValue({})
- await session.addMessages(message);
+ await session.addMessages(message)
- expect(mockClient.workspaces.sessions.messages.create).toHaveBeenCalledWith(
- 'test-workspace',
- 'test-session',
- {
- messages: [{
+ expect(
+ mockClient.workspaces.sessions.messages.create
+ ).toHaveBeenCalledWith('test-workspace', 'test-session', {
+ messages: [
+ {
peer_id: 'peer1',
content: 'Message without timestamp',
created_at: null,
- metadata: { test: 'no_timestamp' }
- }]
- }
- );
- });
+ metadata: { test: 'no_timestamp' },
+ },
+ ],
+ })
+ })
it('should add mixed messages with and without timestamps', async () => {
const messages = [
@@ -512,51 +526,49 @@ describe('Session', () => {
peer_id: 'peer1',
content: 'Message with timestamp',
created_at: '2023-01-01T12:00:00Z',
- metadata: { type: 'historical' }
+ metadata: { type: 'historical' },
},
{
peer_id: 'peer2',
content: 'Message without timestamp',
- metadata: { type: 'current' }
+ metadata: { type: 'current' },
},
{
peer_id: 'peer3',
content: 'Message with null timestamp',
created_at: null,
- metadata: { type: 'default' }
- }
- ];
- mockClient.workspaces.sessions.messages.create.mockResolvedValue({});
+ metadata: { type: 'default' },
+ },
+ ]
+ mockClient.workspaces.sessions.messages.create.mockResolvedValue({})
- await session.addMessages(messages);
+ await session.addMessages(messages)
- expect(mockClient.workspaces.sessions.messages.create).toHaveBeenCalledWith(
- 'test-workspace',
- 'test-session',
- {
- messages: [
- {
- peer_id: 'peer1',
- content: 'Message with timestamp',
- created_at: '2023-01-01T12:00:00Z',
- metadata: { type: 'historical' }
- },
- {
- peer_id: 'peer2',
- content: 'Message without timestamp',
- metadata: { type: 'current' }
- },
- {
- peer_id: 'peer3',
- content: 'Message with null timestamp',
- created_at: null,
- metadata: { type: 'default' }
- }
- ]
- }
- );
- });
- });
+ expect(
+ mockClient.workspaces.sessions.messages.create
+ ).toHaveBeenCalledWith('test-workspace', 'test-session', {
+ messages: [
+ {
+ peer_id: 'peer1',
+ content: 'Message with timestamp',
+ created_at: '2023-01-01T12:00:00Z',
+ metadata: { type: 'historical' },
+ },
+ {
+ peer_id: 'peer2',
+ content: 'Message without timestamp',
+ metadata: { type: 'current' },
+ },
+ {
+ peer_id: 'peer3',
+ content: 'Message with null timestamp',
+ created_at: null,
+ metadata: { type: 'default' },
+ },
+ ],
+ })
+ })
+ })
describe('getMessages', () => {
it('should get messages without filter', async () => {
@@ -568,18 +580,20 @@ describe('Session', () => {
total: 2,
size: 2,
hasNextPage: false,
- };
- mockClient.workspaces.sessions.messages.list.mockResolvedValue(mockMessagesData);
+ }
+ mockClient.workspaces.sessions.messages.list.mockResolvedValue(
+ mockMessagesData
+ )
- const messagesPage = await session.getMessages();
+ const messagesPage = await session.getMessages()
- expect(messagesPage).toBeInstanceOf(Page);
+ expect(messagesPage).toBeInstanceOf(Page)
expect(mockClient.workspaces.sessions.messages.list).toHaveBeenCalledWith(
'test-workspace',
'test-session',
undefined
- );
- });
+ )
+ })
it('should get messages with filter options', async () => {
const mockMessagesData = {
@@ -587,94 +601,105 @@ describe('Session', () => {
total: 0,
size: 0,
hasNextPage: false,
- };
- mockClient.workspaces.sessions.messages.list.mockResolvedValue(mockMessagesData);
+ }
+ mockClient.workspaces.sessions.messages.list.mockResolvedValue(
+ mockMessagesData
+ )
- const filter = { peer_id: { value: 'peer1' }, type: { value: 'important' } };
- await session.getMessages(filter);
+ const filter = {
+ peer_id: { value: 'peer1' },
+ type: { value: 'important' },
+ }
+ await session.getMessages(filter)
expect(mockClient.workspaces.sessions.messages.list).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{ peer_id: { value: 'peer1' }, type: { value: 'important' } }
- );
- });
+ )
+ })
it('should handle API errors', async () => {
- mockClient.workspaces.sessions.messages.list.mockRejectedValue(new Error('Failed to get messages'));
+ mockClient.workspaces.sessions.messages.list.mockRejectedValue(
+ new Error('Failed to get messages')
+ )
- await expect(session.getMessages()).rejects.toThrow();
- });
- });
+ await expect(session.getMessages()).rejects.toThrow()
+ })
+ })
describe('getMetadata', () => {
it('should return session metadata', async () => {
const mockSession = {
id: 'test-session',
metadata: { name: 'Test Session', active: true },
- };
- mockClient.workspaces.sessions.getOrCreate.mockResolvedValue(mockSession);
+ }
+ mockClient.workspaces.sessions.getOrCreate.mockResolvedValue(mockSession)
- const metadata = await session.getMetadata();
+ const metadata = await session.getMetadata()
- expect(metadata).toEqual({ name: 'Test Session', active: true });
+ expect(metadata).toEqual({ name: 'Test Session', active: true })
expect(mockClient.workspaces.sessions.getOrCreate).toHaveBeenCalledWith(
'test-workspace',
{ id: 'test-session' }
- );
- });
+ )
+ })
it('should return empty object when no metadata exists', async () => {
const mockSession = {
id: 'test-session',
metadata: null,
- };
- mockClient.workspaces.sessions.getOrCreate.mockResolvedValue(mockSession);
+ }
+ mockClient.workspaces.sessions.getOrCreate.mockResolvedValue(mockSession)
- const metadata = await session.getMetadata();
+ const metadata = await session.getMetadata()
- expect(metadata).toEqual({});
- });
+ expect(metadata).toEqual({})
+ })
it('should handle API errors', async () => {
- mockClient.workspaces.sessions.getOrCreate.mockRejectedValue(new Error('Session not found'));
+ mockClient.workspaces.sessions.getOrCreate.mockRejectedValue(
+ new Error('Session not found')
+ )
- await expect(session.getMetadata()).rejects.toThrow();
- });
- });
+ await expect(session.getMetadata()).rejects.toThrow()
+ })
+ })
describe('setMetadata', () => {
it('should update session metadata', async () => {
- const metadata = { name: 'Updated Session', status: 'active' };
- mockClient.workspaces.sessions.update.mockResolvedValue({});
+ const metadata = { name: 'Updated Session', status: 'active' }
+ mockClient.workspaces.sessions.update.mockResolvedValue({})
- await session.setMetadata(metadata);
+ await session.setMetadata(metadata)
expect(mockClient.workspaces.sessions.update).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{ metadata }
- );
- });
+ )
+ })
it('should handle empty metadata', async () => {
- mockClient.workspaces.sessions.update.mockResolvedValue({});
+ mockClient.workspaces.sessions.update.mockResolvedValue({})
- await session.setMetadata({});
+ await session.setMetadata({})
expect(mockClient.workspaces.sessions.update).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{ metadata: {} }
- );
- });
+ )
+ })
it('should handle API errors', async () => {
- mockClient.workspaces.sessions.update.mockRejectedValue(new Error('Update failed'));
+ mockClient.workspaces.sessions.update.mockRejectedValue(
+ new Error('Update failed')
+ )
- await expect(session.setMetadata({ key: 'value' })).rejects.toThrow();
- });
- });
+ await expect(session.setMetadata({ key: 'value' })).rejects.toThrow()
+ })
+ })
describe('getContext', () => {
it('should get session context without options', async () => {
@@ -688,23 +713,23 @@ describe('Session', () => {
message_id: 10,
summary_type: 'short',
created_at: '2024-01-01T00:00:00Z',
- token_count: 100
+ token_count: 100,
},
- };
- mockClient.workspaces.sessions.getContext.mockResolvedValue(mockContext);
+ }
+ mockClient.workspaces.sessions.getContext.mockResolvedValue(mockContext)
- const context = await session.getContext();
+ const context = await session.getContext()
- expect(context).toBeInstanceOf(SessionContext);
- expect(context.sessionId).toBe('test-session');
- expect(context.messages).toEqual(mockContext.messages);
- expect(context.summary?.content).toBe('Conversation summary');
+ expect(context).toBeInstanceOf(SessionContext)
+ expect(context.sessionId).toBe('test-session')
+ expect(context.messages).toEqual(mockContext.messages)
+ expect(context.summary?.content).toBe('Conversation summary')
expect(mockClient.workspaces.sessions.getContext).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{ tokens: undefined, summary: undefined }
- );
- });
+ )
+ })
it('should get session context with options', async () => {
const mockContext = {
@@ -714,102 +739,111 @@ describe('Session', () => {
message_id: 5,
summary_type: 'short',
created_at: '2024-01-01T00:00:00Z',
- token_count: 50
+ token_count: 50,
},
- };
- mockClient.workspaces.sessions.getContext.mockResolvedValue(mockContext);
+ }
+ mockClient.workspaces.sessions.getContext.mockResolvedValue(mockContext)
- const context = await session.getContext({ summary: true, tokens: 1000 });
+ const context = await session.getContext({ summary: true, tokens: 1000 })
- expect(context).toBeInstanceOf(SessionContext);
+ expect(context).toBeInstanceOf(SessionContext)
expect(mockClient.workspaces.sessions.getContext).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{ tokens: 1000, summary: true }
- );
- });
+ )
+ })
it('should handle context without summary', async () => {
const mockContext = {
messages: [{ id: 'msg1', content: 'Hello', peer_id: 'peer1' }],
- };
- mockClient.workspaces.sessions.getContext.mockResolvedValue(mockContext);
+ }
+ mockClient.workspaces.sessions.getContext.mockResolvedValue(mockContext)
- const context = await session.getContext();
+ const context = await session.getContext()
- expect(context.summary).toBeNull();
- });
+ expect(context.summary).toBeNull()
+ })
it('should handle API errors', async () => {
- mockClient.workspaces.sessions.getContext.mockRejectedValue(new Error('Failed to get context'));
+ mockClient.workspaces.sessions.getContext.mockRejectedValue(
+ new Error('Failed to get context')
+ )
- await expect(session.getContext()).rejects.toThrow();
- });
- });
+ await expect(session.getContext()).rejects.toThrow()
+ })
+ })
describe('search', () => {
it('should search session messages and return Page', async () => {
const mockSearchResults = [
{ id: 'msg1', content: 'Hello world', peer_id: 'peer1' },
{ id: 'msg2', content: 'Hello there', peer_id: 'peer2' },
- ];
- mockClient.workspaces.sessions.search.mockResolvedValue(mockSearchResults);
+ ]
+ mockClient.workspaces.sessions.search.mockResolvedValue(mockSearchResults)
- const results = await session.search('hello');
+ const results = await session.search('hello')
- expect(Array.isArray(results)).toBe(true);
+ expect(Array.isArray(results)).toBe(true)
expect(mockClient.workspaces.sessions.search).toHaveBeenCalledWith(
'test-workspace',
'test-session',
{ query: 'hello', limit: undefined }
- );
- });
+ )
+ })
it('should handle empty search results', async () => {
- const mockSearchResults: any[] = [];
- mockClient.workspaces.sessions.search.mockResolvedValue(mockSearchResults);
+ const mockSearchResults: any[] = []
+ mockClient.workspaces.sessions.search.mockResolvedValue(mockSearchResults)
- const results = await session.search('nonexistent');
+ const results = await session.search('nonexistent')
- expect(Array.isArray(results)).toBe(true);
- });
+ expect(Array.isArray(results)).toBe(true)
+ })
it('should throw error for empty query', async () => {
- await expect(session.search('')).rejects.toThrow();
- await expect(session.search(' ')).rejects.toThrow();
- });
+ await expect(session.search('')).rejects.toThrow()
+ await expect(session.search(' ')).rejects.toThrow()
+ })
it('should throw error for non-string query', async () => {
- await expect(session.search(null as any)).rejects.toThrow();
- await expect(session.search(undefined as any)).rejects.toThrow();
- await expect(session.search(123 as any)).rejects.toThrow();
- });
+ await expect(session.search(null as any)).rejects.toThrow()
+ await expect(session.search(undefined as any)).rejects.toThrow()
+ await expect(session.search(123 as any)).rejects.toThrow()
+ })
it('should handle API errors', async () => {
- mockClient.workspaces.sessions.search.mockRejectedValue(new Error('Search failed'));
+ mockClient.workspaces.sessions.search.mockRejectedValue(
+ new Error('Search failed')
+ )
- await expect(session.search('test')).rejects.toThrow();
- });
- });
+ await expect(session.search('test')).rejects.toThrow()
+ })
+ })
describe('uploadFile', () => {
it('should upload file and return messages', async () => {
- const mockFile = new File(['test content'], 'test.txt', { type: 'text/plain' });
+ const mockFile = new File(['test content'], 'test.txt', {
+ type: 'text/plain',
+ })
const mockMessages = [
- { id: 'msg1', content: 'test content', peer_id: 'peer1' }
- ];
- mockClient.workspaces.sessions.messages.upload.mockResolvedValue(mockMessages);
+ { id: 'msg1', content: 'test content', peer_id: 'peer1' },
+ ]
+ mockClient.workspaces.sessions.messages.upload.mockResolvedValue(
+ mockMessages
+ )
- const messages = await session.uploadFile(mockFile, 'peer1');
+ const messages = await session.uploadFile(mockFile, 'peer1')
- expect(messages).toEqual(mockMessages);
- expect(mockClient.workspaces.sessions.messages.upload).toHaveBeenCalledWith(
- 'test-workspace',
- 'test-session',
- { file: mockFile, peer_id: 'peer1' }
- );
- });
- });
+ expect(messages).toEqual(mockMessages)
+ expect(
+ mockClient.workspaces.sessions.messages.upload
+ ).toHaveBeenCalledWith('test-workspace', 'test-session', {
+ file: mockFile,
+ peer_id: 'peer1',
+ })
+ })
+ })
describe('workingRep', () => {
it('should get working representation with peer string', async () => {
@@ -817,77 +851,212 @@ describe('Session', () => {
peer_id: 'peer1',
knowledge: 'Some knowledge about the peer',
relationships: ['peer2', 'peer3'],
- };
- mockClient.workspaces.peers.workingRepresentation.mockResolvedValue(mockRepresentation);
+ }
+ mockClient.workspaces.peers.workingRepresentation.mockResolvedValue(
+ mockRepresentation
+ )
- const result = await session.workingRep('peer1');
+ const result = await session.workingRep('peer1')
- expect(result).toEqual(mockRepresentation);
- expect(mockClient.workspaces.peers.workingRepresentation).toHaveBeenCalledWith(
- 'test-workspace',
- 'peer1',
- { session_id: 'test-session', target: undefined }
- );
- });
+ expect(result).toEqual(mockRepresentation)
+ expect(
+ mockClient.workspaces.peers.workingRepresentation
+ ).toHaveBeenCalledWith('test-workspace', 'peer1', {
+ session_id: 'test-session',
+ target: undefined,
+ })
+ })
it('should get working representation with Peer object', async () => {
- const peer = new Peer('peer1', 'test-workspace', mockClient);
+ const peer = new Peer('peer1', 'test-workspace', mockClient)
const mockRepresentation = {
peer_id: 'peer1',
knowledge: 'Some knowledge',
- };
- mockClient.workspaces.peers.workingRepresentation.mockResolvedValue(mockRepresentation);
+ }
+ mockClient.workspaces.peers.workingRepresentation.mockResolvedValue(
+ mockRepresentation
+ )
- const result = await session.workingRep(peer);
+ const result = await session.workingRep(peer)
- expect(result).toEqual(mockRepresentation);
- expect(mockClient.workspaces.peers.workingRepresentation).toHaveBeenCalledWith(
- 'test-workspace',
- 'peer1',
- { session_id: 'test-session', target: undefined }
- );
- });
+ expect(result).toEqual(mockRepresentation)
+ expect(
+ mockClient.workspaces.peers.workingRepresentation
+ ).toHaveBeenCalledWith('test-workspace', 'peer1', {
+ session_id: 'test-session',
+ target: undefined,
+ })
+ })
it('should get working representation with target peer string', async () => {
const mockRepresentation = {
peer_id: 'peer1',
target_knowledge: 'What peer1 knows about target',
- };
- mockClient.workspaces.peers.workingRepresentation.mockResolvedValue(mockRepresentation);
+ }
+ mockClient.workspaces.peers.workingRepresentation.mockResolvedValue(
+ mockRepresentation
+ )
- const result = await session.workingRep('peer1', 'target-peer');
+ const result = await session.workingRep('peer1', 'target-peer')
- expect(result).toEqual(mockRepresentation);
- expect(mockClient.workspaces.peers.workingRepresentation).toHaveBeenCalledWith(
- 'test-workspace',
- 'peer1',
- { session_id: 'test-session', target: 'target-peer' }
- );
- });
+ expect(result).toEqual(mockRepresentation)
+ expect(
+ mockClient.workspaces.peers.workingRepresentation
+ ).toHaveBeenCalledWith('test-workspace', 'peer1', {
+ session_id: 'test-session',
+ target: 'target-peer',
+ })
+ })
it('should get working representation with target Peer object', async () => {
- const peer = new Peer('peer1', 'test-workspace', mockClient);
- const target = new Peer('target-peer', 'test-workspace', mockClient);
+ const peer = new Peer('peer1', 'test-workspace', mockClient)
+ const target = new Peer('target-peer', 'test-workspace', mockClient)
const mockRepresentation = {
peer_id: 'peer1',
target_knowledge: 'What peer1 knows about target',
- };
- mockClient.workspaces.peers.workingRepresentation.mockResolvedValue(mockRepresentation);
+ }
+ mockClient.workspaces.peers.workingRepresentation.mockResolvedValue(
+ mockRepresentation
+ )
- const result = await session.workingRep(peer, target);
+ const result = await session.workingRep(peer, target)
- expect(result).toEqual(mockRepresentation);
- expect(mockClient.workspaces.peers.workingRepresentation).toHaveBeenCalledWith(
- 'test-workspace',
- 'peer1',
- { session_id: 'test-session', target: 'target-peer' }
- );
- });
+ expect(result).toEqual(mockRepresentation)
+ expect(
+ mockClient.workspaces.peers.workingRepresentation
+ ).toHaveBeenCalledWith('test-workspace', 'peer1', {
+ session_id: 'test-session',
+ target: 'target-peer',
+ })
+ })
it('should handle API errors', async () => {
- mockClient.workspaces.peers.workingRepresentation.mockRejectedValue(new Error('Failed to get working representation'));
+ mockClient.workspaces.peers.workingRepresentation.mockRejectedValue(
+ new Error('Failed to get working representation')
+ )
- await expect(session.workingRep('peer1')).rejects.toThrow();
- });
- });
-});
+ await expect(session.workingRep('peer1')).rejects.toThrow()
+ })
+ })
+ describe('delete', () => {
+ it('should delete the session', async () => {
+ mockClient.workspaces.sessions.delete.mockResolvedValue({})
+
+ await session.delete()
+
+ expect(mockClient.workspaces.sessions.delete).toHaveBeenCalledWith(
+ 'test-workspace',
+ 'test-session'
+ )
+ })
+
+ it('should handle API errors', async () => {
+ mockClient.workspaces.sessions.delete.mockRejectedValue(
+ new Error('Failed to delete session')
+ )
+
+ await expect(session.delete()).rejects.toThrow('Failed to delete session')
+ })
+ })
+
+ describe('getDeriverStatus', () => {
+ it('should return deriver status without options', async () => {
+ const mockStatus = {
+ total_work_units: 10,
+ completed_work_units: 5,
+ in_progress_work_units: 3,
+ pending_work_units: 2,
+ sessions: { session1: { status: 'active' } },
+ }
+ mockClient.workspaces.deriverStatus.mockResolvedValue(mockStatus)
+
+ const status = await session.getDeriverStatus()
+
+ expect(status).toEqual({
+ totalWorkUnits: 10,
+ completedWorkUnits: 5,
+ inProgressWorkUnits: 3,
+ pendingWorkUnits: 2,
+ sessions: { session1: { status: 'active' } },
+ })
+ expect(mockClient.workspaces.deriverStatus).toHaveBeenCalledWith(
+ 'test-workspace',
+ { session_id: 'test-session' }
+ )
+ })
+
+ it('should return deriver status with options', async () => {
+ const mockStatus = {
+ total_work_units: 5,
+ completed_work_units: 3,
+ in_progress_work_units: 1,
+ pending_work_units: 1,
+ }
+ mockClient.workspaces.deriverStatus.mockResolvedValue(mockStatus)
+
+ const status = await session.getDeriverStatus({
+ observerId: 'observer1',
+ senderId: 'sender1',
+ })
+
+ expect(status).toEqual({
+ totalWorkUnits: 5,
+ completedWorkUnits: 3,
+ inProgressWorkUnits: 1,
+ pendingWorkUnits: 1,
+ sessions: undefined,
+ })
+ expect(mockClient.workspaces.deriverStatus).toHaveBeenCalledWith(
+ 'test-workspace',
+ {
+ observer_id: 'observer1',
+ sender_id: 'sender1',
+ session_id: 'test-session',
+ }
+ )
+ })
+
+ describe('pollDeriverStatus', () => {
+ it('should poll until processing is complete', async () => {
+ const mockStatusComplete = {
+ total_work_units: 5,
+ completed_work_units: 5,
+ in_progress_work_units: 0,
+ pending_work_units: 0,
+ }
+ mockClient.workspaces.deriverStatus.mockResolvedValue(
+ mockStatusComplete
+ )
+
+ const status = await session.pollDeriverStatus()
+
+ expect(status).toEqual({
+ totalWorkUnits: 5,
+ completedWorkUnits: 5,
+ inProgressWorkUnits: 0,
+ pendingWorkUnits: 0,
+ sessions: undefined,
+ })
+
+ expect(mockClient.workspaces.deriverStatus).toHaveBeenCalledWith(
+ 'test-workspace',
+ { session_id: 'test-session' }
+ )
+ })
+
+ it('should timeout if processing takes too long', async () => {
+ const mockStatusPending = {
+ total_work_units: 5,
+ completed_work_units: 2,
+ in_progress_work_units: 2,
+ pending_work_units: 1,
+ }
+ mockClient.workspaces.deriverStatus.mockResolvedValue(mockStatusPending)
+
+ await expect(
+ session.pollDeriverStatus({ timeoutMs: 100 })
+ ).rejects.toThrow()
+ })
+ })
+ })
+})
diff --git a/sdks/typescript/bun.lock b/sdks/typescript/bun.lock
index c6ea15db..7ec73bd1 100644
--- a/sdks/typescript/bun.lock
+++ b/sdks/typescript/bun.lock
@@ -4,7 +4,7 @@
"": {
"name": "@honcho-ai/sdk",
"dependencies": {
- "@honcho-ai/core": "^1.4.0",
+ "@honcho-ai/core": "^1.4.1-alpha.0",
"@types/node": "^24.0.1",
"zod": "4.0.0",
},
@@ -18,15 +18,13 @@
},
},
"packages": {
- "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
-
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
- "@babel/compat-data": ["@babel/compat-data@7.28.0", "", {}, "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw=="],
+ "@babel/compat-data": ["@babel/compat-data@7.28.4", "", {}, "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw=="],
- "@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
+ "@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="],
- "@babel/generator": ["@babel/generator@7.28.0", "", { "dependencies": { "@babel/parser": "^7.28.0", "@babel/types": "^7.28.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg=="],
+ "@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
@@ -34,7 +32,7 @@
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
- "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.27.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg=="],
+ "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
@@ -44,9 +42,9 @@
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
- "@babel/helpers": ["@babel/helpers@7.28.2", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.2" } }, "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw=="],
+ "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
- "@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="],
+ "@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
"@babel/plugin-syntax-async-generators": ["@babel/plugin-syntax-async-generators@7.8.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw=="],
@@ -84,31 +82,33 @@
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
- "@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="],
+ "@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
- "@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
+ "@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="],
- "@biomejs/biome": ["@biomejs/biome@2.1.3", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.1.3", "@biomejs/cli-darwin-x64": "2.1.3", "@biomejs/cli-linux-arm64": "2.1.3", "@biomejs/cli-linux-arm64-musl": "2.1.3", "@biomejs/cli-linux-x64": "2.1.3", "@biomejs/cli-linux-x64-musl": "2.1.3", "@biomejs/cli-win32-arm64": "2.1.3", "@biomejs/cli-win32-x64": "2.1.3" }, "bin": { "biome": "bin/biome" } }, "sha512-KE/tegvJIxTkl7gJbGWSgun7G6X/n2M6C35COT6ctYrAy7SiPyNvi6JtoQERVK/VRbttZfgGq96j2bFmhmnH4w=="],
+ "@biomejs/biome": ["@biomejs/biome@2.2.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.4", "@biomejs/cli-darwin-x64": "2.2.4", "@biomejs/cli-linux-arm64": "2.2.4", "@biomejs/cli-linux-arm64-musl": "2.2.4", "@biomejs/cli-linux-x64": "2.2.4", "@biomejs/cli-linux-x64-musl": "2.2.4", "@biomejs/cli-win32-arm64": "2.2.4", "@biomejs/cli-win32-x64": "2.2.4" }, "bin": { "biome": "bin/biome" } }, "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg=="],
- "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.1.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LFLkSWRoSGS1wVUD/BE6Nlt2dSn0ulH3XImzg2O/36BoToJHKXjSxzPEMAqT9QvwVtk7/9AQhZpTneERU9qaXA=="],
+ "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA=="],
- "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.1.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Q/4OTw8P9No9QeowyxswcWdm0n2MsdCwWcc5NcKQQvzwPjwuPdf8dpPPf4r+x0RWKBtl1FLiAUtJvBlri6DnYw=="],
+ "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg=="],
- "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-2hS6LgylRqMFmAZCOFwYrf77QMdUwJp49oe8PX/O8+P2yKZMSpyQTf3Eo5ewnsMFUEmYbPOskafdV1ds1MZMJA=="],
+ "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw=="],
- "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-KXouFSBnoxAWZYDQrnNRzZBbt5s9UJkIm40hdvSL9mBxSSoxRFQJbtg1hP3aa8A2SnXyQHxQfpiVeJlczZt76w=="],
+ "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ=="],
- "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-NxlSCBhLvQtWGagEztfAZ4WcE1AkMTntZV65ZvR+J9jp06+EtOYEBPQndA70ZGhHbEDG57bR6uNvqkd1WrEYVA=="],
+ "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ=="],
- "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-KaLAxnROouzIWtl6a0Y88r/4hW5oDUJTIqQorOTVQITaKQsKjZX4XCUmHIhdEk8zMnaiLZzRTAwk1yIAl+mIew=="],
+ "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg=="],
- "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.1.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-V9CUZCtWH4u0YwyCYbQ3W5F4ZGPWp2C2TYcsiWFNNyRfmOW1j/TY/jAurl33SaRjgZPO5UUhGyr9m6BN9t84NQ=="],
+ "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ=="],
- "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.1.3", "", { "os": "win32", "cpu": "x64" }, "sha512-dxy599q6lgp8ANPpR8sDMscwdp9oOumEsVXuVCVT9N2vAho8uYXlCz53JhxX6LtJOXaE73qzgkGQ7QqvFlMC0g=="],
+ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg=="],
- "@honcho-ai/core": ["@honcho-ai/core@1.4.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-pHyUgkiIlf+pMfZJx7NmJ1UH8yOKWZ+vmPRummg3olgstMXApcBrW0bhJTPjHWoGGA+OFYcOPxJBoZKY6VCtMw=="],
+ "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
+
+ "@honcho-ai/core": ["@honcho-ai/core@1.4.1-alpha.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-cRp3ug8cBe9E7NZDgCXkI0EHIgm8x5Jq/nQB0xA5629GuniS99yq6M786I//FjOZhB2YEZoOxd4Zkg412uk5Yg=="],
"@istanbuljs/load-nyc-config": ["@istanbuljs/load-nyc-config@1.1.0", "", { "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" } }, "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ=="],
@@ -142,13 +142,15 @@
"@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="],
- "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
+
+ "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
- "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
- "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="],
@@ -156,6 +158,40 @@
"@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="],
+ "@swc/core": ["@swc/core@1.13.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.24" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.13.5", "@swc/core-darwin-x64": "1.13.5", "@swc/core-linux-arm-gnueabihf": "1.13.5", "@swc/core-linux-arm64-gnu": "1.13.5", "@swc/core-linux-arm64-musl": "1.13.5", "@swc/core-linux-x64-gnu": "1.13.5", "@swc/core-linux-x64-musl": "1.13.5", "@swc/core-win32-arm64-msvc": "1.13.5", "@swc/core-win32-ia32-msvc": "1.13.5", "@swc/core-win32-x64-msvc": "1.13.5" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ=="],
+
+ "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.13.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ=="],
+
+ "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.13.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng=="],
+
+ "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.13.5", "", { "os": "linux", "cpu": "arm" }, "sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ=="],
+
+ "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.13.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw=="],
+
+ "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.13.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ=="],
+
+ "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.13.5", "", { "os": "linux", "cpu": "x64" }, "sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA=="],
+
+ "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.13.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q=="],
+
+ "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.13.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw=="],
+
+ "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.13.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw=="],
+
+ "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.13.5", "", { "os": "win32", "cpu": "x64" }, "sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q=="],
+
+ "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
+
+ "@swc/types": ["@swc/types@0.1.25", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g=="],
+
+ "@tsconfig/node10": ["@tsconfig/node10@1.0.11", "", {}, "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="],
+
+ "@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="],
+
+ "@tsconfig/node14": ["@tsconfig/node14@1.0.3", "", {}, "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="],
+
+ "@tsconfig/node16": ["@tsconfig/node16@1.0.4", "", {}, "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="],
+
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
@@ -174,7 +210,7 @@
"@types/jest": ["@types/jest@29.5.14", "", { "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ=="],
- "@types/node": ["@types/node@24.2.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw=="],
+ "@types/node": ["@types/node@24.6.1", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-ljvjjs3DNXummeIaooB4cLBKg2U6SPI6Hjra/9rRIy7CpM0HpLtG9HptkMKAb4HYWy5S7HUvJEuWgr/y0U8SHw=="],
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
@@ -186,6 +222,10 @@
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
+ "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
+
+ "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="],
+
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
@@ -196,6 +236,8 @@
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
+ "arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="],
+
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
@@ -212,11 +254,13 @@
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+ "baseline-browser-mapping": ["baseline-browser-mapping@2.8.9", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA=="],
+
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
- "browserslist": ["browserslist@4.25.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="],
+ "browserslist": ["browserslist@4.26.2", "", { "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", "electron-to-chromium": "^1.5.218", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A=="],
"bs-logger": ["bs-logger@0.2.6", "", { "dependencies": { "fast-json-stable-stringify": "2.x" } }, "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog=="],
@@ -230,7 +274,7 @@
"camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="],
- "caniuse-lite": ["caniuse-lite@1.0.30001731", "", {}, "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg=="],
+ "caniuse-lite": ["caniuse-lite@1.0.30001746", "", {}, "sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -258,11 +302,13 @@
"create-jest": ["create-jest@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-config": "^29.7.0", "jest-util": "^29.7.0", "prompts": "^2.0.1" }, "bin": { "create-jest": "bin/create-jest.js" } }, "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q=="],
+ "create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="],
+
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
- "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
- "dedent": ["dedent@1.6.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA=="],
+ "dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="],
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
@@ -270,17 +316,19 @@
"detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="],
+ "diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="],
+
"diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
- "electron-to-chromium": ["electron-to-chromium@1.5.197", "", {}, "sha512-m1xWB3g7vJ6asIFz+2pBUbq3uGmfmln1M9SSvBe4QIFWYrRHylP73zL/3nMjDmwz8V+1xAXQDfBd6+HPW0WvDQ=="],
+ "electron-to-chromium": ["electron-to-chromium@1.5.228", "", {}, "sha512-nxkiyuqAn4MJ1QbobwqJILiDtu/jk14hEAWaMiJmNPh1Z+jqoFlBFZjdXwLWGeVSeu9hGLg6+2G9yJaW8rBIFA=="],
"emittery": ["emittery@0.13.1", "", {}, "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
- "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="],
+ "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
@@ -388,7 +436,7 @@
"istanbul-lib-source-maps": ["istanbul-lib-source-maps@4.0.1", "", { "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", "source-map": "^0.6.1" } }, "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw=="],
- "istanbul-reports": ["istanbul-reports@3.1.7", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g=="],
+ "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="],
"jest": ["jest@29.7.0", "", { "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", "import-local": "^3.0.2", "jest-cli": "^29.7.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"], "bin": { "jest": "bin/jest.js" } }, "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw=="],
@@ -498,7 +546,7 @@
"node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="],
- "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
+ "node-releases": ["node-releases@2.0.21", "", {}, "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
@@ -594,20 +642,24 @@
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
- "ts-jest": ["ts-jest@29.4.1", "", { "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", "handlebars": "^4.7.8", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", "semver": "^7.7.2", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@jest/transform": "^29.0.0 || ^30.0.0", "@jest/types": "^29.0.0 || ^30.0.0", "babel-jest": "^29.0.0 || ^30.0.0", "jest": "^29.0.0 || ^30.0.0", "jest-util": "^29.0.0 || ^30.0.0", "typescript": ">=4.3 <6" }, "optionalPeers": ["@babel/core", "@jest/transform", "@jest/types", "babel-jest", "jest-util"], "bin": { "ts-jest": "cli.js" } }, "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw=="],
+ "ts-jest": ["ts-jest@29.4.4", "", { "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", "handlebars": "^4.7.8", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", "semver": "^7.7.2", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@jest/transform": "^29.0.0 || ^30.0.0", "@jest/types": "^29.0.0 || ^30.0.0", "babel-jest": "^29.0.0 || ^30.0.0", "jest": "^29.0.0 || ^30.0.0", "jest-util": "^29.0.0 || ^30.0.0", "typescript": ">=4.3 <6" }, "optionalPeers": ["@babel/core", "@jest/transform", "@jest/types", "babel-jest", "jest-util"], "bin": { "ts-jest": "cli.js" } }, "sha512-ccVcRABct5ZELCT5U0+DZwkXMCcOCLi2doHRrKy1nK/s7J7bch6TzJMsrY09WxgUUIP/ITfmcDS8D2yl63rnXw=="],
+
+ "ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-script": "dist/bin-script-deprecated.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="],
"type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
- "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="],
- "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
+ "undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="],
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
+ "v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="],
+
"v8-to-istanbul": ["v8-to-istanbul@9.3.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^2.0.0" } }, "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA=="],
"walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="],
@@ -636,6 +688,8 @@
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
+ "yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="],
+
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"zod": ["zod@4.0.0", "", {}, "sha512-9diLdTPc/L7w/5jI4C3gHYNiGHDV9IZYxo1e5LSD8cabi65WVTWWb+g2BGPEpUUCOxR4D+6O5B0AzyMdUAXwrw=="],
@@ -644,7 +698,9 @@
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
- "@honcho-ai/core/@types/node": ["@types/node@18.19.122", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-yzegtT82dwTNEe/9y+CM8cgb42WrUfMMCg2QqSddzO1J6uPmBD7qKCZ7dOHZP2Yrpm/kb0eqdNMn2MUyEiqBmA=="],
+ "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
+
+ "@honcho-ai/core/@types/node": ["@types/node@18.19.129", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-hrmi5jWt2w60ayox3iIXwpMEnfUvOLJCRtrOPbHtH15nTjvO7uhnelvrdAs0dO0/zl5DZ3ZbahiaXEVb54ca/A=="],
"@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json
index 016d8bc6..1e9750d3 100644
--- a/sdks/typescript/package.json
+++ b/sdks/typescript/package.json
@@ -1,6 +1,6 @@
{
"name": "@honcho-ai/sdk",
- "version": "1.4.0",
+ "version": "1.4.1",
"description": "Official DX Optimized TypeScript SDK for Honcho",
"author": "Plastic Labs ",
"license": "Apache-2.0",
@@ -20,7 +20,7 @@
"test:coverage": "jest --coverage"
},
"dependencies": {
- "@honcho-ai/core": "^1.4.0",
+ "@honcho-ai/core": "^1.4.1-alpha.0",
"@types/node": "^24.0.1",
"zod": "4.0.0"
},
diff --git a/sdks/typescript/src/client.ts b/sdks/typescript/src/client.ts
index 058eade6..0400381c 100644
--- a/sdks/typescript/src/client.ts
+++ b/sdks/typescript/src/client.ts
@@ -1,10 +1,10 @@
import HonchoCore from '@honcho-ai/core'
-import type { DefaultQuery } from '@honcho-ai/core/src/core'
-import type { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
+import type { DefaultQuery } from '@honcho-ai/core/core'
+import type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages'
import type {
DeriverStatus,
WorkspaceDeriverStatusParams,
-} from '@honcho-ai/core/src/resources/workspaces/workspaces'
+} from '@honcho-ai/core/resources/workspaces/workspaces'
import { Page } from './pagination'
import { Peer } from './peer'
import { Session } from './session'
@@ -16,6 +16,7 @@ import {
type HonchoConfig,
HonchoConfigSchema,
LimitSchema,
+ MessageMetadataSchema,
type PeerConfig,
PeerConfigSchema,
PeerIdSchema,
@@ -449,6 +450,47 @@ export class Honcho {
}
}
+ /**
+ * Update the metadata of a message.
+ *
+ * Makes an API call to update the metadata of a specific message within a session.
+ *
+ * @param message - Either a Message object or a message ID string
+ * @param metadata - The metadata to update for the message
+ * @param sessionId - The ID of the session (required if message is a string ID, ignored if message is a Message object)
+ * @returns Promise resolving to the updated Message object
+ * @throws Error if message is a string ID but sessionId is not provided
+ */
+ async updateMessage(
+ message: Message | string,
+ metadata: Record,
+ sessionId?: string
+ ): Promise {
+ const validatedMetadata = MessageMetadataSchema.parse(metadata)
+ let messageId: string
+ let resolvedSessionId: string
+
+ if (typeof message === 'string') {
+ messageId = message
+ if (!sessionId) {
+ throw new Error('sessionId is required when message is a string ID')
+ }
+ resolvedSessionId = sessionId
+ } else {
+ messageId = message.id
+ resolvedSessionId = message.session_id
+ }
+
+ return await this._client.workspaces.sessions.messages.update(
+ this.workspaceId,
+ resolvedSessionId,
+ messageId,
+ {
+ metadata: validatedMetadata,
+ }
+ )
+ }
+
/**
* Return a string representation of the Honcho client.
*
diff --git a/sdks/typescript/src/index.ts b/sdks/typescript/src/index.ts
index 3aed433b..bc2b5906 100644
--- a/sdks/typescript/src/index.ts
+++ b/sdks/typescript/src/index.ts
@@ -1,7 +1,7 @@
// Main entry point for the Honcho TypeScript SDK
// Exports all main classes and types
-export { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
+export type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages'
export { Honcho } from './client'
export { Page } from './pagination'
export { Peer } from './peer'
@@ -12,6 +12,11 @@ export {
Summary,
SummaryData,
} from './session_context'
+export {
+ type DialecticStreamChunk,
+ type DialecticStreamDelta,
+ DialecticStreamResponse,
+} from './types'
// Export validation types for advanced usage
export type {
diff --git a/sdks/typescript/src/pagination.ts b/sdks/typescript/src/pagination.ts
index 63c78d2f..952d9831 100644
--- a/sdks/typescript/src/pagination.ts
+++ b/sdks/typescript/src/pagination.ts
@@ -1,80 +1,127 @@
+import type { Page as CorePage } from '@honcho-ai/core/pagination'
+
/**
* Generic paginated result wrapper for Honcho SDK.
- * Provides async iteration and transformation capabilities.
+ * Provides async iteration and transformation capabilities while preserving
+ * pagination functionality from the underlying core Page.
*/
-export class Page implements AsyncIterable {
- private _originalPage: any
- private _transformFunc?: (item: any) => T
+export class Page implements AsyncIterable {
+ private _originalPage: CorePage
+ private _transformFunc?: (item: TOriginal) => T
/**
* Initialize a new Page.
+ *
+ * @param originalPage - The original Page to wrap
+ * @param transformFunc - Optional function to transform objects from the original type to type T.
+ * If not provided, objects are passed through unchanged.
*/
- constructor(originalPage: any, transformFunc?: (item: any) => T) {
+ constructor(
+ originalPage: CorePage,
+ transformFunc?: (item: TOriginal) => T
+ ) {
this._originalPage = originalPage
this._transformFunc = transformFunc
}
/**
- * Async iterator for the page's items.
+ * Async iterator for all transformed items across all pages.
*/
async *[Symbol.asyncIterator](): AsyncIterator {
- // Handle different page structure formats
- const items = this._originalPage.items || this._originalPage.data || []
- for (const item of items) {
- yield this._transformFunc ? this._transformFunc(item) : item
+ for await (const item of this._originalPage) {
+ yield this._transformFunc
+ ? this._transformFunc(item)
+ : (item as unknown as T)
}
}
/**
- * Get an item by index.
+ * Get a transformed item by index on the current page.
*/
- async get(index: number): Promise {
- if (
- !this._originalPage?.get ||
- typeof this._originalPage.get !== 'function'
- ) {
- throw new Error('Original page does not support indexed access')
+ get(index: number): T {
+ const items = this._originalPage.items || []
+ if (index < 0 || index >= items.length) {
+ throw new RangeError(
+ `Index ${index} is out of bounds for page with ${items.length} items`
+ )
}
- const item = await this._originalPage.get(index)
- return this._transformFunc ? this._transformFunc(item) : item
+ const item = items[index]
+ return this._transformFunc
+ ? this._transformFunc(item)
+ : (item as unknown as T)
}
/**
- * Get the size of the page.
+ * Get the number of items on the current page.
*/
- get size(): number {
- return this._originalPage?.size
+ get length(): number {
+ const items = this._originalPage.items || []
+ return items.length
}
/**
- * Get the total number of items.
+ * Get all transformed items on the current page.
*/
- get total(): number {
+ get items(): T[] {
+ const items = this._originalPage.items || []
+ return this._transformFunc
+ ? items.map(this._transformFunc)
+ : (items as unknown as T[])
+ }
+
+ /**
+ * Get the total number of items across all pages.
+ */
+ get total(): number | undefined {
return this._originalPage?.total
}
/**
- * Get all data as a list.
+ * Get the current page number.
*/
- async data(): Promise {
- const items = this._originalPage.items || this._originalPage.data || []
- const data = typeof items === 'function' ? await items() : items
- return this._transformFunc ? data.map(this._transformFunc) : data
+ get page(): number | undefined {
+ return this._originalPage?.page
+ }
+
+ /**
+ * Get the page size.
+ */
+ get size(): number | undefined {
+ return this._originalPage?.size
+ }
+
+ /**
+ * Get the total number of pages.
+ */
+ get pages(): number | undefined {
+ return this._originalPage?.pages
}
/**
* Check if there's a next page.
*/
get hasNextPage(): boolean {
- return this._originalPage?.hasNextPage
+ const hasNext = this._originalPage.hasNextPage
+ if (typeof hasNext === 'function') {
+ return hasNext()
+ }
+ return hasNext || false
}
/**
- * Get the next page.
+ * Fetch the next page of results.
+ * Returns null if there are no more pages.
*/
- async nextPage(): Promise | null> {
- const nextPage = await this._originalPage.nextPage()
- if (!nextPage) return null
- return new Page(nextPage, this._transformFunc)
+ async getNextPage(): Promise | null> {
+ if (!this._originalPage.getNextPage) {
+ return null
+ }
+
+ const nextOriginalPage = await this._originalPage.getNextPage()
+ if (!nextOriginalPage) {
+ return null
+ }
+
+ return new Page(nextOriginalPage, this._transformFunc)
}
}
diff --git a/sdks/typescript/src/peer.ts b/sdks/typescript/src/peer.ts
index 360a98fa..465bedd2 100644
--- a/sdks/typescript/src/peer.ts
+++ b/sdks/typescript/src/peer.ts
@@ -1,7 +1,8 @@
import type HonchoCore from '@honcho-ai/core'
-import type { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
+import type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages'
import { Page } from './pagination'
import { Session } from './session'
+import { type DialecticStreamChunk, DialecticStreamResponse } from './types'
import {
ChatQuerySchema,
FilterSchema,
@@ -61,8 +62,9 @@ export class Peer {
* querying the peer's global representation
* @param sessionId - Optional session ID to scope the query to a specific session.
* If provided, only information from that session is considered
- * @returns Promise resolving to response string containing the answer to the query,
- * or null if no relevant information is available
+ * @returns Promise resolving to:
+ * - For non-streaming: response string or null if no relevant information
+ * - For streaming: DialecticStreamResponse that can be iterated over
*/
async chat(
query: string,
@@ -71,19 +73,98 @@ export class Peer {
target?: string | Peer
sessionId?: string
}
- ): Promise {
+ ): Promise {
const chatParams = ChatQuerySchema.parse({
query,
stream: options?.stream,
target: options?.target,
sessionId: options?.sessionId,
})
+
+ if (chatParams.stream) {
+ const body = {
+ query: chatParams.query,
+ stream: true,
+ target: chatParams.target
+ ? typeof chatParams.target === 'string'
+ ? chatParams.target
+ : chatParams.target.id
+ : undefined,
+ session_id: chatParams.sessionId,
+ }
+
+ const url = `${this._client.baseURL}/v2/workspaces/${this.workspaceId}/peers/${this.id}/chat`
+ const apiKey = this._client.apiKey
+
+ async function* streamResponse(): AsyncGenerator<
+ string,
+ void,
+ undefined
+ > {
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'text/event-stream',
+ // Include auth headers if present
+ ...(apiKey && {
+ Authorization: `Bearer ${apiKey}`,
+ }),
+ },
+ body: JSON.stringify(body),
+ })
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`)
+ }
+
+ if (!response.body) {
+ throw new Error('Response body is null')
+ }
+
+ const reader = response.body.getReader()
+ const decoder = new TextDecoder()
+ let buffer = ''
+
+ try {
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) break
+
+ buffer += decoder.decode(value, { stream: true })
+ const lines = buffer.split('\n')
+ buffer = lines.pop() || ''
+
+ for (const line of lines) {
+ if (line.startsWith('data: ')) {
+ const jsonStr = line.slice(6) // Remove "data: " prefix
+ try {
+ const chunkData: DialecticStreamChunk = JSON.parse(jsonStr)
+ if (chunkData.done) {
+ return
+ }
+ const content = chunkData.delta.content
+ if (content) {
+ yield content
+ }
+ } catch {}
+ }
+ }
+ }
+ } finally {
+ reader.releaseLock()
+ }
+ }
+
+ return new DialecticStreamResponse(streamResponse())
+ }
+
const response = await this._client.workspaces.peers.chat(
this.workspaceId,
this.id,
{
query: chatParams.query,
- stream: chatParams.stream,
+ stream: false,
target: chatParams.target
? typeof chatParams.target === 'string'
? chatParams.target
@@ -258,6 +339,50 @@ export class Peer {
)
}
+ /**
+ * Get the peer card for this peer.
+ *
+ * Makes an API call to retrieve the peer card, which contains a representation
+ * of what this peer knows. If a target is provided, returns this peer's local
+ * representation of the target peer.
+ *
+ * @param target - Optional target peer for local card. If provided, returns this
+ * peer's card of the target peer. Can be a Peer object or peer ID string.
+ * @returns Promise resolving to a string containing the peer card
+ */
+ async card(target?: string | Peer): Promise {
+ // Validate target parameter
+ if (
+ target !== undefined &&
+ typeof target !== 'string' &&
+ !(target instanceof Peer)
+ ) {
+ throw new TypeError(
+ `target must be string, Peer, or undefined, got ${typeof target}`
+ )
+ }
+
+ if (typeof target === 'string' && target.trim().length === 0) {
+ throw new Error('target string cannot be empty')
+ }
+
+ const response = await this._client.workspaces.peers.card(
+ this.workspaceId,
+ this.id,
+ {
+ target: target instanceof Peer ? target.id : target,
+ }
+ )
+
+ if (!response.peer_card) {
+ return ''
+ }
+
+ const items: string[] = response.peer_card
+
+ return items.join('\n')
+ }
+
/**
* Return a string representation of the Peer.
*
diff --git a/sdks/typescript/src/session.ts b/sdks/typescript/src/session.ts
index 7f39e09e..6bc3e16f 100644
--- a/sdks/typescript/src/session.ts
+++ b/sdks/typescript/src/session.ts
@@ -1,11 +1,17 @@
import type HonchoCore from '@honcho-ai/core'
-import type { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
-import type { Uploadable } from '@honcho-ai/core/src/uploads'
+import type {
+ DeriverStatus,
+ WorkspaceDeriverStatusParams,
+} from '@honcho-ai/core/resources/index'
+import type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages'
+import type { Uploadable } from '@honcho-ai/core/uploads'
import { Page } from './pagination'
import { Peer } from './peer'
import { SessionContext, SessionSummaries, Summary } from './session_context'
import {
ContextParamsSchema,
+ type DeriverStatusOptions,
+ DeriverStatusOptionsSchema,
FileUploadSchema,
FilterSchema,
type Filters,
@@ -427,6 +433,15 @@ export class Session {
})
}
+ /**
+ * Delete this session.
+ *
+ * Makes an API call to mark this session as inactive.
+ */
+ async delete(): Promise {
+ await this._client.workspaces.sessions.delete(this.workspaceId, this.id)
+ }
+
/**
* Get optimized context for this session within a token limit.
*
@@ -532,6 +547,112 @@ export class Session {
)
}
+ /**
+ * Get the deriver processing status for this session, optionally scoped to an observer or sender.
+ *
+ * Makes an API call to retrieve the current status of the deriver processing queue.
+ * The deriver is responsible for processing messages and updating peer representations.
+ * This method automatically scopes the status to this session.
+ *
+ * @param options - Configuration options for the status request
+ * @param options.observerId - Optional observer ID to scope the status to
+ * @param options.senderId - Optional sender ID to scope the status to
+ * @returns Promise resolving to the deriver status information including work unit counts
+ */
+ async getDeriverStatus(
+ options?: Omit
+ ): Promise<{
+ totalWorkUnits: number
+ completedWorkUnits: number
+ inProgressWorkUnits: number
+ pendingWorkUnits: number
+ sessions?: Record
+ }> {
+ const validatedOptions = options
+ ? DeriverStatusOptionsSchema.parse(options)
+ : undefined
+ const queryParams: WorkspaceDeriverStatusParams = {
+ session_id: this.id, // Always use this session's ID
+ }
+ if (validatedOptions?.observerId)
+ queryParams.observer_id = validatedOptions.observerId
+ if (validatedOptions?.senderId)
+ queryParams.sender_id = validatedOptions.senderId
+
+ const status = await this._client.workspaces.deriverStatus(
+ this.workspaceId,
+ queryParams
+ )
+
+ return {
+ totalWorkUnits: status.total_work_units,
+ completedWorkUnits: status.completed_work_units,
+ inProgressWorkUnits: status.in_progress_work_units,
+ pendingWorkUnits: status.pending_work_units,
+ sessions: status.sessions || undefined,
+ }
+ }
+
+ /**
+ * Poll getDeriverStatus until pending_work_units and in_progress_work_units are both 0.
+ * This allows you to guarantee that all messages have been processed by the deriver for
+ * use with the dialectic endpoint.
+ *
+ * The polling estimates sleep time by assuming each work unit takes 1 second.
+ *
+ * @param options - Configuration options for the status request
+ * @param options.observerId - Optional observer ID to scope the status to
+ * @param options.senderId - Optional sender ID to scope the status to
+ * @param options.timeoutMs - Optional timeout in milliseconds (default: 300000 - 5 minutes)
+ * @returns Promise resolving to the final deriver status when processing is complete
+ * @throws Error if timeout is exceeded before processing completes
+ */
+ async pollDeriverStatus(
+ options?: Omit
+ ): Promise<{
+ totalWorkUnits: number
+ completedWorkUnits: number
+ inProgressWorkUnits: number
+ pendingWorkUnits: number
+ sessions?: Record
+ }> {
+ const validatedOptions = options
+ ? DeriverStatusOptionsSchema.parse(options)
+ : undefined
+ const timeoutMs = validatedOptions?.timeoutMs ?? 300000 // Default to 5 minutes
+ const startTime = Date.now()
+
+ while (true) {
+ const status = await this.getDeriverStatus(validatedOptions)
+ if (status.pendingWorkUnits === 0 && status.inProgressWorkUnits === 0) {
+ return status
+ }
+
+ // Check if timeout has been exceeded
+ const elapsedTime = Date.now() - startTime
+ if (elapsedTime >= timeoutMs) {
+ throw new Error(
+ `Polling timeout exceeded after ${timeoutMs}ms. ` +
+ `Current status: ${status.pendingWorkUnits} pending, ${status.inProgressWorkUnits} in progress work units.`
+ )
+ }
+
+ // Sleep for the expected time to complete all current work units
+ // Assuming each pending and in-progress work unit takes 1 second
+ const totalWorkUnits =
+ status.pendingWorkUnits + status.inProgressWorkUnits
+ const sleepMs = Math.max(1000, totalWorkUnits * 1000) // Sleep at least 1 second
+
+ // Ensure we don't sleep past the timeout
+ const remainingTime = timeoutMs - elapsedTime
+ const actualSleepMs = Math.min(sleepMs, remainingTime)
+
+ if (actualSleepMs > 0) {
+ await new Promise((resolve) => setTimeout(resolve, actualSleepMs))
+ }
+ }
+ }
+
/**
* Upload a file to create messages in this session.
*
diff --git a/sdks/typescript/src/session_context.ts b/sdks/typescript/src/session_context.ts
index 80df6539..ecd9b8c1 100644
--- a/sdks/typescript/src/session_context.ts
+++ b/sdks/typescript/src/session_context.ts
@@ -1,4 +1,4 @@
-import type { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
+import type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages'
import type { Peer } from './peer'
export interface SummaryData {
diff --git a/sdks/typescript/src/types.ts b/sdks/typescript/src/types.ts
new file mode 100644
index 00000000..64a0a6f4
--- /dev/null
+++ b/sdks/typescript/src/types.ts
@@ -0,0 +1,86 @@
+/**
+ * Shared types for the Honcho TypeScript SDK.
+ */
+
+/**
+ * Delta object for streaming dialectic responses.
+ */
+export interface DialecticStreamDelta {
+ content?: string
+ // Future fields can be added here:
+ // premises?: string[]
+ // tokens?: number
+ // analytics?: Record
+}
+
+/**
+ * Chunk in a streaming dialectic response.
+ */
+export interface DialecticStreamChunk {
+ delta: DialecticStreamDelta
+ done: boolean
+}
+
+/**
+ * Iterator for streaming dialectic responses with utilities for accessing the final response.
+ *
+ * Similar to OpenAI and Anthropic streaming patterns, this allows you to:
+ * - Iterate over chunks as they arrive
+ * - Access the final accumulated response after streaming completes
+ *
+ * @example
+ * ```typescript
+ * const stream = await peer.chat("Hello", { stream: true })
+ *
+ * // Stream chunks
+ * for await (const chunk of stream) {
+ * process.stdout.write(chunk)
+ * }
+ *
+ * // Get final response object
+ * const final = stream.getFinalResponse()
+ * console.log(`\nFull content: ${final.content}`)
+ * ```
+ */
+export class DialecticStreamResponse implements AsyncIterable {
+ private iterator: AsyncIterator
+ private accumulatedContent: string[] = []
+ private _isComplete = false
+
+ constructor(iterator: AsyncIterator) {
+ this.iterator = iterator
+ }
+
+ [Symbol.asyncIterator](): AsyncIterator {
+ return {
+ next: async () => {
+ const result = await this.iterator.next()
+ if (result.done) {
+ this._isComplete = true
+ return { done: true, value: undefined }
+ }
+ this.accumulatedContent.push(result.value)
+ return { done: false, value: result.value }
+ },
+ }
+ }
+
+ /**
+ * Get the final accumulated response after streaming completes.
+ *
+ * @returns An object with the full content
+ *
+ * @note This should be called after the stream has been fully consumed.
+ * If called before completion, it returns the content accumulated so far.
+ */
+ getFinalResponse(): { content: string } {
+ return { content: this.accumulatedContent.join('') }
+ }
+
+ /**
+ * Check if the stream has finished.
+ */
+ get isComplete(): boolean {
+ return this._isComplete
+ }
+}
diff --git a/sdks/typescript/src/validation.ts b/sdks/typescript/src/validation.ts
index f8fae934..3c30943b 100644
--- a/sdks/typescript/src/validation.ts
+++ b/sdks/typescript/src/validation.ts
@@ -118,7 +118,7 @@ export const FilterSchema = z.record(z.string(), z.unknown()).optional()
*/
export const ChatQuerySchema = z.object({
query: SearchQuerySchema,
- stream: z.boolean().optional(),
+ stream: z.boolean().optional().default(false),
target: z.union([z.string(), z.object({ id: z.string() })]).optional(),
sessionId: z.string().optional(),
})
diff --git a/src/config.py b/src/config.py
index a8bed82e..a87ac9b5 100644
--- a/src/config.py
+++ b/src/config.py
@@ -188,7 +188,7 @@ class DeriverSettings(HonchoSettings):
STALE_SESSION_TIMEOUT_MINUTES: Annotated[int, Field(default=5, gt=0, le=1440)] = 5
PROVIDER: SupportedProviders = "google"
- MODEL: str = "gemini-2.5-flash"
+ MODEL: str = "gemini-2.5-flash-lite"
MAX_OUTPUT_TOKENS: Annotated[int, Field(default=2500, gt=0, le=100_000)] = 2500
# Thinking budget tokens are only applied when using Anthropic as provider
diff --git a/src/deriver/consumer.py b/src/deriver/consumer.py
index 0692db47..ae2e5442 100644
--- a/src/deriver/consumer.py
+++ b/src/deriver/consumer.py
@@ -1,6 +1,7 @@
import logging
from typing import Any
+import sentry_sdk
from langfuse import get_client
from pydantic import ValidationError
from rich.console import Console
@@ -51,19 +52,29 @@ async def process_item(task_type: str, queue_payload: dict[str, Any]) -> None:
queue_payload,
)
raise ValueError(f"Invalid payload structure: {str(e)}") from e
-
- if settings.LANGFUSE_PUBLIC_KEY:
- with lf.start_as_current_span(
- name="summary_processing",
- input={
- "workspace_name": validated.workspace_name,
- "session_name": validated.session_name,
- "message_id": validated.message_id,
- },
- metadata={
- "summary_model": settings.SUMMARY.MODEL,
- },
- ):
+ with sentry_sdk.start_transaction(name="process_summary_task", op="deriver"):
+ if settings.LANGFUSE_PUBLIC_KEY:
+ with lf.start_as_current_span(
+ name="summary_processing",
+ input={
+ "workspace_name": validated.workspace_name,
+ "session_name": validated.session_name,
+ "message_id": validated.message_id,
+ },
+ metadata={
+ "summary_model": settings.SUMMARY.MODEL,
+ },
+ ):
+ await summarizer.summarize_if_needed(
+ validated.workspace_name,
+ validated.session_name,
+ validated.message_id,
+ validated.message_seq_in_session,
+ )
+ log_performance_metrics(
+ f"summary_{validated.workspace_name}_{validated.message_id}"
+ )
+ else:
await summarizer.summarize_if_needed(
validated.workspace_name,
validated.session_name,
@@ -73,16 +84,6 @@ async def process_item(task_type: str, queue_payload: dict[str, Any]) -> None:
log_performance_metrics(
f"summary_{validated.workspace_name}_{validated.message_id}"
)
- else:
- await summarizer.summarize_if_needed(
- validated.workspace_name,
- validated.session_name,
- validated.message_id,
- validated.message_seq_in_session,
- )
- log_performance_metrics(
- f"summary_{validated.workspace_name}_{validated.message_id}"
- )
else:
raise ValueError(f"Invalid task type: {task_type}")
diff --git a/src/deriver/deriver.py b/src/deriver/deriver.py
index d3373b9c..e94ccd19 100644
--- a/src/deriver/deriver.py
+++ b/src/deriver/deriver.py
@@ -40,6 +40,7 @@ from src.utils.shared_models import (
ReasoningResponseWithThinking,
UnifiedObservation,
)
+from src.utils.tracing import with_sentry_transaction
from .prompts import (
critical_analysis_prompt,
@@ -116,7 +117,7 @@ async def peer_card_call(
return response.content
-@sentry_sdk.trace
+@with_sentry_transaction("process_representation_tasks_batch", op="deriver")
async def process_representation_tasks_batch(
sender_name: str,
target_name: str,
diff --git a/src/deriver/queue_manager.py b/src/deriver/queue_manager.py
index 1ba9fcfe..539e685f 100644
--- a/src/deriver/queue_manager.py
+++ b/src/deriver/queue_manager.py
@@ -288,8 +288,6 @@ class QueueManager:
######################
# Queue Worker Logic #
######################
-
- @sentry_sdk.trace
async def process_work_unit(self, work_unit_key: str, worker_id: str) -> None:
"""Process all messages for a specific work unit by routing to the correct handler."""
logger.debug(
diff --git a/src/main.py b/src/main.py
index 0ff72d93..a3e1594e 100644
--- a/src/main.py
+++ b/src/main.py
@@ -117,7 +117,7 @@ app = FastAPI(
title="Honcho API",
summary="The Identity Layer for the Agentic World",
description="""Honcho is a platform for giving agents user-centric memory and social cognition""",
- version="2.3.2",
+ version="2.3.3",
contact={
"name": "Plastic Labs",
"url": "https://honcho.dev",
diff --git a/src/routers/peers.py b/src/routers/peers.py
index 31d11d79..423aeb3d 100644
--- a/src/routers/peers.py
+++ b/src/routers/peers.py
@@ -137,11 +137,15 @@ async def get_sessions_for_peer(
@router.post(
"/{peer_id}/chat",
- response_model=schemas.DialecticResponse,
responses={
200: {
"description": "Response to a question informed by Honcho's User Representation",
- "content": {"text/event-stream": {}},
+ "content": {
+ "application/json": {
+ "schema": schemas.DialecticResponse.model_json_schema()
+ },
+ "text/event-stream": {},
+ },
},
},
dependencies=[
@@ -186,7 +190,17 @@ async def chat(
)
if isinstance(stream, AsyncIterator):
async for chunk in stream:
- yield chunk.content
+ if chunk.content:
+ # Send each chunk as JSON with nested delta object
+ stream_chunk = schemas.DialecticStreamChunk(
+ delta=schemas.DialecticStreamDelta(content=chunk.content)
+ )
+ yield f"data: {stream_chunk.model_dump_json()}\n\n"
+ # Send final done message
+ final_chunk = schemas.DialecticStreamChunk(
+ delta=schemas.DialecticStreamDelta(), done=True
+ )
+ yield f"data: {final_chunk.model_dump_json()}\n\n"
else:
raise HTTPException(status_code=500, detail="Invalid stream type")
except Exception as e:
diff --git a/src/schemas.py b/src/schemas.py
index ac1b226c..a9c2f8aa 100644
--- a/src/schemas.py
+++ b/src/schemas.py
@@ -315,6 +315,23 @@ class DialecticResponse(BaseModel):
content: str
+class DialecticStreamDelta(BaseModel):
+ """Delta object for streaming dialectic responses."""
+
+ content: str | None = None
+ # Future fields can be added here:
+ # premises: str | None = None
+ # tokens: int | None = None
+ # analytics: dict[str, Any] | None = None
+
+
+class DialecticStreamChunk(BaseModel):
+ """Chunk in a streaming dialectic response."""
+
+ delta: DialecticStreamDelta
+ done: bool = False
+
+
class SessionCounts(BaseModel):
"""Counts for a specific session in queue processing."""
diff --git a/src/utils/tracing.py b/src/utils/tracing.py
new file mode 100644
index 00000000..9d08dbc7
--- /dev/null
+++ b/src/utils/tracing.py
@@ -0,0 +1,48 @@
+import asyncio
+from collections.abc import Callable
+from functools import wraps
+from typing import ParamSpec, TypeVar, cast
+
+import sentry_sdk
+
+P = ParamSpec("P")
+T = TypeVar("T")
+
+
+def with_sentry_transaction(
+ name: str, op: str
+) -> Callable[[Callable[P, T]], Callable[P, T]]:
+ """Decorator to wrap a function in a Sentry transaction.
+
+ Args:
+ name: The name of the transaction.
+ op: The operation type (e.g., "deriver", "http.server").
+
+ Returns:
+ A decorator that wraps the target function in a Sentry transaction.
+
+ Example:
+ @with_sentry_transaction("process_task", op="deriver")
+ def process_task(data):
+ ...
+ """
+
+ def decorator(func: Callable[P, T]) -> Callable[P, T]:
+ if asyncio.iscoroutinefunction(func):
+
+ @wraps(func)
+ async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
+ with sentry_sdk.start_transaction(name=name, op=op):
+ return await func(*args, **kwargs)
+
+ return cast(Callable[P, T], async_wrapper)
+ else:
+
+ @wraps(func)
+ def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
+ with sentry_sdk.start_transaction(name=name, op=op):
+ return func(*args, **kwargs)
+
+ return sync_wrapper
+
+ return decorator
diff --git a/tests/bench/harness.py b/tests/bench/harness.py
index 1fd54113..f7f235a0 100755
--- a/tests/bench/harness.py
+++ b/tests/bench/harness.py
@@ -563,8 +563,10 @@ except Exception as e:
# Create an empty .env file in temp directory to satisfy docker-compose
# (even though the database service doesn't actually use it)
- if self.temp_dir:
+ if self.temp_dir and self.temp_dir.exists():
(self.temp_dir / ".env").touch()
+ else:
+ raise Exception("Temporary directory does not exist")
# Start database
self.start_database()
diff --git a/tests/sdk/test_client.py b/tests/sdk/test_client.py
index daff2157..42530e63 100644
--- a/tests/sdk/test_client.py
+++ b/tests/sdk/test_client.py
@@ -3,6 +3,7 @@ from unittest.mock import patch
import pytest
from fastapi.testclient import TestClient
from honcho_core.types import DeriverStatus
+from honcho_core.types.workspaces.sessions.message import Message
from sdks.python.src.honcho.async_client.client import AsyncHoncho
from sdks.python.src.honcho.async_client.pagination import AsyncPage
@@ -318,3 +319,118 @@ async def test_poll_deriver_status(client_fixture: tuple[Honcho | AsyncHoncho, s
observer_id=peer.id, sender_id=peer.id
)
assert isinstance(status, DeriverStatus)
+
+
+@pytest.mark.asyncio
+async def test_update_message_with_message_object(
+ client_fixture: tuple[Honcho | AsyncHoncho, str],
+):
+ """
+ Tests updating message metadata using a Message object.
+ """
+ honcho_client, client_type = client_fixture
+
+ if client_type == "async":
+ assert isinstance(honcho_client, AsyncHoncho)
+ session = await honcho_client.session(id="test-update-msg-session")
+ peer = await honcho_client.peer(id="test-update-msg-peer")
+
+ # Create a message
+ await session.add_messages([peer.message("test message")])
+ messages = await session.get_messages()
+ assert len(messages) >= 1
+ message = messages[0]
+ assert isinstance(message, Message)
+
+ # Update using Message object
+ updated = await honcho_client.update_message(message, {"key": "value"})
+ assert isinstance(updated, Message)
+ assert updated.metadata == {"key": "value"}
+ assert updated.id == message.id
+ else:
+ assert isinstance(honcho_client, Honcho)
+ session = honcho_client.session(id="test-update-msg-session")
+ peer = honcho_client.peer(id="test-update-msg-peer")
+
+ # Create a message
+ session.add_messages([peer.message("test message")])
+ messages = session.get_messages()
+ assert len(messages) >= 1
+ message = messages[0]
+ assert isinstance(message, Message)
+
+ # Update using Message object
+ updated = honcho_client.update_message(message, {"key": "value"})
+ assert isinstance(updated, Message)
+ assert updated.metadata == {"key": "value"}
+ assert updated.id == message.id
+
+
+@pytest.mark.asyncio
+async def test_update_message_with_message_id(
+ client_fixture: tuple[Honcho | AsyncHoncho, str],
+):
+ """
+ Tests updating message metadata using message_id string.
+ """
+ honcho_client, client_type = client_fixture
+
+ if client_type == "async":
+ assert isinstance(honcho_client, AsyncHoncho)
+ session = await honcho_client.session(id="test-update-msg-id-session")
+ peer = await honcho_client.peer(id="test-update-msg-id-peer")
+
+ # Create a message
+ await session.add_messages([peer.message("test message")])
+
+ messages = await session.get_messages()
+ assert len(messages) >= 1
+ message = messages[0]
+ assert message.metadata == {}
+
+ # Update using message_id string
+ updated = await honcho_client.update_message(
+ message.id, {"updated": True}, session_id=session.id
+ )
+ assert isinstance(updated, Message)
+ assert updated.metadata == {"updated": True}
+ assert updated.id == message.id
+ else:
+ assert isinstance(honcho_client, Honcho)
+ session = honcho_client.session(id="test-update-msg-id-session")
+ peer = honcho_client.peer(id="test-update-msg-id-peer")
+
+ # Create a message
+ messages = session.add_messages([peer.message("test message")])
+
+ messages = session.get_messages()
+ assert len(messages) >= 1
+ message = messages[0]
+ assert message.metadata == {}
+
+ # Update using message_id string
+ updated = honcho_client.update_message(
+ message.id, {"updated": True}, session_id=session.id
+ )
+ assert isinstance(updated, Message)
+ assert updated.metadata == {"updated": True}
+ assert updated.id == message.id
+
+
+@pytest.mark.asyncio
+async def test_update_message_validation(
+ client_fixture: tuple[Honcho | AsyncHoncho, str],
+):
+ """
+ Tests that update_message raises ValueError when message_id is provided without session_id.
+ """
+ honcho_client, client_type = client_fixture
+
+ if client_type == "async":
+ assert isinstance(honcho_client, AsyncHoncho)
+ with pytest.raises(ValueError, match="session_id is required"):
+ await honcho_client.update_message("msg_123", {"key": "value"})
+ else:
+ assert isinstance(honcho_client, Honcho)
+ with pytest.raises(ValueError, match="session_id is required"):
+ honcho_client.update_message("msg_123", {"key": "value"})
diff --git a/tests/sdk/test_pagination.py b/tests/sdk/test_pagination.py
new file mode 100644
index 00000000..2dcaceb9
--- /dev/null
+++ b/tests/sdk/test_pagination.py
@@ -0,0 +1,284 @@
+import pytest
+
+from sdks.python.src.honcho.async_client.client import AsyncHoncho
+from sdks.python.src.honcho.async_client.pagination import AsyncPage
+from sdks.python.src.honcho.async_client.peer import AsyncPeer
+from sdks.python.src.honcho.client import Honcho
+from sdks.python.src.honcho.pagination import SyncPage
+from sdks.python.src.honcho.peer import Peer
+
+
+@pytest.mark.asyncio
+async def test_sync_page_get_next_page(
+ client_fixture: tuple[Honcho | AsyncHoncho, str],
+):
+ """
+ Tests that SyncPage.get_next_page() works correctly for sync clients.
+ """
+ honcho_client, client_type = client_fixture
+
+ if client_type != "sync":
+ pytest.skip("Test only applicable to sync client")
+
+ assert isinstance(honcho_client, Honcho)
+
+ # Create multiple peers to test pagination
+ for i in range(15):
+ peer = honcho_client.peer(id=f"pagination-test-peer-{i}")
+ peer.get_metadata() # Create the peer
+
+ # Get first page - SDK doesn't expose size parameter yet, so we get default pagination
+ first_page = honcho_client.get_peers()
+ assert isinstance(first_page, SyncPage)
+ assert len(first_page.items) > 0
+
+ # If there's a next page, test get_next_page()
+ if first_page.has_next_page():
+ second_page = first_page.get_next_page()
+ assert second_page is not None
+ assert isinstance(second_page, SyncPage)
+ assert second_page.page == 2
+
+
+@pytest.mark.asyncio
+async def test_async_page_get_next_page(
+ client_fixture: tuple[Honcho | AsyncHoncho, str],
+):
+ """
+ Tests that AsyncPage.get_next_page() works correctly for async clients.
+ """
+ honcho_client, client_type = client_fixture
+
+ if client_type != "async":
+ pytest.skip("Test only applicable to async client")
+
+ assert isinstance(honcho_client, AsyncHoncho)
+
+ # Create multiple peers to test pagination
+ for i in range(15):
+ peer = await honcho_client.peer(id=f"pagination-test-peer-async-{i}")
+ await peer.get_metadata() # Create the peer
+
+ # Get first page - SDK doesn't expose size parameter yet, so we get default pagination
+ first_page = await honcho_client.get_peers()
+ assert isinstance(first_page, AsyncPage)
+ assert len(first_page.items) > 0
+
+ # If there's a next page, test get_next_page()
+ if first_page.has_next_page():
+ second_page = await first_page.get_next_page()
+ assert second_page is not None
+ assert isinstance(second_page, AsyncPage)
+ assert second_page.page == 2
+
+
+@pytest.mark.asyncio
+async def test_sync_page_transform_preserved_across_pages(
+ client_fixture: tuple[Honcho | AsyncHoncho, str],
+):
+ """
+ Tests that transformation function is preserved when getting next page (sync).
+ """
+ honcho_client, client_type = client_fixture
+
+ if client_type != "sync":
+ pytest.skip("Test only applicable to sync client")
+
+ assert isinstance(honcho_client, Honcho)
+
+ # Create multiple peers to ensure pagination
+ for i in range(25):
+ peer = honcho_client.peer(id=f"transform-test-peer-{i}")
+ peer.get_metadata()
+
+ # Get first page
+ first_page = honcho_client.get_peers()
+ assert isinstance(first_page, SyncPage)
+
+ # Verify items are Peer instances (transformed)
+ for item in first_page.items:
+ assert isinstance(item, Peer)
+
+ # Get next page and verify transformation is preserved
+ if first_page.has_next_page():
+ second_page = first_page.get_next_page()
+ assert second_page is not None
+ for item in second_page.items:
+ assert isinstance(item, Peer)
+
+
+@pytest.mark.asyncio
+async def test_async_page_transform_preserved_across_pages(
+ client_fixture: tuple[Honcho | AsyncHoncho, str],
+):
+ """
+ Tests that transformation function is preserved when getting next page (async).
+ """
+ honcho_client, client_type = client_fixture
+
+ if client_type != "async":
+ pytest.skip("Test only applicable to async client")
+
+ assert isinstance(honcho_client, AsyncHoncho)
+
+ # Create multiple peers to ensure pagination
+ for i in range(25):
+ peer = await honcho_client.peer(id=f"transform-test-peer-async-{i}")
+ await peer.get_metadata()
+
+ # Get first page
+ first_page = await honcho_client.get_peers()
+ assert isinstance(first_page, AsyncPage)
+
+ # Verify items are AsyncPeer instances (transformed)
+ for item in first_page.items:
+ assert isinstance(item, AsyncPeer)
+
+ # Get next page and verify transformation is preserved
+ if first_page.has_next_page():
+ second_page = await first_page.get_next_page()
+ assert second_page is not None
+ for item in second_page.items:
+ assert isinstance(item, AsyncPeer)
+
+
+@pytest.mark.asyncio
+async def test_sync_page_get_next_page_throws_exception_on_last_page(
+ client_fixture: tuple[Honcho | AsyncHoncho, str],
+):
+ """
+ Tests that get_next_page() returns None when on the last page (sync).
+ """
+ honcho_client, client_type = client_fixture
+
+ if client_type != "sync":
+ pytest.skip("Test only applicable to sync client")
+
+ assert isinstance(honcho_client, Honcho)
+
+ # Create just a few peers to ensure we're on the last page
+ for i in range(3):
+ peer = honcho_client.peer(id=f"last-page-test-peer-{i}")
+ peer.get_metadata()
+
+ # Get first page
+ first_page = honcho_client.get_peers()
+ assert isinstance(first_page, SyncPage)
+
+ # Should be on last page (or only page)
+ if not first_page.has_next_page():
+ # get_next_page should return None
+ try:
+ first_page.get_next_page()
+ except Exception as e:
+ assert isinstance(e, RuntimeError)
+
+
+@pytest.mark.asyncio
+async def test_async_page_get_next_page_throws_exception_on_last_page(
+ client_fixture: tuple[Honcho | AsyncHoncho, str],
+):
+ """
+ Tests that get_next_page() returns None when on the last page (async).
+ """
+ honcho_client, client_type = client_fixture
+
+ if client_type != "async":
+ pytest.skip("Test only applicable to async client")
+
+ assert isinstance(honcho_client, AsyncHoncho)
+
+ # Create just a few peers to ensure we're on the last page
+ for i in range(3):
+ peer = await honcho_client.peer(id=f"last-page-test-peer-async-{i}")
+ await peer.get_metadata()
+
+ # Get first page
+ first_page = await honcho_client.get_peers()
+ assert isinstance(first_page, AsyncPage)
+
+ # Should be on last page (or only page)
+ if not first_page.has_next_page():
+ # get_next_page should throw runtime error
+ try:
+ await first_page.get_next_page()
+ except Exception as e:
+ assert isinstance(e, RuntimeError)
+
+
+@pytest.mark.asyncio
+async def test_sync_page_manual_pagination(
+ client_fixture: tuple[Honcho | AsyncHoncho, str],
+):
+ """
+ Tests manual pagination with get_next_page (sync).
+ """
+ honcho_client, client_type = client_fixture
+
+ if client_type != "sync":
+ pytest.skip("Test only applicable to sync client")
+
+ assert isinstance(honcho_client, Honcho)
+
+ # Create enough peers to ensure multiple pages
+ for i in range(25):
+ peer = honcho_client.peer(id=f"manual-pagination-test-peer-{i}")
+ peer.get_metadata()
+
+ # Collect items via manual pagination
+ manual_items = []
+ manual_page = honcho_client.get_peers()
+ page_count = 0
+
+ while manual_page is not None:
+ page_count += 1
+ manual_items.extend(manual_page.items) # pyright: ignore
+
+ if not manual_page.has_next_page():
+ break
+
+ manual_page = manual_page.get_next_page()
+
+ # Should have collected all items
+ assert len(manual_items) >= 25 # pyright: ignore
+ # All items should be Peer instances
+ assert all(isinstance(item, Peer) for item in manual_items) # pyright: ignore
+
+
+@pytest.mark.asyncio
+async def test_async_page_manual_pagination(
+ client_fixture: tuple[Honcho | AsyncHoncho, str],
+):
+ """
+ Tests manual pagination with get_next_page (async).
+ """
+ honcho_client, client_type = client_fixture
+
+ if client_type != "async":
+ pytest.skip("Test only applicable to async client")
+
+ assert isinstance(honcho_client, AsyncHoncho)
+
+ # Create enough peers to ensure multiple pages
+ for i in range(25):
+ peer = await honcho_client.peer(id=f"manual-pagination-test-peer-async-{i}")
+ await peer.get_metadata()
+
+ # Collect items via manual pagination
+ manual_items = []
+ manual_page = await honcho_client.get_peers()
+ page_count = 0
+
+ while manual_page is not None:
+ page_count += 1
+ manual_items.extend(manual_page.items) # pyright: ignore
+
+ if not manual_page.has_next_page():
+ break
+
+ manual_page = await manual_page.get_next_page()
+
+ # Should have collected all items
+ assert len(manual_items) >= 25 # pyright: ignore
+ # All items should be AsyncPeer instances
+ assert all(isinstance(item, AsyncPeer) for item in manual_items) # pyright: ignore
diff --git a/tests/sdk/test_peer.py b/tests/sdk/test_peer.py
index fdd6032b..76120469 100644
--- a/tests/sdk/test_peer.py
+++ b/tests/sdk/test_peer.py
@@ -1,9 +1,12 @@
+from unittest.mock import AsyncMock, Mock, patch
+
import pytest
from sdks.python.src.honcho.async_client.client import AsyncHoncho
from sdks.python.src.honcho.async_client.peer import AsyncPeer
from sdks.python.src.honcho.client import Honcho
from sdks.python.src.honcho.peer import Peer
+from sdks.python.src.honcho.types import DialecticStreamResponse
@pytest.mark.asyncio
@@ -74,3 +77,211 @@ async def test_peer_get_sessions(client_fixture: tuple[Honcho | AsyncHoncho, str
session_ids = {s.id for s in sessions}
assert "s1" in session_ids
assert "s2" in session_ids
+
+
+@pytest.mark.asyncio
+async def test_peer_card_global(client_fixture: tuple[Honcho | AsyncHoncho, str]):
+ """
+ Tests getting a global peer card (no target).
+ """
+ honcho_client, client_type = client_fixture
+
+ if client_type == "async":
+ assert isinstance(honcho_client, AsyncHoncho)
+ peer = await honcho_client.peer(id="test-card-global-peer")
+ session = await honcho_client.session(id="test-card-global-session")
+
+ # Add some messages to create context
+ await session.add_messages([peer.message("I like pizza")])
+
+ # Get global peer card
+ card_response = await peer.card()
+ assert isinstance(card_response, str)
+ else:
+ assert isinstance(honcho_client, Honcho)
+ peer = honcho_client.peer(id="test-card-global-peer")
+ session = honcho_client.session(id="test-card-global-session")
+
+ # Add some messages to create context
+ session.add_messages([peer.message("I like pizza")])
+
+ # Get global peer card
+ card_response = peer.card()
+ assert isinstance(card_response, str)
+
+
+@pytest.mark.asyncio
+async def test_peer_card_local(client_fixture: tuple[Honcho | AsyncHoncho, str]):
+ """
+ Tests getting a local peer card (with target).
+ """
+ honcho_client, client_type = client_fixture
+
+ if client_type == "async":
+ assert isinstance(honcho_client, AsyncHoncho)
+ observer = await honcho_client.peer(id="test-card-local-observer")
+ target = await honcho_client.peer(id="test-card-local-target")
+ session = await honcho_client.session(id="test-card-local-session")
+
+ # Add messages from both peers
+ await session.add_messages(
+ [observer.message("Hello"), target.message("Hi there")]
+ )
+
+ # Get local peer card with target as Peer object
+ card_response = await observer.card(target=target)
+ assert isinstance(card_response, str)
+
+ # Get local peer card with target as string
+ card_response = await observer.card(target=target.id)
+ assert isinstance(card_response, str)
+ else:
+ assert isinstance(honcho_client, Honcho)
+ observer = honcho_client.peer(id="test-card-local-observer")
+ target = honcho_client.peer(id="test-card-local-target")
+ session = honcho_client.session(id="test-card-local-session")
+
+ # Add messages from both peers
+ 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)
+ assert isinstance(card_response, str)
+
+ # Get local peer card with target as string
+ card_response = observer.card(target=target.id)
+ assert isinstance(card_response, str)
+
+
+@pytest.mark.asyncio
+async def test_peer_card_validation(client_fixture: tuple[Honcho | AsyncHoncho, str]):
+ """
+ Tests peer card validation.
+ """
+ honcho_client, client_type = client_fixture
+
+ if client_type == "async":
+ assert isinstance(honcho_client, AsyncHoncho)
+ peer = await honcho_client.peer(id="test-card-validation-peer")
+
+ # Test with empty string
+ with pytest.raises(ValueError, match="target string cannot be empty"):
+ await peer.card(target="")
+ else:
+ assert isinstance(honcho_client, Honcho)
+ peer = honcho_client.peer(id="test-card-validation-peer")
+
+ # Test with empty string
+ with pytest.raises(ValueError, match="target string cannot be empty"):
+ peer.card(target="")
+
+
+@pytest.mark.asyncio
+async def test_peer_chat_streaming(client_fixture: tuple[Honcho | AsyncHoncho, str]):
+ """
+ Tests streaming chat with mocked response generator.
+ """
+
+ honcho_client, client_type = client_fixture
+ if client_type == "async":
+ assert isinstance(honcho_client, AsyncHoncho)
+ peer = await honcho_client.peer(id="test-stream-async-peer")
+ session = await honcho_client.session(id="test-stream-async-session")
+
+ # Add some messages to create context
+ await session.add_messages([peer.message("I like pizza")])
+
+ # Mock the async streaming response
+ async def mock_aiter_lines():
+ yield 'data: {"delta": {"content": "Hello"}}'
+ yield 'data: {"delta": {"content": " async"}}'
+ yield 'data: {"done": true}'
+
+ mock_response = AsyncMock()
+ mock_response.iter_lines = mock_aiter_lines
+ mock_response.__aenter__ = AsyncMock(return_value=mock_response)
+ mock_response.__aexit__ = AsyncMock(return_value=None)
+
+ # Mock the with_streaming_response.chat call
+ with patch.object(
+ honcho_client.core.workspaces.peers.with_streaming_response,
+ "chat",
+ return_value=mock_response,
+ ):
+ result = await peer.chat("Tell me something", stream=True)
+ assert isinstance(result, DialecticStreamResponse)
+
+ # Collect chunks
+ chunks: list[str] = []
+ async for chunk in result:
+ assert isinstance(chunk, str)
+ chunks.append(chunk)
+ assert chunks == ["Hello", " async"]
+ assert result.get_final_response()["content"] == "Hello async"
+ else:
+ assert isinstance(honcho_client, Honcho)
+ peer = honcho_client.peer(id="test-stream-peer")
+ session = honcho_client.session(id="test-stream-session")
+
+ # Add some messages to create context
+ session.add_messages([peer.message("I like pizza")])
+
+ # Mock the streaming response
+ def mock_iter_lines():
+ yield 'data: {"delta": {"content": "Hello"}}'
+ yield 'data: {"delta": {"content": " world"}}'
+ yield 'data: {"done": true}'
+
+ mock_response = Mock()
+ mock_response.iter_lines = mock_iter_lines
+ mock_response.__enter__ = Mock(return_value=mock_response)
+ mock_response.__exit__ = Mock(return_value=None)
+
+ # Mock the with_streaming_response.chat call
+ with patch.object(
+ honcho_client.core.workspaces.peers.with_streaming_response,
+ "chat",
+ return_value=mock_response,
+ ):
+ result = peer.chat("Tell me something", stream=True)
+ assert isinstance(result, DialecticStreamResponse)
+
+ # Collect chunks
+ chunks = list(result)
+ assert chunks == ["Hello", " world"]
+ assert result.get_final_response()["content"] == "Hello world"
+
+
+@pytest.mark.asyncio
+async def test_peer_chat_non_streaming(
+ client_fixture: tuple[Honcho | AsyncHoncho, str],
+):
+ """
+ Tests non-streaming chat (already using core SDK).
+ """
+ honcho_client, client_type = client_fixture
+
+ if client_type == "async":
+ assert isinstance(honcho_client, AsyncHoncho)
+ peer = await honcho_client.peer(id="test-non-stream-peer")
+ session = await honcho_client.session(id="test-non-stream-session")
+
+ # Add some messages
+ await session.add_messages([peer.message("I like pizza")])
+
+ # Non-streaming chat
+ response = await peer.chat("What do I like?", stream=False)
+ # Response can be None or a string
+ assert response is None or isinstance(response, str)
+ else:
+ assert isinstance(honcho_client, Honcho)
+ peer = honcho_client.peer(id="test-non-stream-peer")
+ session = honcho_client.session(id="test-non-stream-session")
+
+ # Add some messages
+ session.add_messages([peer.message("I like pizza")])
+
+ # Non-streaming chat
+ response = peer.chat("What do I like?", stream=False)
+ # Response can be None or a string
+ assert response is None or isinstance(response, str)
diff --git a/tests/sdk/test_session.py b/tests/sdk/test_session.py
index ba9797e2..ff6c10a8 100644
--- a/tests/sdk/test_session.py
+++ b/tests/sdk/test_session.py
@@ -1,4 +1,7 @@
+from unittest.mock import AsyncMock, patch
+
import pytest
+from honcho_core.types import DeriverStatus
from sdks.python.src.honcho.async_client.client import AsyncHoncho
from sdks.python.src.honcho.async_client.peer import AsyncPeer
@@ -297,3 +300,161 @@ async def test_session_working_rep(client_fixture: tuple[Honcho | AsyncHoncho, s
assert isinstance(peer, Peer)
session.add_messages([peer.message("test message for working rep")])
_working_rep = session.working_rep(peer)
+
+
+@pytest.mark.asyncio
+async def test_session_delete(client_fixture: tuple[Honcho | AsyncHoncho, str]) -> None:
+ """
+ Tests deleting a session.
+ """
+ honcho_client, client_type = client_fixture
+
+ if client_type == "async":
+ assert isinstance(honcho_client, AsyncHoncho)
+ session = await honcho_client.session(id="test-session-delete")
+ assert isinstance(session, AsyncSession)
+
+ # Add a peer to make the session exist
+ user = await honcho_client.peer(id="user-delete")
+ await session.add_peers([user])
+
+ # Delete should not raise an exception
+ await session.delete()
+
+ all_sessions_page = await honcho_client.get_sessions({"is_active": True})
+ all_sessions = all_sessions_page.items
+ all_session_ids = [s.id for s in all_sessions]
+
+ assert "test-session-delete" not in all_session_ids
+
+
+@pytest.mark.asyncio
+async def test_session_get_deriver_status(
+ client_fixture: tuple[Honcho | AsyncHoncho, str],
+):
+ """
+ Tests getting deriver status with various parameter combinations for sessions.
+ """
+ honcho_client, client_type = client_fixture
+
+ if client_type == "async":
+ assert isinstance(honcho_client, AsyncHoncho)
+ session = await honcho_client.session(id="test-session-deriver-status")
+ assert isinstance(session, AsyncSession)
+
+ status = await session.get_deriver_status()
+ assert isinstance(status, DeriverStatus)
+ assert hasattr(status, "total_work_units")
+ assert hasattr(status, "completed_work_units")
+ assert hasattr(status, "in_progress_work_units")
+ assert hasattr(status, "pending_work_units")
+ assert status.sessions is None
+
+ # Test with observer_id only
+ peer = await honcho_client.peer(id="test-peer-session-deriver")
+ await peer.get_metadata() # Create the peer
+ status = await session.get_deriver_status(observer_id=peer.id)
+ assert isinstance(status, DeriverStatus)
+
+ # Test with sender_id only
+ status = await session.get_deriver_status(sender_id=peer.id)
+ assert isinstance(status, DeriverStatus)
+
+ # Test with both observer_id and sender_id
+ status = await session.get_deriver_status(
+ observer_id=peer.id, sender_id=peer.id
+ )
+ assert isinstance(status, DeriverStatus)
+ else:
+ assert isinstance(honcho_client, Honcho)
+ session = honcho_client.session(id="test-session-deriver-status")
+ assert isinstance(session, Session)
+
+ # Test with no parameters
+ status = session.get_deriver_status()
+ assert isinstance(status, DeriverStatus)
+ assert hasattr(status, "total_work_units")
+ assert hasattr(status, "completed_work_units")
+ assert hasattr(status, "in_progress_work_units")
+ assert hasattr(status, "pending_work_units")
+ assert status.sessions is None
+
+ # Test with observer_id only
+ peer = honcho_client.peer(id="test-peer-session-deriver")
+ peer.get_metadata() # Create the peer
+ status = session.get_deriver_status(observer_id=peer.id)
+ assert isinstance(status, DeriverStatus)
+
+ # Test with sender_id only
+ status = session.get_deriver_status(sender_id=peer.id)
+ assert isinstance(status, DeriverStatus)
+
+ # Test with both observer_id and sender_id
+ status = session.get_deriver_status(observer_id=peer.id, sender_id=peer.id)
+ assert isinstance(status, DeriverStatus)
+
+
+@pytest.mark.asyncio
+async def test_session_poll_deriver_status(
+ client_fixture: tuple[Honcho | AsyncHoncho, str],
+):
+ """
+ Tests polling deriver status until completion for sessions.
+ """
+ honcho_client, client_type = client_fixture
+
+ # Mock the get_deriver_status method to return a "completed" status
+ # to avoid infinite polling in tests
+ completed_status = DeriverStatus(
+ total_work_units=0,
+ completed_work_units=0,
+ in_progress_work_units=0,
+ pending_work_units=0,
+ )
+
+ if client_type == "async":
+ assert isinstance(honcho_client, AsyncHoncho)
+ session = await honcho_client.session(id="test-session-poll-deriver")
+ assert isinstance(session, AsyncSession)
+
+ with patch.object(
+ session.__class__,
+ "get_deriver_status",
+ new=AsyncMock(return_value=completed_status),
+ ):
+ status = await session.poll_deriver_status()
+ assert isinstance(status, DeriverStatus)
+ assert status.pending_work_units == 0
+ assert status.in_progress_work_units == 0
+
+ # Test with parameters
+ peer = await honcho_client.peer(id="test-peer-session-poll")
+ with patch.object(
+ session.__class__,
+ "get_deriver_status",
+ new=AsyncMock(return_value=completed_status),
+ ):
+ status = await session.poll_deriver_status(
+ observer_id=peer.id, sender_id=peer.id
+ )
+ assert isinstance(status, DeriverStatus)
+ else:
+ assert isinstance(honcho_client, Honcho)
+ session = honcho_client.session(id="test-session-poll-deriver")
+ assert isinstance(session, Session)
+
+ with patch.object(
+ session.__class__, "get_deriver_status", return_value=completed_status
+ ):
+ status = session.poll_deriver_status()
+ assert isinstance(status, DeriverStatus)
+ assert status.pending_work_units == 0
+ assert status.in_progress_work_units == 0
+
+ # Test with parameters
+ peer = honcho_client.peer(id="test-peer-session-poll")
+ with patch.object(
+ session.__class__, "get_deriver_status", return_value=completed_status
+ ):
+ status = session.poll_deriver_status(observer_id=peer.id, sender_id=peer.id)
+ assert isinstance(status, DeriverStatus)
diff --git a/uv.lock b/uv.lock
index 7f5c7d67..f47926b5 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,5 +1,5 @@
version = 1
-revision = 2
+revision = 3
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.13'",
@@ -673,7 +673,7 @@ wheels = [
[[package]]
name = "honcho"
-version = "2.3.2"
+version = "2.3.3"
source = { virtual = "." }
dependencies = [
{ name = "alembic" },
@@ -760,12 +760,13 @@ dev = [
[[package]]
name = "honcho-ai"
-version = "1.4.0"
+version = "1.4.1"
source = { editable = "sdks/python" }
dependencies = [
{ name = "honcho-core" },
{ name = "httpx" },
{ name = "pydantic" },
+ { name = "typing-extensions", marker = "python_full_version < '3.12'" },
]
[package.dev-dependencies]
@@ -775,9 +776,10 @@ dev = [
[package.metadata]
requires-dist = [
- { name = "honcho-core", specifier = ">=1.4.0" },
+ { name = "honcho-core", specifier = ">=1.4.1a0" },
{ name = "httpx", specifier = ">=0.28.0,<1" },
{ name = "pydantic", specifier = ">=2.0.0,<3" },
+ { name = "typing-extensions", marker = "python_full_version < '3.12'", specifier = ">=4.12.0" },
]
[package.metadata.requires-dev]
@@ -785,7 +787,7 @@ dev = [{ name = "ruff", specifier = ">=0.11.13" }]
[[package]]
name = "honcho-core"
-version = "1.4.0"
+version = "1.4.1a0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -795,9 +797,9 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/71/c2/4d3737d5c0a75e2f59f324bed7428a9593c8edfcb7500cf402c46ad7378a/honcho_core-1.4.0.tar.gz", hash = "sha256:b30de9247763c01c3c37ed22b5d04d3c611440dd1983168f3d7d93f2cc0ecf7d", size = 126164, upload-time = "2025-08-12T19:06:54.828Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/2d/2f/fbf3ca017ffcdaef92e26fe84b77268c1c0b26f95e282d8684f50932d81a/honcho_core-1.4.1a0.tar.gz", hash = "sha256:e0023f46c65500d727e3fb1275781ae1e0b162c226a01352afd45c421c5f350b", size = 130826, upload-time = "2025-10-01T03:23:27.143Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c1/ac/6eb2e38376736bb1dfd26bbdfd96490bfbb6f0ddcb00b39913e93799e7d8/honcho_core-1.4.0-py3-none-any.whl", hash = "sha256:a84397fd9daf546a04f5458d7233ebd5eeafcb97af75ec60a9327db89abdaf03", size = 117602, upload-time = "2025-08-12T19:06:53.667Z" },
+ { url = "https://files.pythonhosted.org/packages/83/5f/2aa746b7fd92690ef782dcacc56a5c47a19a999b9729597c04c7cc509e52/honcho_core-1.4.1a0-py3-none-any.whl", hash = "sha256:8b4c86e3ac3964ff56fe6a449b9fa23be49530609c34bdf35c9ecde7cd871bba", size = 121704, upload-time = "2025-10-01T03:23:25.66Z" },
]
[[package]]