From 73c5eb1f5a86173a890d896bed6c4b1eef904666 Mon Sep 17 00:00:00 2001 From: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:25:30 -0400 Subject: [PATCH] SDK Changes (#223) * fix: Add top level transactions for tracing in deriver * fix: Add Delete methods to sdks for sessions * feat: Add session deletion and session level deriver status methods * fix: Streaming support to python sdk * fix: Checkpoint Streaming working in SDKs with workarounds for stainless * feat: fix pagination, add message metadata update, and get peer card methods * feat: parity in typescript and python sdks * fix: Address Coderabbit * fix: Address Coderabbit * chore: add basedpyright rules to sdk * fix: Code Rabbit & Changelog * fix: updated docs with api-reference --- .gitignore | 1 + CHANGELOG.md | 7 + README.md | 2 +- docs/changelog/compatibility-guide.mdx | 14 +- docs/changelog/introduction.mdx | 9 +- docs/docs.json | 60 +- .../endpoint/peers/get-peer-card.mdx | 3 + docs/v2/openapi.documented.yml | 546 +++++---- pyproject.toml | 4 +- sdks/python/pyproject.toml | 15 +- sdks/python/src/honcho/__init__.py | 2 + sdks/python/src/honcho/async_client/client.py | 53 +- .../src/honcho/async_client/pagination.py | 106 +- sdks/python/src/honcho/async_client/peer.py | 85 +- .../python/src/honcho/async_client/session.py | 125 +- sdks/python/src/honcho/client.py | 55 +- sdks/python/src/honcho/pagination.py | 100 +- sdks/python/src/honcho/peer.py | 85 +- sdks/python/src/honcho/session.py | 124 +- sdks/python/src/honcho/types.py | 106 ++ sdks/typescript/__tests__/client.test.ts | 90 ++ sdks/typescript/__tests__/integration.test.ts | 42 +- sdks/typescript/__tests__/pagination.test.ts | 559 ++++----- sdks/typescript/__tests__/peer.test.ts | 137 ++- sdks/typescript/__tests__/session.test.ts | 1029 ++++++++++------- sdks/typescript/bun.lock | 130 ++- sdks/typescript/package.json | 4 +- sdks/typescript/src/client.ts | 48 +- sdks/typescript/src/index.ts | 7 +- sdks/typescript/src/pagination.ts | 117 +- sdks/typescript/src/peer.ts | 135 ++- sdks/typescript/src/session.ts | 125 +- sdks/typescript/src/session_context.ts | 2 +- sdks/typescript/src/types.ts | 86 ++ sdks/typescript/src/validation.ts | 2 +- src/config.py | 2 +- src/deriver/consumer.py | 47 +- src/deriver/deriver.py | 3 +- src/deriver/queue_manager.py | 2 - src/main.py | 2 +- src/routers/peers.py | 20 +- src/schemas.py | 17 + src/utils/tracing.py | 48 + tests/bench/harness.py | 4 +- tests/sdk/test_client.py | 116 ++ tests/sdk/test_pagination.py | 284 +++++ tests/sdk/test_peer.py | 211 ++++ tests/sdk/test_session.py | 161 +++ uv.lock | 16 +- 49 files changed, 3610 insertions(+), 1338 deletions(-) create mode 100644 docs/v2/api-reference/endpoint/peers/get-peer-card.mdx create mode 100644 sdks/python/src/honcho/types.py create mode 100644 sdks/typescript/src/types.ts create mode 100644 src/utils/tracing.py create mode 100644 tests/sdk/test_pagination.py 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 @@ --- -![Static Badge](https://img.shields.io/badge/Version-2.3.2-blue) +![Static Badge](https://img.shields.io/badge/Version-2.3.3-blue) [![PyPI version](https://img.shields.io/pypi/v/honcho-ai.svg)](https://pypi.org/project/honcho-ai/) [![NPM version](https://img.shields.io/npm/v/@honcho-ai/sdk.svg)](https://npmjs.org/package/@honcho-ai/sdk) [![Discord](https://img.shields.io/discord/1016845111637839922?style=flat&logo=discord&logoColor=23ffffff&label=Plastic%20Labs&labelColor=235865F2)](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]]