feat: Add get summaries endpoints & Custom Timestamps (#185)

* feat: add support for custom message timestamps in API

- Introduced `created_at` parameter for message creation, allowing users to specify custom timestamps.
- **Single source of truth for timestamp string format**
- Updated SDK documentation to reflect this new feature and its use cases.
- Enhanced validation schemas to include the optional `created_at` field.
- Added tests to verify functionality for messages with and without custom timestamps, ensuring correct behavior and default timestamp usage.

* feat: add timestamp option to sdks

* feat: Add get summaries endpoints

* feat: WIP basic SDK implementation blocked until stainless release

* feat: Implement SDKs with honcho-core methods

* fix (sdk): Used release 1.4.0 core sdks

* fix: Code Rabbit

* chore: Pytest errors

---------

Co-authored-by: Benjamin McCormick <docterformer@protonmail.com>
This commit is contained in:
Vineeth Voruganti 2025-08-12 17:19:53 -04:00 committed by GitHub
parent 0838a281c1
commit e914dbe334
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 2237 additions and 524 deletions

View File

@ -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

View File

@ -1,6 +1,6 @@
# 🫡 Honcho
![Static Badge](https://img.shields.io/badge/Version-2.2.0-blue)
![Static Badge](https://img.shields.io/badge/Version-2.3.0-blue)
[![Discord](https://img.shields.io/discord/1016845111637839922?style=flat&logo=discord&logoColor=23ffffff&label=Plastic%20Labs&labelColor=235865F2)](https://discord.gg/plasticlabs)
[![arXiv](https://img.shields.io/badge/arXiv-2310.06983-b31b1b.svg)](https://arxiv.org/abs/2310.06983)
![GitHub License](https://img.shields.io/github/license/plastic-labs/honcho)

View File

@ -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)
<CardGroup cols={2}>
<Card title="TypeScript SDK" icon="js">
**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
```
</Card>
<Card title="Python SDK" icon="python">
**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
```
</Card>
</CardGroup>
@ -33,20 +33,20 @@ This guide helps you understand which versions of Honcho's API are compatible wi
<CardGroup cols={2}>
<Card title="TypeScript SDK" icon="js">
**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
```
</Card>
<Card title="Python SDK" icon="python">
**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
```
</Card>
@ -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 |

View File

@ -27,7 +27,23 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
### Honcho API and SDK Changelogs
<Tabs>
<Tab title="Honcho API">
<Update label="v2.2.0 (Current)">
<Update label="v2.3.0 (Current)">
### 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
</Update>
<Update label="v2.2.0">
### 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
<Tab title="Python SDK">
[Python SDK](https://pypi.org/project/honcho-ai/)
<Update label="v1.4.0 (Current)">
### Added
- getSummaries API returning structured summaries
### Changed
- Messages can take an optional `created_at` value, defaulting to the current
time (UTC ISO 8601)
</Update>
<Update label="v1.2.2">
### Added
@ -275,6 +301,16 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
<Tab title="TypeScript SDK">
[TypeScript SDK](https://www.npmjs.com/package/@honcho-ai/sdk)
<Update label="v1.4.0 (Current)">
### Added
- getSummaries API returning structured summaries
### Changed
- Messages can take an optional `created_at` value, defaulting to the current
time (UTC ISO 8601)
</Update>
<Update label="v1.2.1">
### Added

View File

@ -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"
]
}
]
},

View File

@ -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": "",

View File

@ -0,0 +1,3 @@
---
openapi: get /v2/workspaces/{workspace_id}/sessions/{session_id}/summaries
---

View File

@ -0,0 +1,3 @@
---
openapi: delete /v2/workspaces/{workspace_id}/webhooks/{endpoint_id}
---

View File

@ -0,0 +1,3 @@
---
openapi: post /v2/workspaces/{workspace_id}/webhooks
---

View File

@ -0,0 +1,3 @@
---
openapi: get /v2/workspaces/{workspace_id}/webhooks
---

View File

@ -0,0 +1,3 @@
---
openapi: get /v2/workspaces/{workspace_id}/webhooks/test
---

View File

@ -555,6 +555,33 @@ const response = await openai.chat.completions.create({
```
</CodeGroup>
### 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.

View File

@ -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:

View File

@ -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 = [

View File

@ -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

View File

@ -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

View File

@ -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())

View File

@ -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!")

View File

@ -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",
]

View File

@ -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",
]

View File

@ -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]:
"""

View File

@ -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

View File

@ -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]:
"""

View File

@ -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

View File

@ -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"<summary>{self.summary}</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"<summary>{self.summary.content}</summary>",
}
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"<summary>{self.summary}</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"<summary>{self.summary.content}</summary>",
}
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'})"

View File

@ -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 = [

View File

@ -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

View File

@ -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: '<summary>Friendly greeting</summary>' },
{ role: 'user', content: 'Hello', name: 'user' },
{ role: 'assistant', content: 'Hi there!', name: 'assistant' },
]);
])
expect(anthropicFormat).toEqual([
{ role: 'user', content: '<summary>Friendly greeting</summary>' },
{ 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')
}
});
});
});
})
})
})

View File

@ -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 () => {

View File

@ -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
});

View File

@ -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=="],

View File

@ -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);
});

View File

@ -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 <hello@plasticlabs.ai>",
"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"
},

View File

@ -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 {

View File

@ -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<string, unknown> }
options?: {
metadata?: Record<string, unknown>
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,
}
}

View File

@ -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<void> {
@ -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<SessionSummaries> {
// 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)
}
/**

View File

@ -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: `<summary>${this.summary}</summary>`,
}
const summaryMessage = this.summary
? {
role: 'system',
content: `<summary>${this.summary.content}</summary>`,
}
: 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: `<summary>${this.summary}</summary>`,
}
const summaryMessage = this.summary
? {
role: 'user',
content: `<summary>${this.summary.content}</summary>`,
}
: 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'})`
}
}

View File

@ -94,6 +94,7 @@ export const MessageCreateSchema = z.object({
peer_id: PeerIdSchema,
content: MessageContentSchema,
metadata: MessageMetadataSchema,
created_at: z.string().nullable().optional(),
})
/**

View File

@ -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)

View File

@ -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

View File

@ -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"""
<summary>
{session_context.summary}
</summary>
<recent_messages>
{session_context.messages}
</recent_messages>
"""
logger.info("Retrieved recent conversation history")
else:
recent_conversation_history = None
logger.info("Query is not session-scoped, skipping recent conversation history")

View File

@ -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):

View File

@ -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",

View File

@ -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,

View File

@ -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,
)

View File

@ -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

View File

@ -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

View File

@ -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,
}

View File

@ -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

View File

@ -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."""

View File

@ -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"""<summary>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

41
uv.lock
View File

@ -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]]