diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0814ebff..90336129 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,23 @@ 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.2.0] - 2025-08-07
+## [2.3.0] — 2025-08-14
+
+### Added
+
+- `getSummaries` endpoint to get all available summaries for a session directly
+
+### Changed
+
+- Session Peer limit to be based on observers instead, renamed config value to
+ `SESSION_OBSERVERS_LIMIT`
+- Deriver uses `get_context` internally to prevent context window limit errors
+- `Messages` can take a custom timestamp for the `created_at` field, defaulting
+ to the current time
+- `get_context` endpoint returns detailed `Summary` object rather than just
+ summary content
+
+## [2.2.0] — 2025-08-07
### Added
@@ -28,14 +44,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Re-introduced Sentry tracing to deriver
- Better integrity logic in get_or_create API methods
-## [2.1.2] - 2025-07-30
+## [2.1.2] — 2025-07-30
### Fixed
- Summarizer module to ignore empty summaries and pass appropriate one to get_context
- Structured Outputs calls with OpenAI provider to pass strict=True to Pydantic Schema
-## [2.1.1] - 2025-07-23
+## [2.1.1] — 2025-07-23
### Added
@@ -58,7 +74,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Refactor summarization for performance and code clarity
- Refactor queue payloads for clarity
-## [2.1.0] - 2025-07-17
+## [2.1.0] — 2025-07-17
### Added
diff --git a/README.md b/README.md
index 3d82a0f7..4f6797f2 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# 🫡 Honcho
-
+
[](https://discord.gg/plasticlabs)
[](https://arxiv.org/abs/2310.06983)

diff --git a/docs/changelog/compatibility-guide.mdx b/docs/changelog/compatibility-guide.mdx
index 6f897c57..d3ed4788 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.2.0 (Current)
+### Honcho API v2.3.0 (Current)
- **Compatible Version:** v1.3.0
+ **Compatible Version:** v1.4.0
Install with:
```bash
- npm install @honcho-ai/sdk@1.3.0
+ npm install @honcho-ai/sdk@1.4.0
```
- **Compatible Version:** v1.3.0
+ **Compatible Version:** v1.4.0
Install with:
```bash
- pip install honcho-ai==1.3.0
+ pip install honcho-ai==1.4.0
```
@@ -33,20 +33,20 @@ This guide helps you understand which versions of Honcho's API are compatible wi
- **Compatible Version:** v1.2.1
+ **Compatible Version:** v1.4.0
Install with:
```bash
- npm install @honcho-ai/sdk@1.2.1
+ npm install @honcho-ai/sdk@1.4.0
```
- **Compatible Version:** v1.2.2
+ **Compatible Version:** v1.4.0
Install with:
```bash
- pip install honcho-ai==1.2.2
+ pip install honcho-ai==1.4.0
```
@@ -56,7 +56,8 @@ This guide helps you understand which versions of Honcho's API are compatible wi
| Honcho API Version | TypeScript SDK | Python SDK |
|-------------------|---------------|------------|
-| v2.2.0 (Current) | v1.3.0 | v1.3.0 |
+| v2.3.0 (Current) | v1.4.0 | v1.4.0 |
+| v2.2.0 | v1.3.0 | v1.3.0 |
| v2.1.2 | v1.2.1 | v1.2.2 |
| v2.1.1 | v1.2.1 | v1.2.2 |
| v2.1.0 | v1.2.1 | v1.2.2 |
diff --git a/docs/changelog/introduction.mdx b/docs/changelog/introduction.mdx
index 225563a0..bd9e6836 100644
--- a/docs/changelog/introduction.mdx
+++ b/docs/changelog/introduction.mdx
@@ -27,7 +27,23 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
### Honcho API and SDK Changelogs
-
+
+ ### Added
+
+ - `getSummaries` endpoint to get all available summaries for a session directly
+
+ ### Changed
+
+ - Session Peer limit to be based on observers instead, renamed config value to
+ `SESSION_OBSERVERS_LIMIT`
+ - Deriver uses `get_context` internally to prevent context window limit errors
+ - `Messages` can take a custom timestamp for the `created_at` field, defaulting
+ to the current time
+ - `get_context` endpoint returns detailed `Summary` object rather than just
+ summary content
+
+
+
### Added
- Arbitrary filters now available on all search endpoints
@@ -239,6 +255,16 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
[Python SDK](https://pypi.org/project/honcho-ai/)
+
+ ### Added
+
+ - getSummaries API returning structured summaries
+
+ ### Changed
+
+ - Messages can take an optional `created_at` value, defaulting to the current
+ time (UTC ISO 8601)
+
### Added
@@ -275,6 +301,16 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
[TypeScript SDK](https://www.npmjs.com/package/@honcho-ai/sdk)
+
+ ### Added
+
+ - getSummaries API returning structured summaries
+
+ ### Changed
+
+ - Messages can take an optional `created_at` value, defaulting to the current
+ time (UTC ISO 8601)
+
### Added
diff --git a/docs/docs.json b/docs/docs.json
index 118d6877..e1fa6b51 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -14,7 +14,7 @@
"navigation": {
"versions": [
{
- "version": "v2.2.0",
+ "version": "v2.3.0",
"api": {
"openapi": ["openapi.documented.yml"]
},
@@ -131,6 +131,7 @@
"v2/api-reference/endpoint/sessions/get-peer-config",
"v2/api-reference/endpoint/sessions/set-peer-config",
"v2/api-reference/endpoint/sessions/get-session-context",
+ "v2/api-reference/endpoint/sessions/get-session-summaries",
"v2/api-reference/endpoint/sessions/search-session"
]
},
@@ -141,12 +142,21 @@
"v2/api-reference/endpoint/messages/get-messages",
"v2/api-reference/endpoint/messages/get-message",
"v2/api-reference/endpoint/messages/update-message",
- "v2/api-reference/endpoint/messages/upload-file"
+ "v2/api-reference/endpoint/messages/create-messages-with-file"
]
},
{
"group": "keys",
"pages": ["v2/api-reference/endpoint/keys/create-key"]
+ },
+ {
+ "group": "webhooks",
+ "pages": [
+ "v2/api-reference/endpoint/webhooks/list-webhook-endpoints",
+ "v2/api-reference/endpoint/webhooks/get-or-create-webhook-endpoint",
+ "v2/api-reference/endpoint/webhooks/delete-webhook-endpoint",
+ "v2/api-reference/endpoint/webhooks/test-emit"
+ ]
}
]
},
diff --git a/docs/package.json b/docs/package.json
index c029ccfa..a9f1757c 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -5,7 +5,7 @@
"main": ".pnp.js",
"scripts": {
"dev": "mint dev",
- "openapi": "npx @mintlify/scraping openapi-file openapi.json -o api-reference/endpoint",
+ "openapi": "npx @mintlify/scraping openapi-file openapi.documented.yml -o api-reference/endpoint",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
diff --git a/docs/v2/api-reference/endpoint/messages/upload-file.mdx b/docs/v2/api-reference/endpoint/messages/create-messages-with-file.mdx
similarity index 100%
rename from docs/v2/api-reference/endpoint/messages/upload-file.mdx
rename to docs/v2/api-reference/endpoint/messages/create-messages-with-file.mdx
diff --git a/docs/v2/api-reference/endpoint/sessions/get-session-summaries.mdx b/docs/v2/api-reference/endpoint/sessions/get-session-summaries.mdx
new file mode 100644
index 00000000..9ae150bc
--- /dev/null
+++ b/docs/v2/api-reference/endpoint/sessions/get-session-summaries.mdx
@@ -0,0 +1,3 @@
+---
+openapi: get /v2/workspaces/{workspace_id}/sessions/{session_id}/summaries
+---
diff --git a/docs/v2/api-reference/endpoint/webhooks/delete-webhook-endpoint.mdx b/docs/v2/api-reference/endpoint/webhooks/delete-webhook-endpoint.mdx
new file mode 100644
index 00000000..03663d39
--- /dev/null
+++ b/docs/v2/api-reference/endpoint/webhooks/delete-webhook-endpoint.mdx
@@ -0,0 +1,3 @@
+---
+openapi: delete /v2/workspaces/{workspace_id}/webhooks/{endpoint_id}
+---
diff --git a/docs/v2/api-reference/endpoint/webhooks/get-or-create-webhook-endpoint.mdx b/docs/v2/api-reference/endpoint/webhooks/get-or-create-webhook-endpoint.mdx
new file mode 100644
index 00000000..3eca8afb
--- /dev/null
+++ b/docs/v2/api-reference/endpoint/webhooks/get-or-create-webhook-endpoint.mdx
@@ -0,0 +1,3 @@
+---
+openapi: post /v2/workspaces/{workspace_id}/webhooks
+---
diff --git a/docs/v2/api-reference/endpoint/webhooks/list-webhook-endpoints.mdx b/docs/v2/api-reference/endpoint/webhooks/list-webhook-endpoints.mdx
new file mode 100644
index 00000000..a205df80
--- /dev/null
+++ b/docs/v2/api-reference/endpoint/webhooks/list-webhook-endpoints.mdx
@@ -0,0 +1,3 @@
+---
+openapi: get /v2/workspaces/{workspace_id}/webhooks
+---
diff --git a/docs/v2/api-reference/endpoint/webhooks/test-emit.mdx b/docs/v2/api-reference/endpoint/webhooks/test-emit.mdx
new file mode 100644
index 00000000..8c2cbddd
--- /dev/null
+++ b/docs/v2/api-reference/endpoint/webhooks/test-emit.mdx
@@ -0,0 +1,3 @@
+---
+openapi: get /v2/workspaces/{workspace_id}/webhooks/test
+---
diff --git a/docs/v2/documentation/reference/sdk.mdx b/docs/v2/documentation/reference/sdk.mdx
index 135cfd1c..45d67e9b 100644
--- a/docs/v2/documentation/reference/sdk.mdx
+++ b/docs/v2/documentation/reference/sdk.mdx
@@ -555,6 +555,33 @@ const response = await openai.chat.completions.create({
```
+### Custom Message Timestamps
+
+When creating messages, you can optionally specify a custom `created_at` timestamp instead of using the server's current time:
+
+```bash
+curl -X POST "https://api.honcho.dev/v2/workspaces/{workspace_id}/sessions/{session_id}/messages" \
+ -H "Authorization: Bearer $API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "messages": [
+ {
+ "peer_id": "user123",
+ "content": "This message happened yesterday",
+ "created_at": "2024-01-01T12:00:00Z",
+ "metadata": {"source": "historical_data"}
+ }
+ ]
+ }'
+```
+
+This is useful for:
+- Importing historical conversation data
+- Backfilling messages from other systems
+- Maintaining accurate timeline ordering when processing batch data
+
+If `created_at` is not provided, messages will use the server's current timestamp.
+
### Metadata and Filtering
See [Using Filters](/v2/guides/using-filters) for more examples on how to use filters.
diff --git a/docs/v2/openapi.documented.yml b/docs/v2/openapi.documented.yml
index ebc1df20..c6567be9 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.1.1
+ version: 2.3.0
servers:
- url: http://localhost:8000
description: Local Development Server
@@ -231,42 +231,23 @@ paths:
description: ID of the workspace to search
title: Workspace Id
description: ID of the workspace to search
- - name: page
- in: query
- required: false
- schema:
- type: integer
- minimum: 1
- description: Page number
- default: 1
- title: Page
- description: Page number
- - name: size
- in: query
- required: false
- schema:
- type: integer
- maximum: 100
- minimum: 1
- description: Page size
- default: 50
- title: Size
- description: Page size
requestBody:
required: true
content:
application/json:
schema:
- type: string
- description: Search query
- title: Query
+ $ref: '#/components/schemas/MessageSearchOptions'
+ description: 'Message search parameters '
responses:
'200':
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Page_Message_'
+ type: array
+ items:
+ $ref: '#/components/schemas/Message'
+ title: Response Search Workspace V2 Workspaces Workspace Id Search Post
'422':
description: Validation Error
content:
@@ -282,10 +263,9 @@ paths:
apiKey: 'My API Key',
});
- // Automatically fetches more pages as needed.
- for await (const message of client.workspaces.search('workspace_id', { body: 'body' })) {
- console.log(message.id);
- }
+ const messages = await client.workspaces.search('workspace_id', { query: 'query' });
+
+ console.log(messages);
- lang: Python
source: |-
from honcho_core import Honcho
@@ -293,12 +273,11 @@ paths:
client = Honcho(
api_key="My API Key",
)
- page = client.workspaces.search(
+ messages = client.workspaces.search(
workspace_id="workspace_id",
- body="body",
+ query="query",
)
- page = page.items[0]
- print(page.id)
+ print(messages)
/v2/workspaces/{workspace_id}/deriver/status:
get:
tags:
@@ -891,27 +870,6 @@ paths:
description: ID of the peer
title: Peer Id
description: ID of the peer
- - name: page
- in: query
- required: false
- schema:
- type: integer
- minimum: 1
- description: Page number
- default: 1
- title: Page
- description: Page number
- - name: size
- in: query
- required: false
- schema:
- type: integer
- maximum: 100
- minimum: 1
- description: Page size
- default: 50
- title: Size
- description: Page size
requestBody:
required: true
content:
@@ -925,7 +883,10 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/Page_Message_'
+ type: array
+ items:
+ $ref: '#/components/schemas/Message'
+ title: Response Search Peer V2 Workspaces Workspace Id Peers Peer Id Search Post
'422':
description: Validation Error
content:
@@ -943,12 +904,11 @@ paths:
});
- // Automatically fetches more pages as needed.
+ const messages = await client.workspaces.peers.search('workspace_id', 'peer_id', { query: 'query'
+ });
- for await (const message of client.workspaces.peers.search('workspace_id', 'peer_id', { query:
- 'query' })) {
- console.log(message.id);
- }
+
+ console.log(messages);
- lang: Python
source: |-
from honcho_core import Honcho
@@ -956,13 +916,12 @@ paths:
client = Honcho(
api_key="My API Key",
)
- page = client.workspaces.peers.search(
+ messages = client.workspaces.peers.search(
peer_id="peer_id",
workspace_id="workspace_id",
query="query",
)
- page = page.items[0]
- print(page.id)
+ print(messages)
/v2/workspaces/{workspace_id}/sessions:
post:
tags:
@@ -1699,20 +1658,20 @@ paths:
$ref: '#/components/schemas/HTTPValidationError'
x-codeSamples:
- lang: JavaScript
- source: >-
+ source: |-
import Honcho from '@honcho-ai/core';
-
const client = new Honcho({
apiKey: 'My API Key',
});
+ const sessionPeerConfig = await client.workspaces.sessions.peers.getConfig(
+ 'workspace_id',
+ 'session_id',
+ 'peer_id',
+ );
- const response = await client.workspaces.sessions.peers.getConfig('workspace_id', 'session_id',
- 'peer_id');
-
-
- console.log(response.observe_me);
+ console.log(sessionPeerConfig.observe_me);
- lang: Python
source: |-
from honcho_core import Honcho
@@ -1720,12 +1679,12 @@ paths:
client = Honcho(
api_key="My API Key",
)
- response = client.workspaces.sessions.peers.get_config(
+ session_peer_config = client.workspaces.sessions.peers.get_config(
peer_id="peer_id",
workspace_id="workspace_id",
session_id="session_id",
)
- print(response.observe_me)
+ print(session_peer_config.observe_me)
post:
tags:
- sessions
@@ -1814,15 +1773,16 @@ paths:
- sessions
summary: Get Session Context
description: >-
- Produce a context object from the session. The caller provides a token limit which the entire context
- must fit into.
+ Produce a context object from the session. The caller provides an optional token limit which the
+ entire context must fit into.
- To do this, we allocate 40% of the token limit to the summary, and 60% to recent messages -- as many
- as can fit.
+ If not provided, the context will be exhaustive (within configured max tokens). To do this, we
+ allocate 40% of the token limit
- If the caller does not want a summary, we allocate all the tokens to recent messages.
+ to the summary, and 60% to recent messages -- as many as can fit. Note that the summary will usually
+ take up less space than
- The default token limit if not provided is 2048. (TODO: make this configurable)
+ this. If the caller does not want a summary, we allocate all the tokens to recent messages.
operationId: get_session_context_v2_workspaces__workspace_id__sessions__session_id__context_get
security:
- HTTPBearer: []
@@ -1850,19 +1810,24 @@ paths:
schema:
anyOf:
- type: integer
+ maximum: 100000
- type: 'null'
- description: Number of tokens to use for the context. Includes summary if set to true
+ 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)
title: Tokens
- description: Number of tokens to use for the context. Includes summary if set to true
+ 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)
- name: summary
in: query
required: false
schema:
type: boolean
- description: Whether to summarize the session history prior to the cutoff message
- default: false
+ description: Whether or not to include a summary *if* one is available for the session
+ default: true
title: Summary
- description: Whether to summarize the session history prior to the cutoff message
+ description: Whether or not to include a summary *if* one is available for the session
responses:
'200':
description: Successful Response
@@ -1900,6 +1865,74 @@ paths:
workspace_id="workspace_id",
)
print(response.id)
+ /v2/workspaces/{workspace_id}/sessions/{session_id}/summaries:
+ get:
+ tags:
+ - sessions
+ summary: Get Session Summaries
+ description: |-
+ Get available summaries for a session.
+
+ Returns both short and long summaries if available, including metadata like
+ the message ID they cover up to, creation timestamp, and token count.
+ operationId: get_session_summaries_v2_workspaces__workspace_id__sessions__session_id__summaries_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: session_id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: ID of the session
+ title: Session Id
+ description: ID of the session
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SessionSummaries'
+ '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.sessions.summaries('workspace_id', 'session_id');
+
+ console.log(response.id);
+ - lang: Python
+ source: |-
+ from honcho_core import Honcho
+
+ client = Honcho(
+ api_key="My API Key",
+ )
+ response = client.workspaces.sessions.summaries(
+ session_id="session_id",
+ workspace_id="workspace_id",
+ )
+ print(response.id)
/v2/workspaces/{workspace_id}/sessions/{session_id}/search:
post:
tags:
@@ -1927,41 +1960,23 @@ paths:
description: ID of the session
title: Session Id
description: ID of the session
- - name: page
- in: query
- required: false
- schema:
- type: integer
- minimum: 1
- description: Page number
- default: 1
- title: Page
- description: Page number
- - name: size
- in: query
- required: false
- schema:
- type: integer
- maximum: 100
- minimum: 1
- description: Page size
- default: 50
- title: Size
- description: Page size
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MessageSearchOptions'
- description: 'Message search parameters '
+ description: Message search parameters
responses:
'200':
description: Successful Response
content:
application/json:
schema:
- $ref: '#/components/schemas/Page_Message_'
+ type: array
+ items:
+ $ref: '#/components/schemas/Message'
+ title: Response Search Session V2 Workspaces Workspace Id Sessions Session Id Search Post
'422':
description: Validation Error
content:
@@ -1970,19 +1985,20 @@ paths:
$ref: '#/components/schemas/HTTPValidationError'
x-codeSamples:
- lang: JavaScript
- source: |-
+ source: >-
import Honcho from '@honcho-ai/core';
+
const client = new Honcho({
apiKey: 'My API Key',
});
- // Automatically fetches more pages as needed.
- for await (const message of client.workspaces.sessions.search('workspace_id', 'session_id', {
- query: 'query',
- })) {
- console.log(message.id);
- }
+
+ const messages = await client.workspaces.sessions.search('workspace_id', 'session_id', { query:
+ 'query' });
+
+
+ console.log(messages);
- lang: Python
source: |-
from honcho_core import Honcho
@@ -1990,13 +2006,12 @@ paths:
client = Honcho(
api_key="My API Key",
)
- page = client.workspaces.sessions.search(
+ messages = client.workspaces.sessions.search(
session_id="session_id",
workspace_id="workspace_id",
query="query",
)
- page = page.items[0]
- print(page.id)
+ print(messages)
/v2/workspaces/{workspace_id}/sessions/{session_id}/messages/:
post:
tags:
@@ -2505,6 +2520,267 @@ paths:
)
key = client.keys.create()
print(key)
+ /v2/workspaces/{workspace_id}/webhooks:
+ post:
+ tags:
+ - webhooks
+ summary: Get Or Create Webhook Endpoint
+ description: Get or create a webhook endpoint URL.
+ operationId: get_or_create_webhook_endpoint_v2_workspaces__workspace_id__webhooks_post
+ security:
+ - HTTPBearer: []
+ - {}
+ parameters:
+ - name: workspace_id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Workspace ID
+ title: Workspace Id
+ description: Workspace ID
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/WebhookEndpointCreate'
+ description: Webhook endpoint parameters
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/WebhookEndpoint'
+ '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 webhookEndpoint = await client.workspaces.webhooks.getOrCreate('workspace_id', { url: 'url'
+ });
+
+
+ console.log(webhookEndpoint.id);
+ - lang: Python
+ source: |-
+ from honcho_core import Honcho
+
+ client = Honcho(
+ api_key="My API Key",
+ )
+ webhook_endpoint = client.workspaces.webhooks.get_or_create(
+ workspace_id="workspace_id",
+ url="url",
+ )
+ print(webhook_endpoint.id)
+ get:
+ tags:
+ - webhooks
+ summary: List Webhook Endpoints
+ description: List all webhook endpoints, optionally filtered by workspace.
+ operationId: list_webhook_endpoints_v2_workspaces__workspace_id__webhooks_get
+ security:
+ - HTTPBearer: []
+ - {}
+ parameters:
+ - name: workspace_id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Workspace ID
+ title: Workspace Id
+ description: Workspace ID
+ - name: page
+ in: query
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ description: Page number
+ default: 1
+ title: Page
+ description: Page number
+ - name: size
+ in: query
+ required: false
+ schema:
+ type: integer
+ maximum: 100
+ minimum: 1
+ description: Page size
+ default: 50
+ title: Size
+ description: Page size
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Page_WebhookEndpoint_'
+ '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',
+ });
+
+ // Automatically fetches more pages as needed.
+ for await (const webhookEndpoint of client.workspaces.webhooks.list('workspace_id')) {
+ console.log(webhookEndpoint.id);
+ }
+ - lang: Python
+ source: |-
+ from honcho_core import Honcho
+
+ client = Honcho(
+ api_key="My API Key",
+ )
+ page = client.workspaces.webhooks.list(
+ workspace_id="workspace_id",
+ )
+ page = page.items[0]
+ print(page.id)
+ /v2/workspaces/{workspace_id}/webhooks/{endpoint_id}:
+ delete:
+ tags:
+ - webhooks
+ summary: Delete Webhook Endpoint
+ description: Delete a specific webhook endpoint.
+ operationId: delete_webhook_endpoint_v2_workspaces__workspace_id__webhooks__endpoint_id__delete
+ security:
+ - HTTPBearer: []
+ - {}
+ parameters:
+ - name: workspace_id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Workspace ID
+ title: Workspace Id
+ description: Workspace ID
+ - name: endpoint_id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Webhook endpoint ID
+ title: Endpoint Id
+ description: Webhook endpoint ID
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema: {}
+ '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 webhook = await client.workspaces.webhooks.delete('workspace_id', 'endpoint_id');
+
+ console.log(webhook);
+ - lang: Python
+ source: |-
+ from honcho_core import Honcho
+
+ client = Honcho(
+ api_key="My API Key",
+ )
+ webhook = client.workspaces.webhooks.delete(
+ endpoint_id="endpoint_id",
+ workspace_id="workspace_id",
+ )
+ print(webhook)
+ /v2/workspaces/{workspace_id}/webhooks/test:
+ get:
+ tags:
+ - webhooks
+ summary: Test Emit
+ description: Test publishing a webhook event.
+ operationId: test_emit_v2_workspaces__workspace_id__webhooks_test_get
+ security:
+ - HTTPBearer: []
+ - {}
+ parameters:
+ - name: workspace_id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Workspace ID
+ title: Workspace Id
+ description: Workspace ID
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema: {}
+ '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.webhooks.testEmit('workspace_id');
+
+ console.log(response);
+ - lang: Python
+ source: |-
+ from honcho_core import Honcho
+
+ client = Honcho(
+ api_key="My API Key",
+ )
+ response = client.workspaces.webhooks.test_emit(
+ "workspace_id",
+ )
+ print(response)
components:
schemas:
Body_create_messages_with_file_v2_workspaces__workspace_id__sessions__session_id__messages_upload_post:
@@ -2612,9 +2888,7 @@ components:
type: string
title: Peer Id
session_id:
- anyOf:
- - type: string
- - type: 'null'
+ type: string
title: Session Id
metadata:
additionalProperties: true
@@ -2670,6 +2944,12 @@ components:
type: object
- type: 'null'
title: Metadata
+ created_at:
+ anyOf:
+ - type: string
+ format: date-time
+ - type: 'null'
+ title: Created At
type: object
required:
- content
@@ -2682,7 +2962,7 @@ components:
- additionalProperties: true
type: object
- type: 'null'
- title: Filter
+ title: Filters
type: object
title: MessageGet
MessageSearchOptions:
@@ -2691,12 +2971,20 @@ components:
type: string
title: Query
description: Search query
- semantic:
+ filters:
anyOf:
- - type: boolean
+ - additionalProperties: true
+ type: object
- type: 'null'
- title: Semantic
- description: Whether to explicitly use semantic search to filter the results
+ title: Filters
+ description: Filters to scope the search
+ limit:
+ type: integer
+ maximum: 100
+ minimum: 1
+ title: Limit
+ description: Number of results to return
+ default: 10
type: object
required:
- query
@@ -2798,6 +3086,35 @@ components:
- page
- size
title: Page[Session]
+ Page_WebhookEndpoint_:
+ properties:
+ items:
+ items:
+ $ref: '#/components/schemas/WebhookEndpoint'
+ type: array
+ title: Items
+ total:
+ type: integer
+ minimum: 0
+ title: Total
+ page:
+ type: integer
+ minimum: 1
+ title: Page
+ size:
+ type: integer
+ minimum: 1
+ title: Size
+ pages:
+ type: integer
+ minimum: 0
+ title: Pages
+ type: object
+ required:
+ - items
+ - page
+ - size
+ title: Page[WebhookEndpoint]
Page_Workspace_:
properties:
items:
@@ -2884,7 +3201,7 @@ components:
- additionalProperties: true
type: object
- type: 'null'
- title: Filter
+ title: Filters
type: object
title: PeerGet
PeerRepresentationGet:
@@ -2960,13 +3277,14 @@ components:
type: array
title: Messages
summary:
- type: string
- title: Summary
+ anyOf:
+ - $ref: '#/components/schemas/Summary'
+ - type: 'null'
+ description: The summary if available
type: object
required:
- id
- messages
- - summary
title: SessionContext
SessionCreate:
properties:
@@ -3037,7 +3355,7 @@ components:
- additionalProperties: true
type: object
- type: 'null'
- title: Filter
+ title: Filters
type: object
title: SessionGet
SessionPeerConfig:
@@ -3059,6 +3377,25 @@ components:
representation of this peer
type: object
title: SessionPeerConfig
+ SessionSummaries:
+ properties:
+ id:
+ type: string
+ title: Id
+ short_summary:
+ anyOf:
+ - $ref: '#/components/schemas/Summary'
+ - type: 'null'
+ description: The short summary if available
+ long_summary:
+ anyOf:
+ - $ref: '#/components/schemas/Summary'
+ - type: 'null'
+ description: The long summary if available
+ type: object
+ required:
+ - id
+ title: SessionSummaries
SessionUpdate:
properties:
metadata:
@@ -3075,6 +3412,36 @@ components:
title: Configuration
type: object
title: SessionUpdate
+ Summary:
+ properties:
+ content:
+ type: string
+ title: Content
+ description: The summary text
+ message_id:
+ type: integer
+ title: Message Id
+ description: The ID of the message that this summary covers up to
+ summary_type:
+ type: string
+ title: Summary Type
+ description: The type of summary (short or long)
+ created_at:
+ type: string
+ title: Created At
+ description: The timestamp of when the summary was created (ISO format)
+ token_count:
+ type: integer
+ title: Token Count
+ description: The number of tokens in the summary text
+ type: object
+ required:
+ - content
+ - message_id
+ - summary_type
+ - created_at
+ - token_count
+ title: Summary
ValidationError:
properties:
loc:
@@ -3096,6 +3463,39 @@ components:
- msg
- type
title: ValidationError
+ WebhookEndpoint:
+ properties:
+ id:
+ type: string
+ title: Id
+ workspace_id:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Workspace Id
+ url:
+ type: string
+ title: Url
+ created_at:
+ type: string
+ format: date-time
+ title: Created At
+ type: object
+ required:
+ - id
+ - workspace_id
+ - url
+ - created_at
+ title: WebhookEndpoint
+ WebhookEndpointCreate:
+ properties:
+ url:
+ type: string
+ title: Url
+ type: object
+ required:
+ - url
+ title: WebhookEndpointCreate
Workspace:
properties:
id:
@@ -3147,7 +3547,7 @@ components:
- additionalProperties: true
type: object
- type: 'null'
- title: Filter
+ title: Filters
type: object
title: WorkspaceGet
WorkspaceUpdate:
diff --git a/pyproject.toml b/pyproject.toml
index 2766f5fd..ac6f8a0d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "honcho"
-version = "2.2.0"
+version = "2.3.0"
description = "Honcho Server"
authors = [
{name = "Plastic Labs", email = "hello@plasticlabs.ai"},
@@ -32,8 +32,6 @@ dependencies = [
]
[tool.uv]
dev-dependencies = [
- "honcho-core==1.3.0",
- "honcho-ai==1.3.0",
"pytest>=8.2.2",
"sqlalchemy-utils>=0.41.2",
"pytest-asyncio>=0.23.7",
@@ -44,8 +42,17 @@ dev-dependencies = [
"basedpyright>=1.29.4",
"pre-commit>=4.2.0",
"pytest-cov>=6.2.1",
+ "honcho-ai",
]
+[tool.uv.workspace]
+members = [
+ "sdks/python",
+]
+
+[tool.uv.sources]
+honcho-ai = { workspace = true }
+
[tool.ruff.lint]
# from https://docs.astral.sh/ruff/linter/#rule-selection example
select = [
diff --git a/scripts/update_version.py b/scripts/update_version.py
index a07a007e..4d99020b 100755
--- a/scripts/update_version.py
+++ b/scripts/update_version.py
@@ -608,7 +608,7 @@ TYPESCRIPT_VERSION=
)
# Add new row at the top
- new_row = f"| v{api_version} (Current) | v{typescript_version} | v{python_version} | Latest release |"
+ new_row = f"| v{api_version} (Current) | v{typescript_version} | v{python_version} |"
# Reconstruct table
new_table = header + new_row + "\n" + rows + after_table
diff --git a/sdks/python/CHANGELOG.md b/sdks/python/CHANGELOG.md
index 6e29a94b..49451499 100644
--- a/sdks/python/CHANGELOG.md
+++ b/sdks/python/CHANGELOG.md
@@ -5,6 +5,17 @@ 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/).
+## [1.4.0] - 2025-08-12
+
+### Added
+
+- getSummaries API returning structured summaries
+
+### Changed
+
+- Messages can take an optional `created_at` value, defaulting to the current
+ time (UTC ISO 8601)
+
## [1.3.0] - 2025-08-04
### Added
diff --git a/sdks/python/examples/get_summaries.py b/sdks/python/examples/get_summaries.py
new file mode 100644
index 00000000..892016d4
--- /dev/null
+++ b/sdks/python/examples/get_summaries.py
@@ -0,0 +1,82 @@
+"""
+Example of getting session summaries using the Honcho Python SDK.
+
+This example demonstrates how to retrieve both short and long summaries
+for a session, including their metadata like message ID, creation timestamp,
+and token count.
+"""
+
+from honcho import AsyncHoncho, Honcho, SessionSummaries
+import asyncio
+import os
+
+# Initialize the Honcho client
+api_key = os.getenv("HONCHO_API_KEY")
+if not api_key:
+ raise ValueError("Please set HONCHO_API_KEY environment variable")
+
+client = Honcho(api_key=api_key)
+
+# Get a session (replace with your actual session ID)
+session = client.session("my-conversation-session")
+
+# Get summaries for the session
+summaries: SessionSummaries = session.get_summaries()
+
+print(f"Session ID: {summaries.id}")
+print("-" * 50)
+
+# Check and display short summary
+if summaries.short_summary:
+ print("SHORT SUMMARY:")
+ print(f" Content: {summaries.short_summary.content[:200]}...") # First 200 chars
+ print(f" Covers up to message ID: {summaries.short_summary.message_id}")
+ print(f" Created at: {summaries.short_summary.created_at}")
+ print(f" Token count: {summaries.short_summary.token_count}")
+ print(f" Type: {summaries.short_summary.summary_type}")
+else:
+ print("No short summary available yet")
+
+print("-" * 50)
+
+# Check and display long summary
+if summaries.long_summary:
+ print("LONG SUMMARY:")
+ print(f" Content: {summaries.long_summary.content[:200]}...") # First 200 chars
+ print(f" Covers up to message ID: {summaries.long_summary.message_id}")
+ print(f" Created at: {summaries.long_summary.created_at}")
+ print(f" Token count: {summaries.long_summary.token_count}")
+ print(f" Type: {summaries.long_summary.summary_type}")
+else:
+ print("No long summary available yet")
+
+# Example with async client
+print("\n" + "=" * 50)
+print("ASYNC EXAMPLE:")
+print("=" * 50)
+
+
+async def get_summaries_async():
+ async_client = AsyncHoncho(api_key=api_key)
+
+ # Get a session
+ async_session = async_client.session("my-conversation-session")
+
+ # Get summaries asynchronously
+ summaries = await async_session.get_summaries()
+
+ print(f"Session ID (async): {summaries.id}")
+
+ if summaries.short_summary:
+ print(
+ f"Short summary available with {summaries.short_summary.token_count} tokens"
+ )
+
+ if summaries.long_summary:
+ print(
+ f"Long summary available with {summaries.long_summary.token_count} tokens"
+ )
+
+
+# Run the async example
+asyncio.run(get_summaries_async())
diff --git a/sdks/python/examples/pydantic_validation_example.py b/sdks/python/examples/pydantic_validation_example.py
index 0fe24c4b..b44fe8d1 100644
--- a/sdks/python/examples/pydantic_validation_example.py
+++ b/sdks/python/examples/pydantic_validation_example.py
@@ -5,11 +5,13 @@ This example shows how the SDK now uses Pydantic to validate inputs at runtime,
providing better error messages and type safety.
"""
+import datetime
import logging
-from honcho import Honcho
from pydantic import ValidationError
+from honcho import Honcho
+
logging.basicConfig(level=logging.INFO)
@@ -119,6 +121,24 @@ def demonstrate_validation():
except ValidationError as e:
print(f"❌ Validation error: {e}")
+ # Example 10: Custom created_at timestamps
+ print("10. Custom created_at timestamps:")
+ try:
+ honcho = Honcho(environment="local", workspace_id="test")
+ peer = honcho.peer("alice")
+ message = peer.message("Hello, world!", created_at=datetime.datetime.now())
+ print(f"✅ Created message: {message}")
+ message = peer.message(
+ "Hello, world!",
+ created_at=datetime.datetime(1999, 1, 1, tzinfo=datetime.timezone.utc),
+ )
+ print(f"✅ Created message: {message}")
+ session = honcho.session("conversation_1")
+ session.add_messages([message])
+ print(f"✅ Added message to session: {session}")
+ except ValidationError as e:
+ print(f"❌ Validation error: {e}")
+
print("\n" + "=" * 50 + "\n")
print("🎉 Pydantic validation examples completed!")
diff --git a/sdks/python/pyproject.toml b/sdks/python/pyproject.toml
index 2fd3cd1f..73254de3 100644
--- a/sdks/python/pyproject.toml
+++ b/sdks/python/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "honcho-ai"
-version = "1.3.0"
+version = "1.4.0"
description = "Official DX Optimized Python SDK for Honcho"
dynamic = ["readme"]
license = "Apache-2.0"
@@ -8,7 +8,7 @@ authors = [
{ name = "Plastic Labs", email = "hello@plasticlabs.ai" },
]
dependencies = [
- "honcho-core==1.3.0",
+ "honcho-core>=1.4.0",
"httpx>=0.28.0, <1",
"pydantic>=2.0.0, <3",
]
diff --git a/sdks/python/src/honcho/__init__.py b/sdks/python/src/honcho/__init__.py
index 16b77130..f3ac73b7 100644
--- a/sdks/python/src/honcho/__init__.py
+++ b/sdks/python/src/honcho/__init__.py
@@ -44,9 +44,9 @@ from .client import Honcho
from .pagination import SyncPage
from .peer import Peer
from .session import Session
-from .session_context import SessionContext
+from .session_context import SessionContext, SessionSummaries, Summary
-__version__ = "1.0.0"
+__version__ = "1.4.0"
__author__ = "Plastic Labs"
__email__ = "hello@plasticlabs.ai"
@@ -59,5 +59,7 @@ __all__ = [
"Peer",
"Session",
"SessionContext",
+ "SessionSummaries",
+ "Summary",
"SyncPage",
]
diff --git a/sdks/python/src/honcho/async_client/peer.py b/sdks/python/src/honcho/async_client/peer.py
index b3c0eeb8..b21968b3 100644
--- a/sdks/python/src/honcho/async_client/peer.py
+++ b/sdks/python/src/honcho/async_client/peer.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import datetime
from typing import TYPE_CHECKING
from honcho_core import AsyncHoncho as AsyncHonchoCore
@@ -175,6 +176,10 @@ class AsyncPeer(BaseModel):
metadata: dict[str, object] | None = Field(
None, description="Optional metadata dictionary"
),
+ created_at: datetime.datetime | str | None = Field(
+ None,
+ description="Optional created-at timestamp for the message. Accepts a datetime which will be converted to an ISO 8601 string, or a preformatted string.",
+ ),
) -> MessageCreateParam:
"""
Create a MessageCreateParam object attributed to this peer.
@@ -189,7 +194,18 @@ class AsyncPeer(BaseModel):
Returns:
A new MessageCreateParam object with this peer's ID and the provided content
"""
- return MessageCreateParam(peer_id=self.id, content=content, metadata=metadata)
+ created_at_str: str | None
+ if isinstance(created_at, datetime.datetime):
+ created_at_str = created_at.isoformat()
+ else:
+ created_at_str = created_at
+
+ return MessageCreateParam(
+ peer_id=self.id,
+ content=content,
+ metadata=metadata,
+ created_at=created_at_str,
+ )
async def get_metadata(self) -> dict[str, object]:
"""
diff --git a/sdks/python/src/honcho/async_client/session.py b/sdks/python/src/honcho/async_client/session.py
index da95859c..64e03ffe 100644
--- a/sdks/python/src/honcho/async_client/session.py
+++ b/sdks/python/src/honcho/async_client/session.py
@@ -8,7 +8,7 @@ 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 ..session_context import SessionContext
+from ..session_context import SessionContext, SessionSummaries, Summary
from ..utils import prepare_file_for_upload
from .pagination import AsyncPage
@@ -440,8 +440,72 @@ class AsyncSession(BaseModel):
summary=summary,
)
+ # Convert the honcho_core summary to our Summary if it exists
+ session_summary = None
+ if context.summary:
+ session_summary = Summary(
+ content=context.summary.content,
+ message_id=context.summary.message_id,
+ summary_type=context.summary.summary_type,
+ created_at=context.summary.created_at,
+ token_count=context.summary.token_count,
+ )
+
return SessionContext(
- session_id=self.id, messages=context.messages, summary=context.summary
+ session_id=self.id, messages=context.messages, summary=session_summary
+ )
+
+ async def get_summaries(self) -> SessionSummaries:
+ """
+ Get available summaries for this session.
+
+ Makes an async API call to retrieve both short and long summaries for this session,
+ if they are available. Summaries are created asynchronously by the backend
+ as messages are added to the session.
+
+ Returns:
+ A SessionSummaries object containing:
+ - id: The session ID
+ - short_summary: The short summary if available, including metadata
+ - long_summary: The long summary if available, including metadata
+
+ Note:
+ Summaries may be None if:
+ - Not enough messages have been added to trigger summary generation
+ - The summary generation is still in progress
+ - Summary generation is disabled for this session
+ """
+ # Use the honcho_core client to get summaries
+ response = await self._client.workspaces.sessions.summaries(
+ session_id=self.id,
+ workspace_id=self.workspace_id,
+ )
+
+ # Create Summary objects from the response data
+ short_summary = None
+ if response.short_summary:
+ short_summary = Summary(
+ content=response.short_summary.content,
+ message_id=response.short_summary.message_id,
+ summary_type=response.short_summary.summary_type,
+ created_at=response.short_summary.created_at,
+ token_count=response.short_summary.token_count,
+ )
+
+ long_summary = None
+ if response.long_summary:
+ long_summary = Summary(
+ content=response.long_summary.content,
+ message_id=response.long_summary.message_id,
+ summary_type=response.long_summary.summary_type,
+ created_at=response.long_summary.created_at,
+ token_count=response.long_summary.token_count,
+ )
+
+ return SessionSummaries(
+ id=response.id or self.id,
+ short_summary=short_summary,
+ long_summary=long_summary,
)
@validate_call
diff --git a/sdks/python/src/honcho/peer.py b/sdks/python/src/honcho/peer.py
index 58f40dea..920163bf 100644
--- a/sdks/python/src/honcho/peer.py
+++ b/sdks/python/src/honcho/peer.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import datetime
from typing import TYPE_CHECKING
from honcho_core import Honcho as HonchoCore
@@ -158,6 +159,10 @@ class Peer(BaseModel):
metadata: dict[str, object] | None = Field(
None, description="Optional metadata dictionary"
),
+ created_at: datetime.datetime | str | None = Field(
+ None,
+ description="Optional created-at timestamp for the message. Accepts a datetime which will be converted to an ISO 8601 string, or a preformatted string.",
+ ),
) -> MessageCreateParam:
"""
Create a MessageCreateParam object attributed to this peer.
@@ -172,7 +177,18 @@ class Peer(BaseModel):
Returns:
A new MessageCreateParam object with this peer's ID and the provided content
"""
- return MessageCreateParam(peer_id=self.id, content=content, metadata=metadata)
+ created_at_str: str | None
+ if isinstance(created_at, datetime.datetime):
+ created_at_str = created_at.isoformat()
+ else:
+ created_at_str = created_at
+
+ return MessageCreateParam(
+ peer_id=self.id,
+ content=content,
+ metadata=metadata,
+ created_at=created_at_str,
+ )
def get_metadata(self) -> dict[str, object]:
"""
diff --git a/sdks/python/src/honcho/session.py b/sdks/python/src/honcho/session.py
index 29216ce7..b3962ea3 100644
--- a/sdks/python/src/honcho/session.py
+++ b/sdks/python/src/honcho/session.py
@@ -9,7 +9,7 @@ from honcho_core.types.workspaces.sessions.message import Message
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
from .pagination import SyncPage
-from .session_context import SessionContext
+from .session_context import SessionContext, SessionSummaries, Summary
from .utils import prepare_file_for_upload
if TYPE_CHECKING:
@@ -421,8 +421,72 @@ class Session(BaseModel):
summary=summary,
)
+ # Convert the honcho_core summary to our Summary if it exists
+ session_summary = None
+ if context.summary:
+ session_summary = Summary(
+ content=context.summary.content,
+ message_id=context.summary.message_id,
+ summary_type=context.summary.summary_type,
+ created_at=context.summary.created_at,
+ token_count=context.summary.token_count,
+ )
+
return SessionContext(
- session_id=self.id, messages=context.messages, summary=context.summary
+ session_id=self.id, messages=context.messages, summary=session_summary
+ )
+
+ def get_summaries(self) -> SessionSummaries:
+ """
+ Get available summaries for this session.
+
+ Makes an API call to retrieve both short and long summaries for this session,
+ if they are available. Summaries are created asynchronously by the backend
+ as messages are added to the session.
+
+ Returns:
+ A SessionSummaries object containing:
+ - id: The session ID
+ - short_summary: The short summary if available, including metadata
+ - long_summary: The long summary if available, including metadata
+
+ Note:
+ Summaries may be None if:
+ - Not enough messages have been added to trigger summary generation
+ - The summary generation is still in progress
+ - Summary generation is disabled for this session
+ """
+ # Use the honcho_core client to get summaries
+ response = self._client.workspaces.sessions.summaries(
+ session_id=self.id,
+ workspace_id=self.workspace_id,
+ )
+
+ # Create Summary objects from the response data
+ short_summary = None
+ if response.short_summary:
+ short_summary = Summary(
+ content=response.short_summary.content,
+ message_id=response.short_summary.message_id,
+ summary_type=response.short_summary.summary_type,
+ created_at=response.short_summary.created_at,
+ token_count=response.short_summary.token_count,
+ )
+
+ long_summary = None
+ if response.long_summary:
+ long_summary = Summary(
+ content=response.long_summary.content,
+ message_id=response.long_summary.message_id,
+ summary_type=response.long_summary.summary_type,
+ created_at=response.long_summary.created_at,
+ token_count=response.long_summary.token_count,
+ )
+
+ return SessionSummaries(
+ id=response.id or self.id,
+ short_summary=short_summary,
+ long_summary=long_summary,
)
@validate_call
diff --git a/sdks/python/src/honcho/session_context.py b/sdks/python/src/honcho/session_context.py
index 87480802..1cd9e7f5 100644
--- a/sdks/python/src/honcho/session_context.py
+++ b/sdks/python/src/honcho/session_context.py
@@ -9,6 +9,34 @@ if TYPE_CHECKING:
from .peer import Peer
+class Summary(BaseModel):
+ """Represents a summary of a session's conversation."""
+
+ content: str = Field(..., description="The summary text")
+ message_id: int = Field(
+ ..., description="The ID of the message that this summary covers up to"
+ )
+ summary_type: str = Field(..., description="The type of summary (short or long)")
+ created_at: str = Field(
+ ..., description="The timestamp of when the summary was created (ISO format)"
+ )
+ token_count: int = Field(
+ ..., description="The number of tokens in the summary text"
+ )
+
+
+class SessionSummaries(BaseModel):
+ """Contains both short and long summaries for a session."""
+
+ id: str = Field(..., description="The session ID")
+ short_summary: Summary | None = Field(
+ None, description="The short summary if available"
+ )
+ long_summary: Summary | None = Field(
+ None, description="The long summary if available"
+ )
+
+
class SessionContext(BaseModel):
"""
Represents the context of a session containing a curated list of messages.
@@ -27,8 +55,8 @@ class SessionContext(BaseModel):
messages: list[Message] = Field(
..., description="List of Message objects to include in the context"
)
- summary: str = Field(
- ..., description="Summary of the session history prior to the message cutoff"
+ summary: Summary | None = Field(
+ None, description="Summary of the session history prior to the message cutoff"
)
@validate_call
@@ -40,8 +68,8 @@ class SessionContext(BaseModel):
messages: list[Message] = Field(
..., description="List of Message objects to include in the context"
),
- summary: str = Field(
- ...,
+ summary: Summary | None = Field(
+ None,
description="Summary of the session history prior to the message cutoff",
),
) -> None:
@@ -50,6 +78,7 @@ class SessionContext(BaseModel):
Args:
messages: List of Message objects to include in the context
+ summary: Optional Summary object containing summary information
"""
super().__init__(
session_id=session_id,
@@ -80,10 +109,6 @@ class SessionContext(BaseModel):
"""
assistant_id = assistant if isinstance(assistant, str) else assistant.id
- summary_message = {
- "role": "system",
- "content": f"{self.summary}",
- }
messages = [
{
"role": "assistant" if message.peer_id == assistant_id else "user",
@@ -92,7 +117,14 @@ class SessionContext(BaseModel):
}
for message in self.messages
]
- return [summary_message, *messages] if self.summary else messages
+
+ if self.summary:
+ summary_message = {
+ "role": "system",
+ "content": f"{self.summary.content}",
+ }
+ return [summary_message, *messages]
+ return messages
def to_anthropic(
self,
@@ -120,10 +152,6 @@ class SessionContext(BaseModel):
"""
assistant_id = assistant if isinstance(assistant, str) else assistant.id
- summary_message = {
- "role": "user",
- "content": f"{self.summary}",
- }
messages = [
{
"role": "assistant",
@@ -136,7 +164,14 @@ class SessionContext(BaseModel):
}
for message in self.messages
]
- return [summary_message, *messages] if self.summary else messages
+
+ if self.summary:
+ summary_message = {
+ "role": "user",
+ "content": f"{self.summary.content}",
+ }
+ return [summary_message, *messages]
+ return messages
def __len__(self) -> int:
"""
@@ -154,4 +189,4 @@ class SessionContext(BaseModel):
Returns:
A string representation suitable for debugging
"""
- return f"SessionContext(messages={len(self.messages)}, summary={self.summary})"
+ return f"SessionContext(messages={len(self.messages)}, summary={'present' if self.summary else 'None'})"
diff --git a/sdks/python/uv.lock b/sdks/python/uv.lock
index c8f8e68e..21c5b230 100644
--- a/sdks/python/uv.lock
+++ b/sdks/python/uv.lock
@@ -1,5 +1,5 @@
version = 1
-revision = 2
+revision = 3
requires-python = ">=3.8"
resolution-markers = [
"python_full_version >= '3.9'",
@@ -38,7 +38,7 @@ wheels = [
[[package]]
name = "anyio"
-version = "4.9.0"
+version = "4.10.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.9'",
@@ -47,20 +47,20 @@ dependencies = [
{ name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" },
{ name = "idna", marker = "python_full_version >= '3.9'" },
{ name = "sniffio", marker = "python_full_version >= '3.9'" },
- { name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" },
+ { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" },
]
[[package]]
name = "certifi"
-version = "2025.4.26"
+version = "2025.8.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
]
[[package]]
@@ -78,7 +78,7 @@ version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" },
+ { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
wheels = [
@@ -96,13 +96,13 @@ wheels = [
[[package]]
name = "honcho-ai"
-version = "1.3.0"
+version = "1.4.0"
source = { editable = "." }
dependencies = [
{ name = "honcho-core" },
{ name = "httpx" },
{ name = "pydantic", version = "2.10.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "pydantic", version = "2.11.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "pydantic", version = "2.11.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
]
[package.dev-dependencies]
@@ -112,7 +112,7 @@ dev = [
[package.metadata]
requires-dist = [
- { name = "honcho-core", specifier = "==1.3.0" },
+ { name = "honcho-core", specifier = ">=1.4.0" },
{ name = "httpx", specifier = ">=0.28.0,<1" },
{ name = "pydantic", specifier = ">=2.0.0,<3" },
]
@@ -122,22 +122,22 @@ dev = [{ name = "ruff", specifier = ">=0.11.13" }]
[[package]]
name = "honcho-core"
-version = "1.3.0"
+version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "anyio", version = "4.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "anyio", version = "4.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
{ name = "distro" },
{ name = "httpx" },
{ name = "pydantic", version = "2.10.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "pydantic", version = "2.11.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "pydantic", version = "2.11.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
{ name = "sniffio" },
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ee/49/1810de2351be1fdff330ffadcc5eef709889b2f698b0340966cae2c1eef9/honcho_core-1.3.0.tar.gz", hash = "sha256:276a73e8d523f7d22f06746922fda97e4a11fbbef4a51a0b43573bc817089cec", size = 123333, upload-time = "2025-08-06T16:42:10.941Z" }
+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" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/6e/3f/3beb750ed65bb1b604c585ae1c51a54f8b34ea3002f4525f93fe86549531/honcho_core-1.3.0-py3-none-any.whl", hash = "sha256:c7c0bf77b61162e6c65a580327fc054785aef794923abc35acd3dbd8287245db", size = 112895, upload-time = "2025-08-06T16:42:08.967Z" },
+ { 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" },
]
[[package]]
@@ -159,7 +159,7 @@ version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "anyio", version = "4.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "anyio", version = "4.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
@@ -197,7 +197,7 @@ wheels = [
[[package]]
name = "pydantic"
-version = "2.11.5"
+version = "2.11.7"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.9'",
@@ -205,12 +205,12 @@ resolution-markers = [
dependencies = [
{ name = "annotated-types", marker = "python_full_version >= '3.9'" },
{ name = "pydantic-core", version = "2.33.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
- { name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
{ name = "typing-inspection", marker = "python_full_version >= '3.9'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload-time = "2025-05-22T21:18:08.761Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload-time = "2025-05-22T21:18:06.329Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" },
]
[[package]]
@@ -334,7 +334,7 @@ resolution-markers = [
"python_full_version >= '3.9'",
]
dependencies = [
- { name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
wheels = [
@@ -440,27 +440,27 @@ wheels = [
[[package]]
name = "ruff"
-version = "0.11.13"
+version = "0.12.8"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054, upload-time = "2025-06-05T21:00:15.721Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516, upload-time = "2025-06-05T20:59:32.944Z" },
- { url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083, upload-time = "2025-06-05T20:59:37.03Z" },
- { url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024, upload-time = "2025-06-05T20:59:39.741Z" },
- { url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324, upload-time = "2025-06-05T20:59:42.185Z" },
- { url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416, upload-time = "2025-06-05T20:59:44.319Z" },
- { url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197, upload-time = "2025-06-05T20:59:46.935Z" },
- { url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615, upload-time = "2025-06-05T20:59:49.534Z" },
- { url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080, upload-time = "2025-06-05T20:59:51.654Z" },
- { url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315, upload-time = "2025-06-05T20:59:54.469Z" },
- { url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640, upload-time = "2025-06-05T20:59:56.986Z" },
- { url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364, upload-time = "2025-06-05T20:59:59.154Z" },
- { url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462, upload-time = "2025-06-05T21:00:01.481Z" },
- { url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028, upload-time = "2025-06-05T21:00:04.06Z" },
- { url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992, upload-time = "2025-06-05T21:00:06.249Z" },
- { url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944, upload-time = "2025-06-05T21:00:08.459Z" },
- { url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669, upload-time = "2025-06-05T21:00:11.147Z" },
- { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928, upload-time = "2025-06-05T21:00:13.758Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" },
+ { url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" },
+ { url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" },
+ { url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745, upload-time = "2025-08-07T19:05:18.709Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" },
+ { url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" },
+ { url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" },
+ { url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162, upload-time = "2025-08-07T19:05:34.449Z" },
+ { url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" },
+ { url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382, upload-time = "2025-08-07T19:05:38.468Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482, upload-time = "2025-08-07T19:05:40.391Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload-time = "2025-08-07T19:05:42.866Z" },
]
[[package]]
@@ -486,14 +486,14 @@ wheels = [
[[package]]
name = "typing-extensions"
-version = "4.14.0"
+version = "4.14.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.9'",
]
-sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
]
[[package]]
@@ -501,7 +501,7 @@ name = "typing-inspection"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
wheels = [
diff --git a/sdks/typescript/CHANGELOG.md b/sdks/typescript/CHANGELOG.md
index ad57256e..cc29097e 100644
--- a/sdks/typescript/CHANGELOG.md
+++ b/sdks/typescript/CHANGELOG.md
@@ -5,6 +5,17 @@ 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/).
+## [1.4.0] - 2025-08-12
+
+### Added
+
+- getSummaries API returning structured summaries
+
+### Changed
+
+- Messages can take an optional `created_at` value, defaulting to the current
+ time (UTC ISO 8601)
+
## [1.3.0] - 2025-08-04
### Added
diff --git a/sdks/typescript/__tests__/integration.test.ts b/sdks/typescript/__tests__/integration.test.ts
index 1c64a5e4..cc876a8b 100644
--- a/sdks/typescript/__tests__/integration.test.ts
+++ b/sdks/typescript/__tests__/integration.test.ts
@@ -1,18 +1,18 @@
-import { Honcho } from '../src/client';
-import { Peer } from '../src/peer';
-import { Session } from '../src/session';
-import { SessionContext } from '../src/session_context';
-import { Page } from '../src/pagination';
+import { Honcho } from '../src/client'
+import { Peer } from '../src/peer'
+import { Session } from '../src/session'
+import { SessionContext } from '../src/session_context'
+import { Page } from '../src/pagination'
// Mock the @honcho-ai/core module
-let mockWorkspacesApi: any;
+let mockWorkspacesApi: any
jest.mock('@honcho-ai/core', () => {
- return jest.fn().mockImplementation(() => mockWorkspacesApi);
-});
+ return jest.fn().mockImplementation(() => mockWorkspacesApi)
+})
describe('Honcho SDK Integration Tests', () => {
- let honcho: Honcho;
+ let honcho: Honcho
beforeEach(() => {
mockWorkspacesApi = {
@@ -29,7 +29,12 @@ describe('Honcho SDK Integration Tests', () => {
},
sessions: {
list: jest.fn(),
- peers: { add: jest.fn(), set: jest.fn(), remove: jest.fn(), list: jest.fn() },
+ peers: {
+ add: jest.fn(),
+ set: jest.fn(),
+ remove: jest.fn(),
+ list: jest.fn(),
+ },
messages: { create: jest.fn(), list: jest.fn() },
getOrCreate: jest.fn(),
update: jest.fn(),
@@ -41,112 +46,135 @@ describe('Honcho SDK Integration Tests', () => {
list: jest.fn(),
search: jest.fn(),
},
- };
+ }
- jest.clearAllMocks();
+ jest.clearAllMocks()
honcho = new Honcho({
workspaceId: 'integration-test-workspace',
apiKey: 'test-api-key',
environment: 'local',
- });
- });
+ })
+ })
describe('Complete Workflow Integration', () => {
it('should handle complete chat session workflow', async () => {
// Setup mock responses
- const mockPeerData = { id: 'assistant', metadata: { role: 'ai' } };
- const mockSessionData = { id: 'chat-session', metadata: { topic: 'general' } };
+ const mockPeerData = { id: 'assistant', metadata: { role: 'ai' } }
+ const mockSessionData = {
+ id: 'chat-session',
+ metadata: { topic: 'general' },
+ }
const mockMessages = [
{ id: 'msg1', content: 'Hello', peer_id: 'user' },
{ id: 'msg2', content: 'Hi there!', peer_id: 'assistant' },
- ];
- const mockContextData = { messages: mockMessages, summary: 'Friendly greeting' };
+ ]
+ const mockContextData = {
+ messages: mockMessages,
+ summary: {
+ content: 'Friendly greeting',
+ message_id: 5,
+ summary_type: 'short',
+ created_at: '2024-01-01T00:00:00Z',
+ token_count: 50,
+ },
+ }
- mockWorkspacesApi.workspaces.peers.getOrCreate.mockResolvedValue(mockPeerData);
- mockWorkspacesApi.workspaces.sessions.getOrCreate.mockResolvedValue(mockSessionData);
- mockWorkspacesApi.workspaces.sessions.peers.add.mockResolvedValue({});
- mockWorkspacesApi.workspaces.sessions.messages.create.mockResolvedValue({});
- mockWorkspacesApi.workspaces.sessions.getContext.mockResolvedValue(mockContextData);
- mockWorkspacesApi.workspaces.peers.chat.mockResolvedValue({ content: 'AI response' });
+ mockWorkspacesApi.workspaces.peers.getOrCreate.mockResolvedValue(
+ mockPeerData
+ )
+ mockWorkspacesApi.workspaces.sessions.getOrCreate.mockResolvedValue(
+ mockSessionData
+ )
+ mockWorkspacesApi.workspaces.sessions.peers.add.mockResolvedValue({})
+ mockWorkspacesApi.workspaces.sessions.messages.create.mockResolvedValue(
+ {}
+ )
+ mockWorkspacesApi.workspaces.sessions.getContext.mockResolvedValue(
+ mockContextData
+ )
+ mockWorkspacesApi.workspaces.peers.chat.mockResolvedValue({
+ content: 'AI response',
+ })
// Step 1: Create peers
- const user = await honcho.peer('user');
- const assistant = await honcho.peer('assistant');
+ const user = await honcho.peer('user')
+ const assistant = await honcho.peer('assistant')
- expect(user).toBeInstanceOf(Peer);
- expect(assistant).toBeInstanceOf(Peer);
- expect(user.id).toBe('user');
- expect(assistant.id).toBe('assistant');
+ expect(user).toBeInstanceOf(Peer)
+ expect(assistant).toBeInstanceOf(Peer)
+ expect(user.id).toBe('user')
+ expect(assistant.id).toBe('assistant')
// Step 2: Create session
- const session = await honcho.session('chat-session');
- expect(session).toBeInstanceOf(Session);
- expect(session.id).toBe('chat-session');
+ const session = await honcho.session('chat-session')
+ expect(session).toBeInstanceOf(Session)
+ expect(session.id).toBe('chat-session')
// Step 3: Add peers to session
- await session.addPeers([user, assistant]);
- expect(mockWorkspacesApi.workspaces.sessions.peers.add).toHaveBeenCalledWith(
- 'integration-test-workspace',
- 'chat-session',
- {
- 'user': {},
- 'assistant': {}
- }
- );
+ await session.addPeers([user, assistant])
+ expect(
+ mockWorkspacesApi.workspaces.sessions.peers.add
+ ).toHaveBeenCalledWith('integration-test-workspace', 'chat-session', {
+ user: {},
+ assistant: {},
+ })
// Step 4: Add messages to session
- const userMessage = user.message('Hello');
- const assistantMessage = assistant.message('Hi there!');
+ const userMessage = user.message('Hello')
+ const assistantMessage = assistant.message('Hi there!')
- await session.addMessages([userMessage, assistantMessage]);
- expect(mockWorkspacesApi.workspaces.sessions.messages.create).toHaveBeenCalledWith(
- 'integration-test-workspace',
- 'chat-session',
- {
- messages: [
- { peer_id: 'user', content: 'Hello', metadata: undefined },
- { peer_id: 'assistant', content: 'Hi there!', metadata: undefined },
- ]
- }
- );
+ await session.addMessages([userMessage, assistantMessage])
+ expect(
+ mockWorkspacesApi.workspaces.sessions.messages.create
+ ).toHaveBeenCalledWith('integration-test-workspace', 'chat-session', {
+ messages: [
+ { peer_id: 'user', content: 'Hello', metadata: undefined },
+ { peer_id: 'assistant', content: 'Hi there!', metadata: undefined },
+ ],
+ })
// Step 5: Get session context
- const context = await session.getContext();
- expect(context).toBeInstanceOf(SessionContext);
- expect(context.sessionId).toBe('chat-session');
- expect(context.messages).toEqual(mockMessages);
- expect(context.summary).toBe('Friendly greeting');
+ const context = await session.getContext()
+ expect(context).toBeInstanceOf(SessionContext)
+ expect(context.sessionId).toBe('chat-session')
+ expect(context.messages).toEqual(mockMessages)
+ expect(context.summary?.content).toBe('Friendly greeting')
// Step 6: Convert context to different formats
- const openAIFormat = context.toOpenAI('assistant');
- const anthropicFormat = context.toAnthropic('assistant');
+ const openAIFormat = context.toOpenAI('assistant')
+ const anthropicFormat = context.toAnthropic('assistant')
expect(openAIFormat).toEqual([
{ role: 'system', content: 'Friendly greeting' },
{ role: 'user', content: 'Hello', name: 'user' },
{ role: 'assistant', content: 'Hi there!', name: 'assistant' },
- ]);
+ ])
expect(anthropicFormat).toEqual([
{ role: 'user', content: 'Friendly greeting' },
{ role: 'user', content: 'user: Hello' },
{ role: 'assistant', content: 'Hi there!' },
- ]);
+ ])
// Step 7: Query assistant
- const response = await assistant.chat('How are you?');
- expect(response).toBe('AI response');
+ const response = await assistant.chat('How are you?')
+ expect(response).toBe('AI response')
expect(mockWorkspacesApi.workspaces.peers.chat).toHaveBeenCalledWith(
'integration-test-workspace',
'assistant',
- { query: 'How are you?', stream: undefined, target: undefined, session_id: undefined }
- );
- });
+ {
+ query: 'How are you?',
+ stream: undefined,
+ target: undefined,
+ session_id: undefined,
+ }
+ )
+ })
it('should handle workspace and peer management workflow', async () => {
// Setup mock responses
- const mockWorkspaceMetadata = { name: 'Test Workspace', version: '1.0' };
+ const mockWorkspaceMetadata = { name: 'Test Workspace', version: '1.0' }
const mockPeersList = {
items: [
{ id: 'peer1', metadata: { role: 'user' } },
@@ -155,105 +183,115 @@ describe('Honcho SDK Integration Tests', () => {
total: 2,
size: 2,
hasNextPage: false,
- };
+ }
mockWorkspacesApi.workspaces.getOrCreate.mockResolvedValue({
id: 'integration-test-workspace',
metadata: mockWorkspaceMetadata,
- });
- mockWorkspacesApi.workspaces.update.mockResolvedValue({});
- mockWorkspacesApi.workspaces.peers.list.mockResolvedValue(mockPeersList);
+ })
+ mockWorkspacesApi.workspaces.update.mockResolvedValue({})
+ mockWorkspacesApi.workspaces.peers.list.mockResolvedValue(mockPeersList)
// Step 1: Get workspace metadata
- const metadata = await honcho.getMetadata();
- expect(metadata).toEqual(mockWorkspaceMetadata);
+ const metadata = await honcho.getMetadata()
+ expect(metadata).toEqual(mockWorkspaceMetadata)
// Step 2: Update workspace metadata
- const newMetadata = { ...mockWorkspaceMetadata, updated: true };
- await honcho.setMetadata(newMetadata);
+ const newMetadata = { ...mockWorkspaceMetadata, updated: true }
+ await honcho.setMetadata(newMetadata)
expect(mockWorkspacesApi.workspaces.update).toHaveBeenCalledWith(
'integration-test-workspace',
{ metadata: newMetadata }
- );
+ )
// Step 3: Get all peers
- const peersPage = await honcho.getPeers();
- expect(peersPage).toBeInstanceOf(Page);
+ const peersPage = await honcho.getPeers()
+ expect(peersPage).toBeInstanceOf(Page)
// Step 4: Iterate through peers
- const peersList: Peer[] = [];
+ const peersList: Peer[] = []
for await (const peer of peersPage) {
- peersList.push(peer);
+ peersList.push(peer)
}
- expect(peersList).toHaveLength(2);
- expect(peersList[0]).toBeInstanceOf(Peer);
- expect(peersList[1]).toBeInstanceOf(Peer);
- expect(peersList[0].id).toBe('peer1');
- expect(peersList[1].id).toBe('peer2');
- });
+ expect(peersList).toHaveLength(2)
+ expect(peersList[0]).toBeInstanceOf(Peer)
+ expect(peersList[1]).toBeInstanceOf(Peer)
+ expect(peersList[0].id).toBe('peer1')
+ expect(peersList[1].id).toBe('peer2')
+ })
it('should handle search functionality across different scopes', async () => {
// Setup mock responses
const mockWorkspaceSearchResults = [
{ id: 'msg1', content: 'workspace message', peer_id: 'peer1' },
- ];
+ ]
const mockPeerSearchResults = [
{ id: 'msg2', content: 'peer message', peer_id: 'peer1' },
- ];
+ ]
const mockSessionSearchResults = [
{ id: 'msg3', content: 'session message', peer_id: 'peer1' },
- ];
+ ]
- mockWorkspacesApi.workspaces.search.mockResolvedValue(mockWorkspaceSearchResults);
- mockWorkspacesApi.workspaces.peers.search.mockResolvedValue(mockPeerSearchResults);
- mockWorkspacesApi.workspaces.sessions.search.mockResolvedValue(mockSessionSearchResults);
+ mockWorkspacesApi.workspaces.search.mockResolvedValue(
+ mockWorkspaceSearchResults
+ )
+ mockWorkspacesApi.workspaces.peers.search.mockResolvedValue(
+ mockPeerSearchResults
+ )
+ mockWorkspacesApi.workspaces.sessions.search.mockResolvedValue(
+ mockSessionSearchResults
+ )
// Step 1: Search workspace
- const workspaceResults = await honcho.search('test query');
- expect(Array.isArray(workspaceResults)).toBe(true);
+ const workspaceResults = await honcho.search('test query')
+ expect(Array.isArray(workspaceResults)).toBe(true)
expect(mockWorkspacesApi.workspaces.search).toHaveBeenCalledWith(
'integration-test-workspace',
{ query: 'test query', limit: undefined }
- );
+ )
// Step 2: Search peer
- const peer = await honcho.peer('test-peer');
- const peerResults = await peer.search('peer query');
- expect(Array.isArray(peerResults)).toBe(true);
+ const peer = await honcho.peer('test-peer')
+ const peerResults = await peer.search('peer query')
+ expect(Array.isArray(peerResults)).toBe(true)
expect(mockWorkspacesApi.workspaces.peers.search).toHaveBeenCalledWith(
'integration-test-workspace',
'test-peer',
{ query: 'peer query', limit: undefined }
- );
+ )
// Step 3: Search session
- const session = await honcho.session('test-session');
- const sessionResults = await session.search('session query');
- expect(Array.isArray(sessionResults)).toBe(true);
+ const session = await honcho.session('test-session')
+ const sessionResults = await session.search('session query')
+ expect(Array.isArray(sessionResults)).toBe(true)
expect(mockWorkspacesApi.workspaces.sessions.search).toHaveBeenCalledWith(
'integration-test-workspace',
'test-session',
{ query: 'session query', limit: undefined }
- );
- });
+ )
+ })
it('should handle error scenarios gracefully', async () => {
// Setup error scenarios
- mockWorkspacesApi.workspaces.peers.chat.mockRejectedValue(new Error('Chat API failed'));
- mockWorkspacesApi.workspaces.sessions.getContext.mockRejectedValue(new Error('Context API failed'));
+ mockWorkspacesApi.workspaces.peers.chat.mockRejectedValue(
+ new Error('Chat API failed')
+ )
+ mockWorkspacesApi.workspaces.sessions.getContext.mockRejectedValue(
+ new Error('Context API failed')
+ )
- const assistant = await honcho.peer('assistant');
- const session = await honcho.session('error-session');
+ const assistant = await honcho.peer('assistant')
+ const session = await honcho.session('error-session')
// Test error handling in chat
- await expect(assistant.chat('Hello')).rejects.toThrow();
+ await expect(assistant.chat('Hello')).rejects.toThrow()
// Test error handling in context
- await expect(session.getContext()).rejects.toThrow();
- });
+ await expect(session.getContext()).rejects.toThrow()
+ })
it('should handle pagination correctly', async () => {
// Setup paginated response
@@ -266,7 +304,7 @@ describe('Honcho SDK Integration Tests', () => {
size: 2,
hasNextPage: true,
nextPage: jest.fn(),
- };
+ }
const secondPageData = {
items: [
@@ -277,34 +315,34 @@ describe('Honcho SDK Integration Tests', () => {
size: 2,
hasNextPage: false,
nextPage: jest.fn().mockResolvedValue(null),
- };
+ }
- firstPageData.nextPage.mockResolvedValue(secondPageData);
- mockWorkspacesApi.workspaces.peers.list.mockResolvedValue(firstPageData);
+ firstPageData.nextPage.mockResolvedValue(secondPageData)
+ mockWorkspacesApi.workspaces.peers.list.mockResolvedValue(firstPageData)
// Step 1: Get first page
- const firstPage = await honcho.getPeers();
- expect(firstPage.total).toBe(4);
- expect(firstPage.size).toBe(2);
- expect(firstPage.hasNextPage).toBe(true);
+ const firstPage = await honcho.getPeers()
+ expect(firstPage.total).toBe(4)
+ expect(firstPage.size).toBe(2)
+ expect(firstPage.hasNextPage).toBe(true)
// Step 2: Get data from first page
- const firstPageData_ = await firstPage.data();
- expect(firstPageData_).toHaveLength(2);
+ const firstPageData_ = await firstPage.data()
+ expect(firstPageData_).toHaveLength(2)
// Step 3: Get next page
- const secondPage = await firstPage.nextPage();
- expect(secondPage).not.toBeNull();
- expect(secondPage!.hasNextPage).toBe(false);
+ const secondPage = await firstPage.nextPage()
+ expect(secondPage).not.toBeNull()
+ expect(secondPage!.hasNextPage).toBe(false)
// Step 4: Get data from second page
- const secondPageData_ = await secondPage!.data();
- expect(secondPageData_).toHaveLength(2);
+ const secondPageData_ = await secondPage!.data()
+ expect(secondPageData_).toHaveLength(2)
// Step 5: Verify no more pages
- const thirdPage = await secondPage!.nextPage();
- expect(thirdPage).toBeNull();
- });
+ const thirdPage = await secondPage!.nextPage()
+ expect(thirdPage).toBeNull()
+ })
it('should handle working representation queries', async () => {
const mockWorkingRep = {
@@ -312,94 +350,115 @@ describe('Honcho SDK Integration Tests', () => {
knowledge: 'Alice likes coffee and works as a developer',
relationships: ['bob', 'charlie'],
context: 'session-specific context',
- };
+ }
- mockWorkspacesApi.workspaces.peers.workingRepresentation.mockResolvedValue(mockWorkingRep);
+ mockWorkspacesApi.workspaces.peers.workingRepresentation.mockResolvedValue(
+ mockWorkingRep
+ )
- const session = await honcho.session('working-rep-session');
- const alice = await honcho.peer('alice');
- const bob = await honcho.peer('bob');
+ const session = await honcho.session('working-rep-session')
+ const alice = await honcho.peer('alice')
+ const bob = await honcho.peer('bob')
// Test working representation without target
- const globalRep = await session.workingRep('alice');
- expect(globalRep).toEqual(mockWorkingRep);
- expect(mockWorkspacesApi.workspaces.peers.workingRepresentation).toHaveBeenCalledWith(
- 'integration-test-workspace',
- 'alice',
- { session_id: 'working-rep-session', target: undefined }
- );
+ const globalRep = await session.workingRep('alice')
+ expect(globalRep).toEqual(mockWorkingRep)
+ expect(
+ mockWorkspacesApi.workspaces.peers.workingRepresentation
+ ).toHaveBeenCalledWith('integration-test-workspace', 'alice', {
+ session_id: 'working-rep-session',
+ target: undefined,
+ })
// Test working representation with target
- await session.workingRep(alice, bob);
- expect(mockWorkspacesApi.workspaces.peers.workingRepresentation).toHaveBeenCalledWith(
- 'integration-test-workspace',
- 'alice',
- { session_id: 'working-rep-session', target: 'bob' }
- );
- });
- });
+ await session.workingRep(alice, bob)
+ expect(
+ mockWorkspacesApi.workspaces.peers.workingRepresentation
+ ).toHaveBeenCalledWith('integration-test-workspace', 'alice', {
+ session_id: 'working-rep-session',
+ target: 'bob',
+ })
+ })
+ })
describe('Edge Cases and Error Handling Integration', () => {
it('should handle empty and null responses gracefully', async () => {
// Setup empty/null responses
- mockWorkspacesApi.workspaces.peers.chat.mockResolvedValue({ content: null });
- mockWorkspacesApi.workspaces.peers.list.mockResolvedValue({ items: [], total: 0, hasNextPage: false });
- mockWorkspacesApi.workspaces.sessions.getContext.mockResolvedValue({ messages: [] });
+ mockWorkspacesApi.workspaces.peers.chat.mockResolvedValue({
+ content: null,
+ })
+ mockWorkspacesApi.workspaces.peers.list.mockResolvedValue({
+ items: [],
+ total: 0,
+ hasNextPage: false,
+ })
+ mockWorkspacesApi.workspaces.sessions.getContext.mockResolvedValue({
+ messages: [],
+ })
- const peer = await honcho.peer('empty-peer');
- const session = await honcho.session('empty-session');
+ const peer = await honcho.peer('empty-peer')
+ const session = await honcho.session('empty-session')
// Test null chat response
- const chatResult = await peer.chat('Hello');
- expect(chatResult).toBeNull();
+ const chatResult = await peer.chat('Hello')
+ expect(chatResult).toBeNull()
// Test empty peers list
- const peersPage = await honcho.getPeers();
- const peersList = await peersPage.data();
- expect(peersList).toEqual([]);
+ const peersPage = await honcho.getPeers()
+ const peersList = await peersPage.data()
+ expect(peersList).toEqual([])
// Test empty context
- const context = await session.getContext();
- expect(context.messages).toEqual([]);
- expect(context.length).toBe(0);
- });
+ const context = await session.getContext()
+ expect(context.messages).toEqual([])
+ expect(context.length).toBe(0)
+ })
it('should maintain type safety throughout the workflow', async () => {
// This test verifies TypeScript types are maintained correctly
- const peer: Peer = await honcho.peer('typed-peer');
- const session: Session = await honcho.session('typed-session');
+ const peer: Peer = await honcho.peer('typed-peer')
+ const session: Session = await honcho.session('typed-session')
- expect(typeof peer.id).toBe('string');
- expect(typeof session.id).toBe('string');
+ expect(typeof peer.id).toBe('string')
+ expect(typeof session.id).toBe('string')
- const message = peer.message('typed message', { metadata: { type: 'test' } });
- expect(typeof message.peer_id).toBe('string');
- expect(typeof message.content).toBe('string');
- expect(typeof message.metadata).toBe('object');
+ const message = peer.message('typed message', {
+ metadata: { type: 'test' },
+ })
+ expect(typeof message.peer_id).toBe('string')
+ expect(typeof message.content).toBe('string')
+ expect(typeof message.metadata).toBe('object')
// Mock successful operations
mockWorkspacesApi.workspaces.sessions.getContext.mockResolvedValue({
messages: [{ id: 'msg1', content: 'Hello', peer_id: 'typed-peer' }],
- summary: 'Test summary',
- });
+ summary: {
+ content: 'Test summary',
+ message_id: 1,
+ summary_type: 'short',
+ created_at: '2024-01-01T00:00:00Z',
+ token_count: 20,
+ },
+ })
- const context: SessionContext = await session.getContext();
- expect(typeof context.sessionId).toBe('string');
- expect(Array.isArray(context.messages)).toBe(true);
- expect(typeof context.summary).toBe('string');
- expect(typeof context.length).toBe('number');
- expect(typeof context.toString()).toBe('string');
+ const context: SessionContext = await session.getContext()
+ expect(typeof context.sessionId).toBe('string')
+ expect(Array.isArray(context.messages)).toBe(true)
+ expect(context.summary).not.toBeNull()
+ expect(typeof context.summary?.content).toBe('string')
+ expect(typeof context.length).toBe('number')
+ expect(typeof context.toString()).toBe('string')
- const openAI = context.toOpenAI(peer);
- const anthropic = context.toAnthropic('assistant');
+ const openAI = context.toOpenAI(peer)
+ const anthropic = context.toAnthropic('assistant')
- expect(Array.isArray(openAI)).toBe(true);
- expect(Array.isArray(anthropic)).toBe(true);
+ expect(Array.isArray(openAI)).toBe(true)
+ expect(Array.isArray(anthropic)).toBe(true)
if (openAI.length > 0) {
- expect(typeof openAI[0].role).toBe('string');
- expect(typeof openAI[0].content).toBe('string');
+ expect(typeof openAI[0].role).toBe('string')
+ expect(typeof openAI[0].content).toBe('string')
}
- });
- });
-});
+ })
+ })
+})
diff --git a/sdks/typescript/__tests__/session.test.ts b/sdks/typescript/__tests__/session.test.ts
index c95f4445..1d0d2d9e 100644
--- a/sdks/typescript/__tests__/session.test.ts
+++ b/sdks/typescript/__tests__/session.test.ts
@@ -455,6 +455,107 @@ describe('Session', () => {
await expect(session.addMessages({ peer_id: 'peer1', content: 'test' })).rejects.toThrow();
});
+
+ it('should add message with custom timestamp', async () => {
+ const message = {
+ peer_id: 'peer1',
+ content: 'Message with timestamp',
+ created_at: '2023-01-01T12:00:00Z',
+ metadata: { test: 'timestamp' },
+ };
+ mockClient.workspaces.sessions.messages.create.mockResolvedValue({});
+
+ await session.addMessages(message);
+
+ 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' }
+ }]
+ }
+ );
+ });
+
+ it('should add message with null timestamp', async () => {
+ const message = {
+ peer_id: 'peer1',
+ content: 'Message without timestamp',
+ created_at: null,
+ metadata: { test: 'no_timestamp' },
+ };
+ mockClient.workspaces.sessions.messages.create.mockResolvedValue({});
+
+ await session.addMessages(message);
+
+ 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' }
+ }]
+ }
+ );
+ });
+
+ it('should add mixed messages with and without timestamps', async () => {
+ const 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' }
+ }
+ ];
+ mockClient.workspaces.sessions.messages.create.mockResolvedValue({});
+
+ 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' }
+ }
+ ]
+ }
+ );
+ });
});
describe('getMessages', () => {
@@ -582,7 +683,13 @@ describe('Session', () => {
{ id: 'msg1', content: 'Hello', peer_id: 'peer1' },
{ id: 'msg2', content: 'Hi there', peer_id: 'peer2' },
],
- summary: 'Conversation summary',
+ summary: {
+ content: 'Conversation summary',
+ message_id: 10,
+ summary_type: 'short',
+ created_at: '2024-01-01T00:00:00Z',
+ token_count: 100
+ },
};
mockClient.workspaces.sessions.getContext.mockResolvedValue(mockContext);
@@ -591,7 +698,7 @@ describe('Session', () => {
expect(context).toBeInstanceOf(SessionContext);
expect(context.sessionId).toBe('test-session');
expect(context.messages).toEqual(mockContext.messages);
- expect(context.summary).toBe('Conversation summary');
+ expect(context.summary?.content).toBe('Conversation summary');
expect(mockClient.workspaces.sessions.getContext).toHaveBeenCalledWith(
'test-workspace',
'test-session',
@@ -602,7 +709,13 @@ describe('Session', () => {
it('should get session context with options', async () => {
const mockContext = {
messages: [{ id: 'msg1', content: 'Hello', peer_id: 'peer1' }],
- summary: 'Brief summary',
+ summary: {
+ content: 'Brief summary',
+ message_id: 5,
+ summary_type: 'short',
+ created_at: '2024-01-01T00:00:00Z',
+ token_count: 50
+ },
};
mockClient.workspaces.sessions.getContext.mockResolvedValue(mockContext);
@@ -624,7 +737,7 @@ describe('Session', () => {
const context = await session.getContext();
- expect(context.summary).toBe('');
+ expect(context.summary).toBeNull();
});
it('should handle API errors', async () => {
diff --git a/sdks/typescript/__tests__/session_context.test.ts b/sdks/typescript/__tests__/session_context.test.ts
index 2af28b4d..9c95dbce 100644
--- a/sdks/typescript/__tests__/session_context.test.ts
+++ b/sdks/typescript/__tests__/session_context.test.ts
@@ -1,4 +1,4 @@
-import { SessionContext } from '../src/session_context';
+import { SessionContext, Summary } from '../src/session_context';
import { Peer } from '../src/peer';
/**
@@ -17,6 +17,19 @@ function createTestMessage(id: string, content: string, peer_id: string, additio
};
}
+/**
+ * Helper function to create a test Summary object
+ */
+function createTestSummary(content: string): Summary {
+ return new Summary({
+ content,
+ message_id: 1,
+ summary_type: 'short',
+ created_at: new Date().toISOString(),
+ token_count: content.length
+ });
+}
+
describe('SessionContext', () => {
let sessionContext: SessionContext;
let mockMessages: any[];
@@ -29,38 +42,39 @@ describe('SessionContext', () => {
createTestMessage('msg4', 'I am doing well, thank you!', 'assistant'),
];
- sessionContext = new SessionContext('test-session', mockMessages, '');
+ sessionContext = new SessionContext('test-session', mockMessages, null);
});
describe('constructor', () => {
it('should initialize with all properties', () => {
expect(sessionContext.sessionId).toBe('test-session');
expect(sessionContext.messages).toEqual(mockMessages);
- expect(sessionContext.summary).toBe('');
+ expect(sessionContext.summary).toBe(null);
});
- it('should initialize with empty summary when not provided', () => {
+ it('should initialize with null summary when not provided', () => {
const context = new SessionContext('session-id', mockMessages);
expect(context.sessionId).toBe('session-id');
expect(context.messages).toEqual(mockMessages);
- expect(context.summary).toBe('');
+ expect(context.summary).toBe(null);
});
it('should handle empty messages array', () => {
- const context = new SessionContext('session-id', [], 'No messages');
+ const summary = createTestSummary('No messages');
+ const context = new SessionContext('session-id', [], summary);
expect(context.sessionId).toBe('session-id');
expect(context.messages).toEqual([]);
- expect(context.summary).toBe('No messages');
+ expect(context.summary).toBe(summary);
});
it('should handle null/undefined summary', () => {
const context1 = new SessionContext('session-id', mockMessages, undefined as any);
- const context2 = new SessionContext('session-id', mockMessages, null as any);
+ const context2 = new SessionContext('session-id', mockMessages, null);
- expect(context1.summary).toBe('');
- expect(context2.summary).toBe('');
+ expect(context1.summary).toBe(null);
+ expect(context2.summary).toBe(null);
});
});
@@ -109,7 +123,8 @@ describe('SessionContext', () => {
});
it('should include summary message when summary exists', () => {
- const contextWithSummary = new SessionContext('test-session', mockMessages, 'This is a summary');
+ const summary = createTestSummary('This is a summary');
+ const contextWithSummary = new SessionContext('test-session', mockMessages, summary);
const openAIMessages = contextWithSummary.toOpenAI('assistant');
expect(openAIMessages).toEqual([
@@ -201,7 +216,8 @@ describe('SessionContext', () => {
});
it('should include summary message when summary exists', () => {
- const contextWithSummary = new SessionContext('test-session', mockMessages, 'This is a summary');
+ const summary = createTestSummary('This is a summary');
+ const contextWithSummary = new SessionContext('test-session', mockMessages, summary);
const anthropicMessages = contextWithSummary.toAnthropic('assistant');
expect(anthropicMessages).toEqual([
@@ -250,13 +266,13 @@ describe('SessionContext', () => {
describe('toString', () => {
it('should return correct string representation', () => {
const result = sessionContext.toString();
- expect(result).toBe('SessionContext(messages=4, summary=)');
+ expect(result).toBe('SessionContext(messages=4, summary=none)');
});
it('should handle empty messages', () => {
const emptyContext = new SessionContext('session-id', []);
const result = emptyContext.toString();
- expect(result).toBe('SessionContext(messages=0, summary=)');
+ expect(result).toBe('SessionContext(messages=0, summary=none)');
});
it('should handle large number of messages', () => {
@@ -266,7 +282,7 @@ describe('SessionContext', () => {
const context = new SessionContext('session-id', manyMessages);
const result = context.toString();
- expect(result).toBe('SessionContext(messages=1000, summary=)');
+ expect(result).toBe('SessionContext(messages=1000, summary=none)');
});
});
@@ -305,11 +321,13 @@ describe('SessionContext', () => {
it('should handle very long session IDs and summaries', () => {
const longSessionId = 'x'.repeat(1000);
- const longSummary = 'Very long summary that goes on and on...'.repeat(100);
+ const longSummaryText = 'Very long summary that goes on and on...'.repeat(100);
+ const longSummary = createTestSummary(longSummaryText);
const context = new SessionContext(longSessionId, mockMessages, longSummary);
expect(context.sessionId).toBe(longSessionId);
expect(context.summary).toBe(longSummary);
+ expect(context.summary?.content).toBe(longSummaryText);
expect(context.length).toBe(5); // 4 messages + 1 summary
});
diff --git a/sdks/typescript/bun.lock b/sdks/typescript/bun.lock
index cccce038..c6ea15db 100644
--- a/sdks/typescript/bun.lock
+++ b/sdks/typescript/bun.lock
@@ -4,7 +4,7 @@
"": {
"name": "@honcho-ai/sdk",
"dependencies": {
- "@honcho-ai/core": "1.3.0",
+ "@honcho-ai/core": "^1.4.0",
"@types/node": "^24.0.1",
"zod": "4.0.0",
},
@@ -108,7 +108,7 @@
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.1.3", "", { "os": "win32", "cpu": "x64" }, "sha512-dxy599q6lgp8ANPpR8sDMscwdp9oOumEsVXuVCVT9N2vAho8uYXlCz53JhxX6LtJOXaE73qzgkGQ7QqvFlMC0g=="],
- "@honcho-ai/core": ["@honcho-ai/core@1.3.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-yhxpZ00sQhO24PrHoTlj1cwR1q7/8FKSFZoCImqsFFmdERiXtE74YXhhfqzseyuShu1WfM7IwD8T2oCOB3hu7A=="],
+ "@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=="],
"@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=="],
@@ -644,7 +644,7 @@
"@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.121", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-bHOrbyztmyYIi4f1R0s17QsPs1uyyYnGcXeZoGEd227oZjry0q6XQBQxd82X1I57zEfwO8h9Xo+Kl5gX1d9MwQ=="],
+ "@honcho-ai/core/@types/node": ["@types/node@18.19.122", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-yzegtT82dwTNEe/9y+CM8cgb42WrUfMMCg2QqSddzO1J6uPmBD7qKCZ7dOHZP2Yrpm/kb0eqdNMn2MUyEiqBmA=="],
"@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
diff --git a/sdks/typescript/examples/get_summaries.ts b/sdks/typescript/examples/get_summaries.ts
new file mode 100644
index 00000000..15bcd4a9
--- /dev/null
+++ b/sdks/typescript/examples/get_summaries.ts
@@ -0,0 +1,104 @@
+/**
+ * Example of getting session summaries using the Honcho TypeScript SDK.
+ *
+ * This example demonstrates how to retrieve both short and long summaries
+ * for a session, including their metadata like message ID, creation timestamp,
+ * and token count.
+ */
+
+import { Honcho, type SessionSummaries, type Summary } from "../src";
+
+async function main() {
+ // Initialize the Honcho client
+ const apiKey = process.env.HONCHO_API_KEY;
+ if (!apiKey) {
+ throw new Error("Please set HONCHO_API_KEY environment variable");
+ }
+
+ const client = new Honcho({ apiKey });
+
+ // Get a session (replace with your actual session ID)
+ const session = await client.session("my-conversation-session");
+
+ // Get summaries for the session
+ const summaries: SessionSummaries = await session.getSummaries();
+
+ console.log(`Session ID: ${summaries.id}`);
+ console.log("-".repeat(50));
+
+ // Check and display short summary
+ if (summaries.shortSummary) {
+ console.log("SHORT SUMMARY:");
+ console.log(
+ ` Content: ${summaries.shortSummary.content.slice(0, 200)}...`,
+ ); // First 200 chars
+ console.log(
+ ` Covers up to message ID: ${summaries.shortSummary.messageId}`,
+ );
+ console.log(` Created at: ${summaries.shortSummary.createdAt}`);
+ console.log(` Token count: ${summaries.shortSummary.tokenCount}`);
+ console.log(` Type: ${summaries.shortSummary.summaryType}`);
+ } else {
+ console.log("No short summary available yet");
+ }
+
+ console.log("-".repeat(50));
+
+ // Check and display long summary
+ if (summaries.longSummary) {
+ console.log("LONG SUMMARY:");
+ console.log(` Content: ${summaries.longSummary.content.slice(0, 200)}...`); // First 200 chars
+ console.log(
+ ` Covers up to message ID: ${summaries.longSummary.messageId}`,
+ );
+ console.log(` Created at: ${summaries.longSummary.createdAt}`);
+ console.log(` Token count: ${summaries.longSummary.tokenCount}`);
+ console.log(` Type: ${summaries.longSummary.summaryType}`);
+ } else {
+ console.log("No long summary available yet");
+ }
+
+ // Example showing how to use summaries in application logic
+ console.log("\n" + "=".repeat(50));
+ console.log("USAGE EXAMPLE:");
+ console.log("=".repeat(50));
+
+ // Check if we have any summaries
+ if (summaries.shortSummary || summaries.longSummary) {
+ // Prefer long summary if available and under token limit
+ const MAX_TOKENS = 2000;
+ let selectedSummary: Summary | null = null;
+
+ if (
+ summaries.longSummary &&
+ summaries.longSummary.tokenCount <= MAX_TOKENS
+ ) {
+ selectedSummary = summaries.longSummary;
+ console.log("Using long summary (within token limit)");
+ } else if (summaries.shortSummary) {
+ selectedSummary = summaries.shortSummary;
+ console.log("Using short summary");
+ }
+
+ if (selectedSummary) {
+ console.log(
+ `Selected summary covers up to message ${selectedSummary.messageId}`,
+ );
+ console.log(`Token count: ${selectedSummary.tokenCount}`);
+
+ // You could use this summary as context for an LLM call
+ // For example, with OpenAI:
+ // const systemPrompt = `Previous conversation summary: ${selectedSummary.content}`
+ }
+ } else {
+ console.log(
+ "No summaries available - would need to use full message history",
+ );
+ }
+}
+
+// Run the example
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json
index 032c227a..016d8bc6 100644
--- a/sdks/typescript/package.json
+++ b/sdks/typescript/package.json
@@ -1,6 +1,6 @@
{
"name": "@honcho-ai/sdk",
- "version": "1.3.0",
+ "version": "1.4.0",
"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.3.0",
+ "@honcho-ai/core": "^1.4.0",
"@types/node": "^24.0.1",
"zod": "4.0.0"
},
diff --git a/sdks/typescript/src/index.ts b/sdks/typescript/src/index.ts
index f4f1da9d..3aed433b 100644
--- a/sdks/typescript/src/index.ts
+++ b/sdks/typescript/src/index.ts
@@ -6,7 +6,12 @@ export { Honcho } from './client'
export { Page } from './pagination'
export { Peer } from './peer'
export { Session, SessionPeerConfig } from './session'
-export { SessionContext } from './session_context'
+export {
+ SessionContext,
+ SessionSummaries,
+ Summary,
+ SummaryData,
+} from './session_context'
// Export validation types for advanced usage
export type {
diff --git a/sdks/typescript/src/peer.ts b/sdks/typescript/src/peer.ts
index 4769dbb5..360a98fa 100644
--- a/sdks/typescript/src/peer.ts
+++ b/sdks/typescript/src/peer.ts
@@ -131,21 +131,31 @@ export class Peer {
*
* @param content - The text content for the message
* @param metadata - Optional metadata to associate with the message
+ * @param created_at - Optional ISO 8601 timestamp for the message
* @returns A new message object with this peer's ID and the provided content
*/
message(
content: string,
- options?: { metadata?: Record }
+ options?: {
+ metadata?: Record
+ created_at?: string | Date
+ }
): ValidatedMessageCreate {
const validatedContent = MessageContentSchema.parse(content)
const validatedMetadata = options?.metadata
? MessageMetadataSchema.parse(options.metadata)
: undefined
+ const createdAt =
+ options?.created_at instanceof Date
+ ? options.created_at.toISOString()
+ : options?.created_at
+
return {
peer_id: this.id,
content: validatedContent,
metadata: validatedMetadata,
+ created_at: createdAt,
}
}
diff --git a/sdks/typescript/src/session.ts b/sdks/typescript/src/session.ts
index 7d7c8262..7f39e09e 100644
--- a/sdks/typescript/src/session.ts
+++ b/sdks/typescript/src/session.ts
@@ -3,7 +3,7 @@ import type { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/
import type { Uploadable } from '@honcho-ai/core/src/uploads'
import { Page } from './pagination'
import { Peer } from './peer'
-import { SessionContext } from './session_context'
+import { SessionContext, SessionSummaries, Summary } from './session_context'
import {
ContextParamsSchema,
FileUploadSchema,
@@ -350,6 +350,12 @@ export class Session {
* { peer_id: 'user1', content: 'Hello!' },
* { peer_id: 'assistant', content: 'Hi there!' }
* ])
+ * // Add message with custom ISO 8601 timestamp
+ * await session.addMessages({
+ * peer_id: 'user123',
+ * content: 'Hello world!',
+ * created_at: '2021-01-01T00:00:00.000Z'
+ * })
* ```
*/
async addMessages(messages: MessageAddition): Promise {
@@ -457,7 +463,37 @@ export class Session {
summary: contextParams.summary,
}
)
- return new SessionContext(this.id, context.messages, context.summary)
+ // Convert the summary response to Summary object if present
+ const summary = context.summary ? new Summary(context.summary) : null
+ return new SessionContext(this.id, context.messages, summary)
+ }
+
+ /**
+ * Get available summaries for this session.
+ *
+ * Makes an API call to retrieve both short and long summaries for this session,
+ * if they are available. Summaries are created asynchronously by the backend
+ * as messages are added to the session.
+ *
+ * @returns Promise resolving to a SessionSummaries object containing:
+ * - id: The session ID
+ * - shortSummary: The short summary if available, including metadata
+ * - longSummary: The long summary if available, including metadata
+ *
+ * @note Summaries may be null if:
+ * - Not enough messages have been added to trigger summary generation
+ * - The summary generation is still in progress
+ * - Summary generation is disabled for this session
+ */
+ async getSummaries(): Promise {
+ // Use the core SDK's summaries method
+ const data = await this._client.workspaces.sessions.summaries(
+ this.workspaceId,
+ this.id
+ )
+
+ // Return a SessionSummaries instance
+ return new SessionSummaries(data)
}
/**
diff --git a/sdks/typescript/src/session_context.ts b/sdks/typescript/src/session_context.ts
index 958c538c..80df6539 100644
--- a/sdks/typescript/src/session_context.ts
+++ b/sdks/typescript/src/session_context.ts
@@ -1,6 +1,84 @@
import type { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
import type { Peer } from './peer'
+export interface SummaryData {
+ content: string
+ message_id: number
+ summary_type: string
+ created_at: string
+ token_count: number
+}
+
+/**
+ * Represents a summary of a session's conversation.
+ */
+export class Summary {
+ /**
+ * The summary text.
+ */
+ readonly content: string
+
+ /**
+ * The ID of the message that this summary covers up to.
+ */
+ readonly messageId: number
+
+ /**
+ * The type of summary (short or long).
+ */
+ readonly summaryType: string
+
+ /**
+ * The timestamp of when the summary was created (ISO format).
+ */
+ readonly createdAt: string
+
+ /**
+ * The number of tokens in the summary text.
+ */
+ readonly tokenCount: number
+
+ constructor(data: SummaryData) {
+ this.content = data.content
+ this.messageId = data.message_id
+ this.summaryType = data.summary_type
+ this.createdAt = data.created_at
+ this.tokenCount = data.token_count
+ }
+}
+
+/**
+ * Contains both short and long summaries for a session.
+ */
+export class SessionSummaries {
+ /**
+ * The session ID.
+ */
+ readonly id: string
+
+ /**
+ * The short summary if available.
+ */
+ readonly shortSummary: Summary | null
+
+ /**
+ * The long summary if available.
+ */
+ readonly longSummary: Summary | null
+
+ constructor(data: {
+ id: string
+ short_summary?: SummaryData | null
+ long_summary?: SummaryData | null
+ }) {
+ this.id = data.id
+ this.shortSummary = data.short_summary
+ ? new Summary(data.short_summary)
+ : null
+ this.longSummary = data.long_summary ? new Summary(data.long_summary) : null
+ }
+}
+
/**
* Represents the context of a session containing a curated list of messages.
*
@@ -22,7 +100,7 @@ export class SessionContext {
/**
* Summary of the session history prior to the message cutoff.
*/
- readonly summary: string
+ readonly summary: Summary | null
/**
* Initialize a new SessionContext.
@@ -31,10 +109,14 @@ export class SessionContext {
* @param messages List of Message objects to include in the context
* @param summary Summary of the session history prior to the message cutoff
*/
- constructor(sessionId: string, messages: Message[], summary: string = '') {
+ constructor(
+ sessionId: string,
+ messages: Message[],
+ summary: Summary | null = null
+ ) {
this.sessionId = sessionId
this.messages = messages
- this.summary = summary || ''
+ this.summary = summary
}
/**
@@ -54,16 +136,18 @@ export class SessionContext {
assistant: string | Peer
): Array<{ role: string; content: string; name?: string }> {
const assistantId = typeof assistant === 'string' ? assistant : assistant.id
- const summaryMessage = {
- role: 'system',
- content: `${this.summary}`,
- }
+ const summaryMessage = this.summary
+ ? {
+ role: 'system',
+ content: `${this.summary.content}`,
+ }
+ : null
const messages = this.messages.map((message) => ({
role: message.peer_id === assistantId ? 'assistant' : 'user',
name: message.peer_id,
content: message.content,
}))
- return this.summary ? [summaryMessage, ...messages] : messages
+ return summaryMessage ? [summaryMessage, ...messages] : messages
}
/**
@@ -86,10 +170,12 @@ export class SessionContext {
assistant: string | Peer
): Array<{ role: string; content: string }> {
const assistantId = typeof assistant === 'string' ? assistant : assistant.id
- const summaryMessage = {
- role: 'user',
- content: `${this.summary}`,
- }
+ const summaryMessage = this.summary
+ ? {
+ role: 'user',
+ content: `${this.summary.content}`,
+ }
+ : null
const messages = this.messages.map((message) =>
message.peer_id === assistantId
? {
@@ -101,20 +187,20 @@ export class SessionContext {
content: `${message.peer_id}: ${message.content}`,
}
)
- return this.summary ? [summaryMessage, ...messages] : messages
+ return summaryMessage ? [summaryMessage, ...messages] : messages
}
/**
* Return the number of messages in the context.
*/
get length(): number {
- return this.messages.length + (this.summary.length > 0 ? 1 : 0)
+ return this.messages.length + (this.summary ? 1 : 0)
}
/**
* Return a string representation of the SessionContext.
*/
toString(): string {
- return `SessionContext(messages=${this.messages.length}, summary=${this.summary})`
+ return `SessionContext(messages=${this.messages.length}, summary=${this.summary ? 'present' : 'none'})`
}
}
diff --git a/sdks/typescript/src/validation.ts b/sdks/typescript/src/validation.ts
index c9e40535..f8fae934 100644
--- a/sdks/typescript/src/validation.ts
+++ b/sdks/typescript/src/validation.ts
@@ -94,6 +94,7 @@ export const MessageCreateSchema = z.object({
peer_id: PeerIdSchema,
content: MessageContentSchema,
metadata: MessageMetadataSchema,
+ created_at: z.string().nullable().optional(),
})
/**
diff --git a/src/crud/message.py b/src/crud/message.py
index 4b37972b..707d6edb 100644
--- a/src/crud/message.py
+++ b/src/crud/message.py
@@ -89,6 +89,7 @@ async def create_messages(
workspace_name=workspace_name,
public_id=generate_nanoid(),
token_count=len(message.encoded_message),
+ created_at=message.created_at, # Use provided created_at if available
)
message_objects.append(message_obj)
diff --git a/src/deriver/deriver.py b/src/deriver/deriver.py
index 73597457..c850da52 100644
--- a/src/deriver/deriver.py
+++ b/src/deriver/deriver.py
@@ -19,6 +19,7 @@ from src.utils.formatting import (
find_new_observations,
format_context_for_prompt,
format_new_turn_with_timestamp,
+ utc_now_iso,
)
from src.utils.logging import (
accumulate_metric,
@@ -630,7 +631,7 @@ async def save_working_representation_to_peer(
working_rep_data = {
"final_observations": final_obs_dict,
"message_id": message_id,
- "created_at": datetime.datetime.now().isoformat(),
+ "created_at": utc_now_iso(),
}
# if session_name is supplied, save working representation to session peer
diff --git a/src/dialectic/chat.py b/src/dialectic/chat.py
index f0880856..c03450cf 100644
--- a/src/dialectic/chat.py
+++ b/src/dialectic/chat.py
@@ -18,7 +18,7 @@ from mirascope.llm import Stream
from src import crud
from src.config import settings
from src.dependencies import tracked_db
-from src.routers.sessions import get_session_context
+from src.utils import summarizer
from src.utils.clients import honcho_llm_call
from src.utils.embedding_store import EmbeddingStore
from src.utils.logging import (
@@ -262,25 +262,16 @@ async def chat(
# If query is session-scoped, get recent conversation history from that session
if session_name:
async with tracked_db("chat.get_session_context") as db:
- session_context = await get_session_context(
- workspace_id=workspace_name,
- session_id=session_name,
- tokens=context_window_size,
- summary=True,
- db=db,
+ recent_conversation_history = (
+ await summarizer.get_session_context_formatted(
+ db,
+ workspace_name=workspace_name,
+ session_name=session_name,
+ token_limit=context_window_size,
+ include_summary=True,
+ )
)
- logger.info(
- "Retrieved recent conversation history with %s messages",
- len(session_context.messages),
- )
- recent_conversation_history = f"""
-
- {session_context.summary}
-
-
- {session_context.messages}
-
- """
+ logger.info("Retrieved recent conversation history")
else:
recent_conversation_history = None
logger.info("Query is not session-scoped, skipping recent conversation history")
diff --git a/src/dialectic/utils.py b/src/dialectic/utils.py
index f190a5c3..8f9cdceb 100644
--- a/src/dialectic/utils.py
+++ b/src/dialectic/utils.py
@@ -11,6 +11,7 @@ from src.utils.clients import honcho_llm_call
from src.utils.embedding_store import EmbeddingStore
from src.utils.formatting import (
format_premises_for_display,
+ parse_datetime_iso,
)
from src.utils.logging import conditional_observe
from src.utils.shared_models import SemanticQueries
@@ -190,11 +191,9 @@ def _format_observations(
if last_accessed:
# Format the last_accessed datetime for display
try:
- from datetime import datetime
-
if isinstance(last_accessed, str):
# Parse ISO format datetime string
- dt = datetime.fromisoformat(last_accessed.replace("Z", "+00:00"))
+ dt = parse_datetime_iso(last_accessed)
formatted_last_accessed: str = dt.strftime("%Y-%m-%d %H:%M")
access_parts.append(f"last accessed {formatted_last_accessed}")
except (ValueError, AttributeError):
diff --git a/src/main.py b/src/main.py
index 44b2fa39..0d3d5ab0 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.2.0",
+ version="2.3.0",
contact={
"name": "Plastic Labs",
"url": "https://honcho.dev",
diff --git a/src/routers/keys.py b/src/routers/keys.py
index becd4355..f4a50375 100644
--- a/src/routers/keys.py
+++ b/src/routers/keys.py
@@ -10,6 +10,7 @@ from src.security import (
create_jwt,
require_auth,
)
+from src.utils.formatting import format_datetime_utc
logger = logging.getLogger(__name__)
@@ -43,7 +44,7 @@ async def create_key(
key_str = create_jwt(
JWTParams(
- exp=expires_at.isoformat() if expires_at else None,
+ exp=format_datetime_utc(expires_at) if expires_at else None,
w=workspace_id,
p=peer_id,
s=session_id,
diff --git a/src/routers/sessions.py b/src/routers/sessions.py
index e8fbbf29..b86a8481 100644
--- a/src/routers/sessions.py
+++ b/src/routers/sessions.py
@@ -374,9 +374,11 @@ async def get_session_context(
le=config.settings.GET_CONTEXT_MAX_TOKENS,
description=f"Number of tokens to use for the context. Includes summary if set to true. If not provided, the context will be exhaustive (within {config.settings.GET_CONTEXT_MAX_TOKENS} tokens)",
),
- summary: bool = Query(
- True,
+ *,
+ include_summary: bool = Query(
+ default=True,
description="Whether or not to include a summary *if* one is available for the session",
+ alias="summary",
),
db: AsyncSession = db,
):
@@ -389,18 +391,59 @@ async def get_session_context(
token_limit = tokens or config.settings.GET_CONTEXT_MAX_TOKENS
# Use the shared get_session_context function from summarizer
- summary_content, messages = await summarizer.get_session_context(
+ summary_obj, messages = await summarizer.get_session_context(
db,
workspace_name=workspace_id,
session_name=session_id,
token_limit=token_limit,
- include_summary=summary,
+ include_summary=include_summary,
)
return schemas.SessionContext(
name=session_id,
messages=messages, # pyright: ignore -- db message type and schema message type are different, but excess gets removed by schema
- summary=summary_content,
+ summary=summary_obj,
+ )
+
+
+@router.get(
+ "/{session_id}/summaries",
+ response_model=schemas.SessionSummaries,
+ dependencies=[
+ Depends(require_auth(workspace_name="workspace_id", session_name="session_id"))
+ ],
+)
+async def get_session_summaries(
+ workspace_id: str = Path(..., description="ID of the workspace"),
+ session_id: str = Path(..., description="ID of the session"),
+ db: AsyncSession = db,
+) -> schemas.SessionSummaries:
+ """
+ Get available summaries for a session.
+
+ Returns both short and long summaries if available, including metadata like
+ the message ID they cover up to, creation timestamp, and token count.
+ """
+ # Reuse the logic from get_context to get both summaries
+ short_summary, long_summary = await summarizer.get_both_summaries(
+ db,
+ workspace_name=workspace_id,
+ session_name=session_id,
+ )
+
+ # Convert the internal Summary TypedDict to our Pydantic schema
+ short_summary_schema = None
+ if short_summary:
+ short_summary_schema = summarizer.to_schema_summary(short_summary)
+
+ long_summary_schema = None
+ if long_summary:
+ long_summary_schema = summarizer.to_schema_summary(long_summary)
+
+ return schemas.SessionSummaries(
+ name=session_id,
+ short_summary=short_summary_schema,
+ long_summary=long_summary_schema,
)
diff --git a/src/schemas.py b/src/schemas.py
index 60a5a5bd..b93c3552 100644
--- a/src/schemas.py
+++ b/src/schemas.py
@@ -117,6 +117,7 @@ class MessageCreate(MessageBase):
content: Annotated[str, Field(min_length=0, max_length=50000)]
peer_name: str = Field(alias="peer_id")
metadata: dict[str, Any] | None = None
+ created_at: datetime.datetime | None = None
_encoded_message: list[int] = PrivateAttr(default=[])
@@ -223,10 +224,38 @@ class Session(SessionBase):
)
+class Summary(BaseModel):
+ content: str = Field(description="The summary text")
+ message_id: int = Field(
+ description="The ID of the message that this summary covers up to"
+ )
+ summary_type: str = Field(description="The type of summary (short or long)")
+ created_at: str = Field(
+ description="The timestamp of when the summary was created (ISO format)"
+ )
+ token_count: int = Field(description="The number of tokens in the summary text")
+
+
class SessionContext(SessionBase):
name: str = Field(serialization_alias="id")
messages: list[Message]
- summary: str
+ summary: Summary | None = Field(
+ default=None, description="The summary if available"
+ )
+
+ model_config = ConfigDict( # pyright: ignore
+ from_attributes=True, populate_by_name=True
+ )
+
+
+class SessionSummaries(SessionBase):
+ name: str = Field(serialization_alias="id")
+ short_summary: Summary | None = Field(
+ default=None, description="The short summary if available"
+ )
+ long_summary: Summary | None = Field(
+ default=None, description="The long summary if available"
+ )
model_config = ConfigDict( # pyright: ignore
from_attributes=True, populate_by_name=True
diff --git a/src/security.py b/src/security.py
index bc91e39b..a0e1a0bc 100644
--- a/src/security.py
+++ b/src/security.py
@@ -8,6 +8,7 @@ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pydantic import BaseModel
from src.config import settings
+from src.utils.formatting import utc_now_iso
from .exceptions import AuthenticationException
@@ -54,7 +55,7 @@ class JWTParams(BaseModel):
`s`: (string) session name
"""
- t: str = datetime.datetime.now().isoformat()
+ t: str = utc_now_iso()
exp: str | None = None
ad: bool | None = None
w: str | None = None
diff --git a/src/utils/embedding_store.py b/src/utils/embedding_store.py
index 84d41759..9bc303ce 100644
--- a/src/utils/embedding_store.py
+++ b/src/utils/embedding_store.py
@@ -12,6 +12,7 @@ from src import crud, models
from src.config import settings
from src.dependencies import tracked_db
from src.embedding_client import embedding_client
+from src.utils.formatting import format_datetime_utc
from src.utils.logging import conditional_observe
from src.utils.shared_models import (
Observation,
@@ -115,7 +116,7 @@ class EmbeddingStore:
"message_id": message_id,
"session_name": session_name,
"premises": obs.premises, # Store premises in metadata
- "created_at": message_created_at.isoformat()
+ "created_at": format_datetime_utc(message_created_at)
if message_created_at
else None,
}
diff --git a/src/utils/filter.py b/src/utils/filter.py
index f7e4cb84..50867da1 100644
--- a/src/utils/filter.py
+++ b/src/utils/filter.py
@@ -7,6 +7,7 @@ from sqlalchemy import ColumnElement, Select, and_, case, cast, literal, not_, o
from sqlalchemy.types import Numeric
from ..exceptions import FilterError
+from .formatting import parse_datetime_iso
logger = getLogger(__name__)
@@ -561,9 +562,9 @@ def _validate_datetime_string(value: str) -> datetime.datetime | None:
except ValueError:
continue
- # Try fromisoformat as a fallback (Python 3.7+)
+ # Try fromisoformat as a fallback (Python 3.7+) with Z format support
try:
- return datetime.datetime.fromisoformat(value.replace("Z", "+00:00"))
+ return parse_datetime_iso(value)
except ValueError:
pass
diff --git a/src/utils/formatting.py b/src/utils/formatting.py
index a9a75add..6b479e3b 100644
--- a/src/utils/formatting.py
+++ b/src/utils/formatting.py
@@ -5,13 +5,83 @@ This module contains helper functions for processing observations, formatting co
and handling temporal metadata for the reasoning system.
"""
-from datetime import datetime
+from datetime import datetime, timezone
from typing import Any, Protocol, cast, runtime_checkable
from src.utils.logging import conditional_observe
from src.utils.shared_models import ReasoningResponse
+def format_datetime_utc(dt: datetime) -> str:
+ """
+ Format datetime to ISO 8601 string with Z suffix for UTC timezone.
+
+ This ensures consistent datetime formatting across the entire backend,
+ using the Z format which is the ISO 8601 standard for UTC and matches
+ Pydantic's JSON serialization behavior.
+
+ Args:
+ dt: datetime object (should be timezone-aware)
+
+ Returns:
+ ISO 8601 formatted string with Z suffix for UTC
+
+ Example:
+ >>> dt = datetime(2023, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
+ >>> format_datetime_utc(dt)
+ '2023-01-01T12:00:00Z'
+ """
+ if dt.tzinfo is None:
+ # If no timezone info, assume UTC
+ dt = dt.replace(tzinfo=timezone.utc)
+
+ # Convert to UTC if not already
+ if dt.tzinfo != timezone.utc:
+ dt = dt.astimezone(timezone.utc)
+
+ # Format and replace +00:00 with Z
+ return dt.isoformat().replace("+00:00", "Z")
+
+
+def utc_now_iso() -> str:
+ """
+ Get current UTC time as ISO 8601 string with Z suffix.
+
+ Returns:
+ Current UTC time in ISO 8601 format with Z suffix
+
+ Example:
+ >>> utc_now_iso()
+ '2023-01-01T12:34:56.789123Z'
+ """
+ return format_datetime_utc(datetime.now(timezone.utc))
+
+
+def parse_datetime_iso(iso_string: str) -> datetime:
+ """
+ Parse ISO 8601 datetime string, handling both Z and +00:00 UTC formats.
+
+ This function handles the fact that Python's fromisoformat() doesn't
+ directly support the 'Z' suffix, which is the standard ISO 8601 way
+ to represent UTC timezone.
+
+ Args:
+ iso_string: ISO 8601 formatted datetime string
+
+ Returns:
+ datetime object with timezone information
+
+ Example:
+ >>> parse_datetime_iso('2023-01-01T12:00:00Z')
+ datetime.datetime(2023, 1, 1, 12, 0, tzinfo=datetime.timezone.utc)
+ >>> parse_datetime_iso('2023-01-01T12:00:00+00:00')
+ datetime.datetime(2023, 1, 1, 12, 0, tzinfo=datetime.timezone.utc)
+ """
+ # Convert Z format to +00:00 format for Python's fromisoformat
+ normalized_string = iso_string.replace("Z", "+00:00")
+ return datetime.fromisoformat(normalized_string)
+
+
@runtime_checkable
class StructuredObservation(Protocol):
"""Protocol for observations that have conclusion and premises attributes."""
diff --git a/src/utils/summarizer.py b/src/utils/summarizer.py
index 352dec23..c3391e63 100644
--- a/src/utils/summarizer.py
+++ b/src/utils/summarizer.py
@@ -1,5 +1,4 @@
import asyncio
-import datetime
import logging
import time
from enum import Enum
@@ -9,10 +8,12 @@ from sqlalchemy import update
from sqlalchemy.ext.asyncio import AsyncSession
from typing_extensions import TypedDict
+from src import schemas
from src.config import settings
from src.dependencies import tracked_db
from src.exceptions import ResourceNotFoundException
from src.utils.clients import honcho_llm_call
+from src.utils.formatting import utc_now_iso
from src.utils.logging import accumulate_metric
from .. import crud, models
@@ -40,6 +41,16 @@ class Summary(TypedDict):
token_count: int
+def to_schema_summary(s: Summary) -> schemas.Summary:
+ return schemas.Summary(
+ content=s["content"],
+ message_id=s["message_id"],
+ summary_type=s["summary_type"],
+ created_at=s["created_at"],
+ token_count=s["token_count"],
+ )
+
+
# Export the public functions
__all__ = [
"get_summary",
@@ -49,6 +60,7 @@ __all__ = [
"get_session_context_formatted",
"SummaryType",
"Summary",
+ "to_schema_summary",
]
@@ -388,7 +400,7 @@ async def _create_summary(
content=summary_text,
message_id=messages[-1].id if messages else 0,
summary_type=summary_type.value,
- created_at=datetime.datetime.now(datetime.timezone.utc).isoformat(),
+ created_at=utc_now_iso(),
token_count=summary_tokens,
)
@@ -564,7 +576,7 @@ async def get_session_context(
*,
cutoff: int | None = None,
include_summary: bool = True,
-) -> tuple[str, list[models.Message]]:
+) -> tuple[schemas.Summary | None, list[models.Message]]:
"""
Get session context similar to the API endpoint but for internal use.
@@ -577,10 +589,10 @@ async def get_session_context(
include_summary: Whether to include summary if available
Returns:
- Tuple of (summary_content, messages) where summary_content is the summary text (or empty string)
+ Tuple of (summary, messages) where summary is a Summary pydantic model (or None)
and messages is the list of message objects
"""
- summary_content = ""
+ summary = None
messages_tokens = token_limit
messages_start_id = 0
@@ -601,13 +613,25 @@ async def get_session_context(
and long_len <= summary_tokens_limit
and long_len > short_len
):
- summary_content = latest_long_summary["content"]
+ summary = schemas.Summary(
+ content=latest_long_summary["content"],
+ message_id=latest_long_summary["message_id"],
+ summary_type=latest_long_summary["summary_type"],
+ created_at=latest_long_summary["created_at"],
+ token_count=latest_long_summary["token_count"],
+ )
messages_tokens = token_limit - latest_long_summary["token_count"]
messages_start_id = latest_long_summary["message_id"]
elif (
latest_short_summary and short_len <= summary_tokens_limit and short_len > 0
):
- summary_content = latest_short_summary["content"]
+ summary = schemas.Summary(
+ content=latest_short_summary["content"],
+ message_id=latest_short_summary["message_id"],
+ summary_type=latest_short_summary["summary_type"],
+ created_at=latest_short_summary["created_at"],
+ token_count=latest_short_summary["token_count"],
+ )
messages_tokens = token_limit - latest_short_summary["token_count"]
messages_start_id = latest_short_summary["message_id"]
else:
@@ -628,7 +652,7 @@ async def get_session_context(
token_limit=messages_tokens,
)
- return summary_content, messages
+ return summary, messages
async def get_session_context_formatted(
@@ -646,7 +670,7 @@ async def get_session_context_formatted(
This is a convenience wrapper around get_session_context that formats
the output as a string.
"""
- summary_content, messages = await get_session_context(
+ summary, messages = await get_session_context(
db,
workspace_name,
session_name,
@@ -657,6 +681,7 @@ async def get_session_context_formatted(
# Format the messages
messages_text = _format_messages(messages)
+ summary_content = summary.content if summary else ""
if summary_content and messages_text:
return f"""
diff --git a/src/webhooks/webhook_delivery.py b/src/webhooks/webhook_delivery.py
index c9a1261d..96f9311a 100644
--- a/src/webhooks/webhook_delivery.py
+++ b/src/webhooks/webhook_delivery.py
@@ -3,7 +3,6 @@ import hashlib
import hmac
import json
import logging
-from datetime import datetime, timezone
import httpx
from sqlalchemy.ext.asyncio import AsyncSession
@@ -11,6 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from src.config import settings
from src.crud.webhook import list_webhook_endpoints
from src.deriver.queue_payload import WebhookPayload
+from src.utils.formatting import utc_now_iso
logger = logging.getLogger(__name__)
@@ -31,7 +31,7 @@ async def deliver_webhook(db: AsyncSession, payload: WebhookPayload) -> None:
event_payload = {
"type": payload.event_type,
"data": payload.data,
- "timestamp": datetime.now(timezone.utc).isoformat(),
+ "timestamp": utc_now_iso(),
}
event_json = json.dumps(
event_payload, separators=(",", ":"), sort_keys=True
diff --git a/tests/bench/run_tests.py b/tests/bench/run_tests.py
index c7510e61..72f7d7b8 100644
--- a/tests/bench/run_tests.py
+++ b/tests/bench/run_tests.py
@@ -509,8 +509,12 @@ Evaluate whether the actual response contains the core correct information from
summary=summary, tokens=max_tokens
)
+ summary_content = ""
+ if session_context.summary:
+ summary_content = session_context.summary.content
+
tokenizer = tiktoken.get_encoding("cl100k_base")
- summary_tokens = len(tokenizer.encode(session_context.summary))
+ summary_tokens = len(tokenizer.encode(summary_content))
print(f" summary: {session_context.summary}")
got_tokens = summary_tokens
diff --git a/tests/routes/test_messages.py b/tests/routes/test_messages.py
index fd8e840e..2f222339 100644
--- a/tests/routes/test_messages.py
+++ b/tests/routes/test_messages.py
@@ -1,3 +1,4 @@
+import datetime
from unittest.mock import patch
import pytest
@@ -921,3 +922,219 @@ async def test_create_messages_with_file_too_large(
# Should raise FileTooLargeError which gets converted to 413
assert response.status_code == 413
+
+
+@pytest.mark.asyncio
+async def test_create_message_with_timestamp(
+ client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
+):
+ """Test creating a message with custom timestamp"""
+ test_workspace, test_peer = sample_data
+
+ # Create a test session
+ test_session = models.Session(
+ workspace_name=test_workspace.name, name=str(generate_nanoid())
+ )
+ db_session.add(test_session)
+ await db_session.commit()
+
+ # Use a specific timestamp for testing
+ custom_timestamp = datetime.datetime(
+ 2023, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc
+ )
+
+ response = client.post(
+ f"/v2/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
+ json={
+ "messages": [
+ {
+ "content": "Test message with timestamp",
+ "peer_id": test_peer.name,
+ "created_at": custom_timestamp.isoformat(),
+ "metadata": {"test": "with_timestamp"},
+ }
+ ]
+ },
+ )
+ assert response.status_code == 200
+ data = response.json()
+ assert len(data) == 1
+ message = data[0]
+ assert message["content"] == "Test message with timestamp"
+ assert message["peer_id"] == test_peer.name
+ assert message["metadata"] == {"test": "with_timestamp"}
+
+ # Verify the created_at field matches our custom timestamp
+ # Pydantic serializes UTC timezone as 'Z' format (ISO 8601 standard)
+ expected_timestamp = "2023-01-01T12:00:00Z"
+ assert message["created_at"] == expected_timestamp
+
+
+@pytest.mark.asyncio
+async def test_create_message_without_timestamp_uses_default(
+ client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
+):
+ """Test creating a message without timestamp uses default timestamp"""
+ test_workspace, test_peer = sample_data
+
+ # Create a test session
+ test_session = models.Session(
+ workspace_name=test_workspace.name, name=str(generate_nanoid())
+ )
+ db_session.add(test_session)
+ await db_session.commit()
+
+ # Record time before request
+ before_request = datetime.datetime.now(datetime.timezone.utc)
+
+ response = client.post(
+ f"/v2/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
+ json={
+ "messages": [
+ {
+ "content": "Test message without timestamp",
+ "peer_id": test_peer.name,
+ "metadata": {"test": "no_timestamp"},
+ }
+ ]
+ },
+ )
+
+ # Record time after request
+ after_request = datetime.datetime.now(datetime.timezone.utc)
+
+ assert response.status_code == 200
+ data = response.json()
+ assert len(data) == 1
+ message = data[0]
+ assert message["content"] == "Test message without timestamp"
+ assert message["peer_id"] == test_peer.name
+ assert message["metadata"] == {"test": "no_timestamp"}
+
+ # Verify the created_at field is between our before/after times
+ message_created_at = datetime.datetime.fromisoformat(
+ message["created_at"].replace("Z", "+00:00")
+ )
+ assert before_request <= message_created_at <= after_request
+
+
+@pytest.mark.asyncio
+async def test_create_batch_messages_with_mixed_timestamps(
+ client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
+):
+ """Test creating batch messages with some having custom timestamps and others using default"""
+ test_workspace, test_peer = sample_data
+
+ # Create a test session
+ test_session = models.Session(
+ workspace_name=test_workspace.name, name=str(generate_nanoid())
+ )
+ db_session.add(test_session)
+ await db_session.commit()
+
+ # Use specific timestamps for testing
+ timestamp1 = datetime.datetime(2023, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc)
+ timestamp2 = datetime.datetime(2023, 1, 2, 12, 0, 0, tzinfo=datetime.timezone.utc)
+
+ # Record time before request for default timestamp
+ before_request = datetime.datetime.now(datetime.timezone.utc)
+
+ response = client.post(
+ f"/v2/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
+ json={
+ "messages": [
+ {
+ "content": "Message 1 with timestamp",
+ "peer_id": test_peer.name,
+ "created_at": timestamp1.isoformat(),
+ "metadata": {"type": "custom_timestamp"},
+ },
+ {
+ "content": "Message 2 without timestamp",
+ "peer_id": test_peer.name,
+ "metadata": {"type": "default_timestamp"},
+ },
+ {
+ "content": "Message 3 with timestamp",
+ "peer_id": test_peer.name,
+ "created_at": timestamp2.isoformat(),
+ "metadata": {"type": "custom_timestamp"},
+ },
+ ]
+ },
+ )
+
+ after_request = datetime.datetime.now(datetime.timezone.utc)
+
+ assert response.status_code == 200
+ data = response.json()
+ assert len(data) == 3
+
+ # Check first message with custom timestamp
+ assert data[0]["content"] == "Message 1 with timestamp"
+ assert (
+ data[0]["created_at"] == "2023-01-01T12:00:00Z"
+ ) # Pydantic converts to Z format
+ assert data[0]["metadata"] == {"type": "custom_timestamp"}
+
+ # Check second message with default timestamp
+ assert data[1]["content"] == "Message 2 without timestamp"
+ message2_created_at = datetime.datetime.fromisoformat(
+ data[1]["created_at"].replace("Z", "+00:00")
+ )
+ assert before_request <= message2_created_at <= after_request
+ assert data[1]["metadata"] == {"type": "default_timestamp"}
+
+ # Check third message with custom timestamp
+ assert data[2]["content"] == "Message 3 with timestamp"
+ assert (
+ data[2]["created_at"] == "2023-01-02T12:00:00Z"
+ ) # Pydantic converts to Z format
+ assert data[2]["metadata"] == {"type": "custom_timestamp"}
+
+
+@pytest.mark.asyncio
+async def test_create_message_with_null_timestamp(
+ client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
+):
+ """Test creating a message with null timestamp uses default"""
+ test_workspace, test_peer = sample_data
+
+ # Create a test session
+ test_session = models.Session(
+ workspace_name=test_workspace.name, name=str(generate_nanoid())
+ )
+ db_session.add(test_session)
+ await db_session.commit()
+
+ # Record time before request
+ before_request = datetime.datetime.now(datetime.timezone.utc)
+
+ response = client.post(
+ f"/v2/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
+ json={
+ "messages": [
+ {
+ "content": "Test message with null timestamp",
+ "peer_id": test_peer.name,
+ "created_at": None,
+ "metadata": {"test": "null_timestamp"},
+ }
+ ]
+ },
+ )
+
+ after_request = datetime.datetime.now(datetime.timezone.utc)
+
+ assert response.status_code == 200
+ data = response.json()
+ assert len(data) == 1
+ message = data[0]
+ assert message["content"] == "Test message with null timestamp"
+ assert message["metadata"] == {"test": "null_timestamp"}
+
+ # Verify the created_at field uses default (current time)
+ message_created_at = datetime.datetime.fromisoformat(
+ message["created_at"].replace("Z", "+00:00")
+ )
+ assert before_request <= message_created_at <= after_request
diff --git a/tests/routes/test_sessions.py b/tests/routes/test_sessions.py
index 5c12c10a..e2017f40 100644
--- a/tests/routes/test_sessions.py
+++ b/tests/routes/test_sessions.py
@@ -784,7 +784,7 @@ def test_get_session_context(client: TestClient, sample_data: tuple[Workspace, P
assert "summary" in data
assert data["id"] == session_id
assert isinstance(data["messages"], list)
- assert data["summary"] == "" # Default is empty when summary=False
+ assert data["summary"] is None # No summary available
def test_get_session_context_with_summary(
@@ -855,6 +855,55 @@ def test_get_session_context_with_all_params(
assert "summary" in data
+def test_get_session_summaries(
+ client: TestClient, sample_data: tuple[Workspace, Peer]
+) -> None:
+ """Test getting summaries for a valid session"""
+ test_workspace, test_peer = sample_data
+ session_id = str(generate_nanoid())
+
+ # Create session
+ client.post(
+ f"/v2/workspaces/{test_workspace.name}/sessions",
+ json={"id": session_id, "peers": {test_peer.name: {}}},
+ )
+
+ # Get summaries
+ response = client.get(
+ f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/summaries",
+ )
+ assert response.status_code == 200
+ data = response.json()
+
+ # Validate response structure
+ assert "id" in data
+ assert data["id"] == session_id
+ assert "short_summary" in data
+ assert "long_summary" in data
+ # Summaries will be None since they're created asynchronously
+ assert data["short_summary"] is None
+ assert data["long_summary"] is None
+
+
+def test_get_session_summaries_nonexistent_session(
+ client: TestClient, sample_data: tuple[Workspace, Peer]
+) -> None:
+ """Test getting summaries for a non-existent session"""
+ test_workspace, _ = sample_data
+ nonexistent_session_id = str(generate_nanoid())
+
+ # Try to get summaries for non-existent session
+ # Should still return 200 with null summaries
+ response = client.get(
+ f"/v2/workspaces/{test_workspace.name}/sessions/{nonexistent_session_id}/summaries",
+ )
+ assert response.status_code == 200
+ data = response.json()
+ assert data["id"] == nonexistent_session_id
+ assert data["short_summary"] is None
+ assert data["long_summary"] is None
+
+
def test_search_session(client: TestClient, sample_data: tuple[Workspace, Peer]):
"""Test the session search functionality"""
test_workspace, test_peer = sample_data
diff --git a/uv.lock b/uv.lock
index fb42a7a8..635733ac 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'",
@@ -7,6 +7,12 @@ resolution-markers = [
"python_full_version < '3.11'",
]
+[manifest]
+members = [
+ "honcho",
+ "honcho-ai",
+]
+
[[package]]
name = "alembic"
version = "1.16.4"
@@ -822,7 +828,7 @@ wheels = [
[[package]]
name = "honcho"
-version = "2.2.0"
+version = "2.3.0"
source = { virtual = "." }
dependencies = [
{ name = "alembic" },
@@ -853,7 +859,6 @@ dev = [
{ name = "basedpyright" },
{ name = "coverage" },
{ name = "honcho-ai" },
- { name = "honcho-core" },
{ name = "interrogate" },
{ name = "pre-commit" },
{ name = "py-spy" },
@@ -893,8 +898,7 @@ requires-dist = [
dev = [
{ name = "basedpyright", specifier = ">=1.29.4" },
{ name = "coverage", specifier = ">=7.6.0" },
- { name = "honcho-ai", specifier = "==1.3.0" },
- { name = "honcho-core", specifier = "==1.3.0" },
+ { name = "honcho-ai", editable = "sdks/python" },
{ name = "interrogate", specifier = ">=1.7.0" },
{ name = "pre-commit", specifier = ">=4.2.0" },
{ name = "py-spy", specifier = ">=0.3.14" },
@@ -907,21 +911,32 @@ dev = [
[[package]]
name = "honcho-ai"
-version = "1.3.0"
-source = { registry = "https://pypi.org/simple" }
+version = "1.4.0"
+source = { editable = "sdks/python" }
dependencies = [
{ name = "honcho-core" },
{ name = "httpx" },
{ name = "pydantic" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a3/d3/c882f95637e30124c57a3496ac8221865c2449323f5bd6823009658ce8f7/honcho_ai-1.3.0.tar.gz", hash = "sha256:674440071ae66f3382c7a2355e1c9e8a2c9f0f9482ca0633151d30d42b20ea2c", size = 25042, upload-time = "2025-08-06T21:49:15.655Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/dc/f9/0d3c71f4a400da433119eabfb03837a00f1badd31994d578e4fdc9f95c1f/honcho_ai-1.3.0-py3-none-any.whl", hash = "sha256:cb610384956752a8a33f7dee1a01f10b7732c76417b72c87f0d91dea2acd1174", size = 32599, upload-time = "2025-08-06T21:49:14.418Z" },
+
+[package.dev-dependencies]
+dev = [
+ { name = "ruff" },
]
+[package.metadata]
+requires-dist = [
+ { name = "honcho-core", specifier = ">=1.4.0" },
+ { name = "httpx", specifier = ">=0.28.0,<1" },
+ { name = "pydantic", specifier = ">=2.0.0,<3" },
+]
+
+[package.metadata.requires-dev]
+dev = [{ name = "ruff", specifier = ">=0.11.13" }]
+
[[package]]
name = "honcho-core"
-version = "1.3.0"
+version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -931,9 +946,9 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ee/49/1810de2351be1fdff330ffadcc5eef709889b2f698b0340966cae2c1eef9/honcho_core-1.3.0.tar.gz", hash = "sha256:276a73e8d523f7d22f06746922fda97e4a11fbbef4a51a0b43573bc817089cec", size = 123333, upload-time = "2025-08-06T16:42:10.941Z" }
+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" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/6e/3f/3beb750ed65bb1b604c585ae1c51a54f8b34ea3002f4525f93fe86549531/honcho_core-1.3.0-py3-none-any.whl", hash = "sha256:c7c0bf77b61162e6c65a580327fc054785aef794923abc35acd3dbd8287245db", size = 112895, upload-time = "2025-08-06T16:42:08.967Z" },
+ { 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" },
]
[[package]]