feat: Release PR for v2.4.0

This commit is contained in:
Vineeth Voruganti 2025-10-08 14:38:37 -04:00
parent f38230fd92
commit ad905fb0c9
20 changed files with 4951 additions and 3858 deletions

61
.github/workflows/fly-deploy-prod.yml vendored Normal file
View File

@ -0,0 +1,61 @@
# See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/
name: Fly Deploy (Production Environment)
permissions:
contents: read
on:
push:
tags:
- v*
workflow_dispatch:
inputs:
version:
description: "Version to deploy (without v prefix)"
required: true
type: string
default: 'manual'
jobs:
deploy-honcho-prod-image:
name: Deploy Honcho Image (Production Environment)
runs-on: ubuntu-latest
concurrency:
group: deploy-prod-group
cancel-in-progress: true
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@1.5
- run: |
# Determine the image label based on trigger type
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
IMAGE_LABEL="deployment-${{ github.event.inputs.version }}"
else
IMAGE_LABEL="deployment-${{ github.ref_name }}"
fi
flyctl deploy -a honcho-prod-image --remote-only --build-only --push --no-cache --image-label "$IMAGE_LABEL"
env:
FLY_API_TOKEN: ${{ secrets.FLY_PROD_API_TOKEN }}
prompt-service:
name: Push to Service (Production Environment)
needs: deploy-honcho-prod-image
runs-on: ubuntu-latest
steps:
- name: Send POST request
env:
GITHUB_REF_NAME: ${{ github.ref_name }}
run: |
# Determine version and image label based on trigger type
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
TAG="${{ github.event.inputs.version }}"
IMAGE_LABEL="honcho-prod-image:deployment-${{ github.event.inputs.version }}"
else
TAG=${GITHUB_REF_NAME#v}
IMAGE_LABEL="honcho-prod-image:deployment-${GITHUB_REF_NAME}"
fi
curl --fail -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${{ secrets.PROD_ENV_WEBHOOK_SECRET }}" \
-d "{\"version\":\"$TAG\",\"image_label\":\"$IMAGE_LABEL\"}" \
"${{ secrets.PROD_ENV_URL }}/webhooks/v1/add_honcho_version"

61
.github/workflows/fly-deploy.yml vendored Normal file
View File

@ -0,0 +1,61 @@
# See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/
name: Fly Deploy (Test Environment)
permissions:
contents: read
on:
push:
tags:
- v*
workflow_dispatch:
inputs:
version:
description: "Version to deploy (without v prefix)"
required: true
type: string
default: 'manual'
jobs:
deploy-honcho-image:
name: Deploy Honcho Image (Test Environment)
runs-on: ubuntu-latest
concurrency:
group: deploy-test-group
cancel-in-progress: true
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@1.5
- run: |
# Determine the image label based on trigger type
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
IMAGE_LABEL="deployment-${{ github.event.inputs.version }}"
else
IMAGE_LABEL="deployment-${{ github.ref_name }}"
fi
flyctl deploy -a honcho-image --remote-only --build-only --push --no-cache --image-label "$IMAGE_LABEL"
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
prompt-service:
name: Push to Service (Test Environment)
runs-on: ubuntu-latest
needs: deploy-honcho-image
steps:
- name: Send POST request
env:
GITHUB_REF_NAME: ${{ github.ref_name }}
run: |
# Determine version and image label based on trigger type
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
TAG="${{ github.event.inputs.version }}"
IMAGE_LABEL="honcho-image:deployment-${{ github.event.inputs.version }}"
else
TAG=${GITHUB_REF_NAME#v}
IMAGE_LABEL="honcho-image:deployment-${GITHUB_REF_NAME}"
fi
curl --fail -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${{ secrets.TEST_ENV_WEBHOOK_SECRET }}" \
-d "{\"version\":\"$TAG\",\"image_label\":\"$IMAGE_LABEL\"}" \
"${{ secrets.TEST_ENV_URL }}/webhooks/v1/add_honcho_version"

View File

@ -5,6 +5,28 @@ 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.4.0] - 2025-10-08
### Added
- Unified `Representation` class
- vllm client support
- Periodic queue cleanup logic
- WIP Dreaming Feature
- LongMemEval to Test Bench
- Prometheus Client for better Metrics
- Performance metrics instrumentation
- Error reporting to deriver
### Changed
- Working Representations are Queried on the fly rather than cached in metadata
- EmbeddingStore to RepresentationFactory
- Summary Response Model to use public_id of message for cutoff
- Semantic across codebase to reference resources based on `observer` and `observed`
- Prompts for Deriver & Dialectic to reference peer_id and add examples
- `Get Context` route returns peer card and representation in addition to messages and summaries
## [2.3.3] — 2025-10-01
### Changed

View File

@ -8,7 +8,7 @@
---
![Static Badge](https://img.shields.io/badge/Version-2.3.3-blue)
![Static Badge](https://img.shields.io/badge/Version-2.4.0-blue)
[![PyPI version](https://img.shields.io/pypi/v/honcho-ai.svg)](https://pypi.org/project/honcho-ai/)
[![NPM version](https://img.shields.io/npm/v/@honcho-ai/sdk.svg)](https://npmjs.org/package/@honcho-ai/sdk)
[![Discord](https://img.shields.io/discord/1016845111637839922?style=flat&logo=discord&logoColor=23ffffff&label=Plastic%20Labs&labelColor=235865F2)](https://discord.gg/plasticlabs)

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.3.3 (Current)
### Honcho API v2.4.0 (Current)
<CardGroup cols={2}>
<Card title="TypeScript SDK" icon="js">
**Compatible Version:** v1.4.1
**Compatible Version:** v1.5.0
Install with:
```bash
npm install @honcho-ai/sdk@1.4.1
npm install @honcho-ai/sdk@1.5.0
```
</Card>
<Card title="Python SDK" icon="python">
**Compatible Version:** v1.4.1
**Compatible Version:** v1.5.0
Install with:
```bash
pip install honcho-ai==1.4.1
pip install honcho-ai==1.5.0
```
</Card>
</CardGroup>
@ -35,7 +35,8 @@ This guide helps you understand which versions of Honcho's API are compatible wi
| Honcho API Version | TypeScript SDK | Python SDK |
|-------------------|---------------|------------|
| v2.3.3 (Current) | v1.4.1 | v1.4.1 |
| v2.4.0 (Current) | v1.5.0 | v1.5.0 |
| v2.3.3 | v1.4.1 | v1.4.1 |
| v2.3.2 | v1.4.0 | v1.4.0 |
| v2.3.1 | v1.4.0 | v1.4.0 |
| v2.3.0 | v1.4.0 | v1.4.0 |

View File

@ -27,7 +27,29 @@ 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.3.3 (Current)">
<Update label="v2.4.0 (Current)">
### Added
- Unified `Representation` class
- vllm client support
- Periodic queue cleanup logic
- WIP Dreaming Feature
- LongMemEval to Test Bench
- Prometheus Client for better Metrics
- Performance metrics instrumentation
- Error reporting to deriver
### Changed
- Working Representations are Queried on the fly rather than cached in metadata
- EmbeddingStore to RepresentationFactory
- Summary Response Model to use public_id of message for cutoff
- Semantic across codebase to reference resources based on `observer` and `observed`
- Prompts for Deriver & Dialectic to reference peer_id and add examples
- `Get Context` route returns peer card and representation in addition to messages and summaries
</Update>
<Update label="v2.3.3">
### Changed
- Deriver Rollup Queue processes interleaved messages for more context
@ -305,6 +327,11 @@ 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.5.0 (Current)">
### Changed
- message_id of `Summary` model is a string nanoid
</Update>
<Update label="v1.4.1">
### Added
@ -369,6 +396,11 @@ 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.5.0 (Current)">
### Changed
- message_id of `Summary` model is a string nanoid
</Update>
<Update label="v1.4.1">
### Added

View File

@ -14,9 +14,9 @@
"navigation": {
"versions": [
{
"version": "v2.3.3",
"version": "v2.4.0",
"api": {
"openapi": ["openapi.documented.yml"]
"openapi": ["openapi.documented.json"]
},
"tabs": [
{
@ -147,10 +147,7 @@
"v2/api-reference/endpoint/messages/create-messages-with-file"
]
},
{
"group": "keys",
"pages": ["v2/api-reference/endpoint/keys/create-key"]
},
{
"group": "webhooks",
"pages": [
@ -159,6 +156,13 @@
"v2/api-reference/endpoint/webhooks/delete-webhook-endpoint",
"v2/api-reference/endpoint/webhooks/test-emit"
]
},
{
"group": "miscellaneous",
"pages": [
"v2/api-reference/endpoint/keys/create-key",
"v2/api-reference/endpoint/metrics"
]
}
]
},

View File

@ -0,0 +1,3 @@
---
openapi: get /metrics
---

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[project]
name = "honcho"
version = "2.3.3"
version = "2.4.0"
description = "Honcho Server"
authors = [
{name = "Plastic Labs", email = "hello@plasticlabs.ai"},

View File

@ -5,6 +5,12 @@ 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.5.0] - 2025-10-08
### Changed
- message_id of `Summary` model is a string nanoid
## [1.4.1] — 2025-10-09
### Added

View File

@ -1,6 +1,6 @@
[project]
name = "honcho-ai"
version = "1.4.1"
version = "1.5.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.4.1-alpha.0",
"honcho-core>=1.5.0",
"httpx>=0.28.0, <1",
"pydantic>=2.0.0, <3",
"typing-extensions>=4.12.0; python_version < \"3.12\"",

View File

@ -47,7 +47,7 @@ from .session import Session
from .session_context import SessionContext, SessionSummaries, Summary
from .types import DialecticStreamResponse
__version__ = "1.4.0"
__version__ = "1.5.0"
__author__ = "Plastic Labs"
__email__ = "hello@plasticlabs.ai"

View File

@ -5,6 +5,12 @@ 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.5.0] - 2025-10-08
### Changed
- message_id of `Summary` model is a string nanoid
## [1.4.1] — 2025-10-09
### Added

View File

@ -1,10 +1,15 @@
import { SessionContext, Summary } from '../src/session_context';
import { Peer } from '../src/peer';
import { SessionContext, Summary } from '../src/session_context'
import { Peer } from '../src/peer'
/**
* Helper function to create a proper Message object for testing
*/
function createTestMessage(id: string, content: string, peer_id: string, additionalProps: any = {}): any {
function createTestMessage(
id: string,
content: string,
peer_id: string,
additionalProps: any = {}
): any {
return {
id,
content,
@ -13,8 +18,8 @@ function createTestMessage(id: string, content: string, peer_id: string, additio
session_id: 'test-session',
token_count: 0,
workspace_id: 'test-workspace',
...additionalProps
};
...additionalProps,
}
}
/**
@ -23,16 +28,16 @@ function createTestMessage(id: string, content: string, peer_id: string, additio
function createTestSummary(content: string): Summary {
return new Summary({
content,
message_id: 1,
message_id: 'test-message-id',
summary_type: 'short',
created_at: new Date().toISOString(),
token_count: content.length
});
token_count: content.length,
})
}
describe('SessionContext', () => {
let sessionContext: SessionContext;
let mockMessages: any[];
let sessionContext: SessionContext
let mockMessages: any[]
beforeEach(() => {
mockMessages = [
@ -40,185 +45,223 @@ describe('SessionContext', () => {
createTestMessage('msg2', 'Hi there', 'user'),
createTestMessage('msg3', 'How are you?', 'user'),
createTestMessage('msg4', 'I am doing well, thank you!', 'assistant'),
];
]
sessionContext = new SessionContext('test-session', mockMessages, null);
});
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(null);
});
expect(sessionContext.sessionId).toBe('test-session')
expect(sessionContext.messages).toEqual(mockMessages)
expect(sessionContext.summary).toBe(null)
})
it('should initialize with null summary when not provided', () => {
const context = new SessionContext('session-id', mockMessages);
const context = new SessionContext('session-id', mockMessages)
expect(context.sessionId).toBe('session-id');
expect(context.messages).toEqual(mockMessages);
expect(context.summary).toBe(null);
});
expect(context.sessionId).toBe('session-id')
expect(context.messages).toEqual(mockMessages)
expect(context.summary).toBe(null)
})
it('should handle empty messages array', () => {
const summary = createTestSummary('No messages');
const context = new SessionContext('session-id', [], summary);
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(summary);
});
expect(context.sessionId).toBe('session-id')
expect(context.messages).toEqual([])
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);
const context1 = new SessionContext(
'session-id',
mockMessages,
undefined as any
)
const context2 = new SessionContext('session-id', mockMessages, null)
expect(context1.summary).toBe(null);
expect(context2.summary).toBe(null);
});
});
expect(context1.summary).toBe(null)
expect(context2.summary).toBe(null)
})
})
describe('toOpenAI', () => {
it('should convert messages to OpenAI format with string assistant', () => {
const openAIMessages = sessionContext.toOpenAI('assistant');
const openAIMessages = sessionContext.toOpenAI('assistant')
expect(openAIMessages).toEqual([
{ role: 'assistant', content: 'Hello', name: 'assistant' },
{ role: 'user', content: 'Hi there', name: 'user' },
{ role: 'user', content: 'How are you?', name: 'user' },
{ role: 'assistant', content: 'I am doing well, thank you!', name: 'assistant' },
]);
});
{
role: 'assistant',
content: 'I am doing well, thank you!',
name: 'assistant',
},
])
})
it('should convert messages to OpenAI format with Peer object', () => {
const mockClient = {} as any;
const assistantPeer = new Peer('assistant', 'test-workspace', mockClient);
const mockClient = {} as any
const assistantPeer = new Peer('assistant', 'test-workspace', mockClient)
const openAIMessages = sessionContext.toOpenAI(assistantPeer);
const openAIMessages = sessionContext.toOpenAI(assistantPeer)
expect(openAIMessages).toEqual([
{ role: 'assistant', content: 'Hello', name: 'assistant' },
{ role: 'user', content: 'Hi there', name: 'user' },
{ role: 'user', content: 'How are you?', name: 'user' },
{ role: 'assistant', content: 'I am doing well, thank you!', name: 'assistant' },
]);
});
{
role: 'assistant',
content: 'I am doing well, thank you!',
name: 'assistant',
},
])
})
it('should handle messages where assistant is different peer', () => {
const openAIMessages = sessionContext.toOpenAI('different-assistant');
const openAIMessages = sessionContext.toOpenAI('different-assistant')
expect(openAIMessages).toEqual([
{ role: 'user', content: 'Hello', name: 'assistant' },
{ role: 'user', content: 'Hi there', name: 'user' },
{ role: 'user', content: 'How are you?', name: 'user' },
{ role: 'user', content: 'I am doing well, thank you!', name: 'assistant' },
]);
});
{
role: 'user',
content: 'I am doing well, thank you!',
name: 'assistant',
},
])
})
it('should handle empty messages array', () => {
const emptyContext = new SessionContext('session-id', []);
const openAIMessages = emptyContext.toOpenAI('assistant');
const emptyContext = new SessionContext('session-id', [])
const openAIMessages = emptyContext.toOpenAI('assistant')
expect(openAIMessages).toEqual([]);
});
expect(openAIMessages).toEqual([])
})
it('should include summary message when summary exists', () => {
const summary = createTestSummary('This is a summary');
const contextWithSummary = new SessionContext('test-session', mockMessages, summary);
const openAIMessages = contextWithSummary.toOpenAI('assistant');
const summary = createTestSummary('This is a summary')
const contextWithSummary = new SessionContext(
'test-session',
mockMessages,
summary
)
const openAIMessages = contextWithSummary.toOpenAI('assistant')
expect(openAIMessages).toEqual([
{ role: 'system', content: '<summary>This is a summary</summary>' },
{ role: 'assistant', content: 'Hello', name: 'assistant' },
{ role: 'user', content: 'Hi there', name: 'user' },
{ role: 'user', content: 'How are you?', name: 'user' },
{ role: 'assistant', content: 'I am doing well, thank you!', name: 'assistant' },
]);
});
{
role: 'assistant',
content: 'I am doing well, thank you!',
name: 'assistant',
},
])
})
it('should handle messages with missing peer_id', () => {
const messagesWithMissingPeer = [
createTestMessage('msg1', 'Hello', 'assistant'),
createTestMessage('msg2', 'No peer', ''), // missing peer_id
createTestMessage('msg3', 'Another message', ''), // null peer_id
];
const context = new SessionContext('test', messagesWithMissingPeer);
]
const context = new SessionContext('test', messagesWithMissingPeer)
const openAIMessages = context.toOpenAI('assistant');
const openAIMessages = context.toOpenAI('assistant')
expect(openAIMessages).toEqual([
{ role: 'assistant', content: 'Hello', name: 'assistant' },
{ role: 'user', content: 'No peer', name: '' },
{ role: 'user', content: 'Another message', name: '' },
]);
});
])
})
it('should handle complex message content', () => {
const complexMessages = [
createTestMessage('msg1', 'Message with\nnewlines and special chars!@#$%', 'assistant'),
createTestMessage(
'msg1',
'Message with\nnewlines and special chars!@#$%',
'assistant'
),
createTestMessage('msg2', '', 'user'), // empty content
createTestMessage('msg3', ' whitespace ', 'assistant'),
];
const context = new SessionContext('test', complexMessages);
]
const context = new SessionContext('test', complexMessages)
const openAIMessages = context.toOpenAI('assistant');
const openAIMessages = context.toOpenAI('assistant')
expect(openAIMessages).toEqual([
{ role: 'assistant', content: 'Message with\nnewlines and special chars!@#$%', name: 'assistant' },
{
role: 'assistant',
content: 'Message with\nnewlines and special chars!@#$%',
name: 'assistant',
},
{ role: 'user', content: '', name: 'user' },
{ role: 'assistant', content: ' whitespace ', name: 'assistant' },
]);
});
});
])
})
})
describe('toAnthropic', () => {
it('should convert messages to Anthropic format with string assistant', () => {
const anthropicMessages = sessionContext.toAnthropic('assistant');
const anthropicMessages = sessionContext.toAnthropic('assistant')
expect(anthropicMessages).toEqual([
{ role: 'assistant', content: 'Hello' },
{ role: 'user', content: 'user: Hi there' },
{ role: 'user', content: 'user: How are you?' },
{ role: 'assistant', content: 'I am doing well, thank you!' },
]);
});
])
})
it('should convert messages to Anthropic format with Peer object', () => {
const mockClient = {} as any;
const assistantPeer = new Peer('assistant', 'test-workspace', mockClient);
const mockClient = {} as any
const assistantPeer = new Peer('assistant', 'test-workspace', mockClient)
const anthropicMessages = sessionContext.toAnthropic(assistantPeer);
const anthropicMessages = sessionContext.toAnthropic(assistantPeer)
expect(anthropicMessages).toEqual([
{ role: 'assistant', content: 'Hello' },
{ role: 'user', content: 'user: Hi there' },
{ role: 'user', content: 'user: How are you?' },
{ role: 'assistant', content: 'I am doing well, thank you!' },
]);
});
])
})
it('should handle messages where assistant is different peer', () => {
const anthropicMessages = sessionContext.toAnthropic('different-assistant');
const anthropicMessages = sessionContext.toAnthropic(
'different-assistant'
)
expect(anthropicMessages).toEqual([
{ role: 'user', content: 'assistant: Hello' },
{ role: 'user', content: 'user: Hi there' },
{ role: 'user', content: 'user: How are you?' },
{ role: 'user', content: 'assistant: I am doing well, thank you!' },
]);
});
])
})
it('should handle empty messages array', () => {
const emptyContext = new SessionContext('session-id', []);
const anthropicMessages = emptyContext.toAnthropic('assistant');
const emptyContext = new SessionContext('session-id', [])
const anthropicMessages = emptyContext.toAnthropic('assistant')
expect(anthropicMessages).toEqual([]);
});
expect(anthropicMessages).toEqual([])
})
it('should include summary message when summary exists', () => {
const summary = createTestSummary('This is a summary');
const contextWithSummary = new SessionContext('test-session', mockMessages, summary);
const anthropicMessages = contextWithSummary.toAnthropic('assistant');
const summary = createTestSummary('This is a summary')
const contextWithSummary = new SessionContext(
'test-session',
mockMessages,
summary
)
const anthropicMessages = contextWithSummary.toAnthropic('assistant')
expect(anthropicMessages).toEqual([
{ role: 'user', content: '<summary>This is a summary</summary>' },
@ -226,157 +269,169 @@ describe('SessionContext', () => {
{ role: 'user', content: 'user: Hi there' },
{ role: 'user', content: 'user: How are you?' },
{ role: 'assistant', content: 'I am doing well, thank you!' },
]);
});
])
})
it('should handle messages with missing peer_id', () => {
const messagesWithMissingPeer = [
createTestMessage('msg1', 'Hello', 'assistant'),
createTestMessage('msg2', 'No peer', ''), // missing peer_id
createTestMessage('msg3', 'Another message', ''), // undefined peer_id
];
const context = new SessionContext('test', messagesWithMissingPeer);
]
const context = new SessionContext('test', messagesWithMissingPeer)
const anthropicMessages = context.toAnthropic('assistant');
const anthropicMessages = context.toAnthropic('assistant')
expect(anthropicMessages).toEqual([
{ role: 'assistant', content: 'Hello' },
{ role: 'user', content: ': No peer' },
{ role: 'user', content: ': Another message' },
]);
});
});
])
})
})
describe('length getter', () => {
it('should return correct message count', () => {
expect(sessionContext.length).toBe(4);
});
expect(sessionContext.length).toBe(4)
})
it('should return zero for empty messages', () => {
const emptyContext = new SessionContext('session-id', []);
expect(emptyContext.length).toBe(0);
});
const emptyContext = new SessionContext('session-id', [])
expect(emptyContext.length).toBe(0)
})
it('should return correct count for single message', () => {
const singleMessageContext = new SessionContext('session-id', [mockMessages[0]]);
expect(singleMessageContext.length).toBe(1);
});
});
const singleMessageContext = new SessionContext('session-id', [
mockMessages[0],
])
expect(singleMessageContext.length).toBe(1)
})
})
describe('toString', () => {
it('should return correct string representation', () => {
const result = sessionContext.toString();
expect(result).toBe('SessionContext(messages=4, summary=none)');
});
const result = sessionContext.toString()
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=none)');
});
const emptyContext = new SessionContext('session-id', [])
const result = emptyContext.toString()
expect(result).toBe('SessionContext(messages=0, summary=none)')
})
it('should handle large number of messages', () => {
const manyMessages = Array.from({ length: 1000 }, (_, i) =>
createTestMessage(`msg${i}`, `Message ${i}`, i % 2 === 0 ? 'assistant' : 'user')
);
const context = new SessionContext('session-id', manyMessages);
createTestMessage(
`msg${i}`,
`Message ${i}`,
i % 2 === 0 ? 'assistant' : 'user'
)
)
const context = new SessionContext('session-id', manyMessages)
const result = context.toString();
expect(result).toBe('SessionContext(messages=1000, summary=none)');
});
});
const result = context.toString()
expect(result).toBe('SessionContext(messages=1000, summary=none)')
})
})
describe('edge cases and error handling', () => {
it('should handle messages with null content', () => {
const messagesWithNullContent = [
createTestMessage('msg1', null as any, 'assistant'),
createTestMessage('msg2', undefined as any, 'user'),
];
const context = new SessionContext('test', messagesWithNullContent);
]
const context = new SessionContext('test', messagesWithNullContent)
const openAIMessages = context.toOpenAI('assistant');
const openAIMessages = context.toOpenAI('assistant')
expect(openAIMessages).toEqual([
{ role: 'assistant', content: null, name: 'assistant' },
{ role: 'user', content: undefined, name: 'user' },
]);
});
])
})
it('should handle messages with non-string content', () => {
const messagesWithNonStringContent = [
createTestMessage('msg1', 123 as any, 'assistant'),
createTestMessage('msg2', { text: 'object content' } as any, 'user'),
createTestMessage('msg3', true as any, 'assistant'),
];
const context = new SessionContext('test', messagesWithNonStringContent);
]
const context = new SessionContext('test', messagesWithNonStringContent)
const openAIMessages = context.toOpenAI('assistant');
const openAIMessages = context.toOpenAI('assistant')
expect(openAIMessages).toEqual([
{ role: 'assistant', content: 123, name: 'assistant' },
{ role: 'user', content: { text: 'object content' }, name: 'user' },
{ role: 'assistant', content: true, name: 'assistant' },
]);
});
])
})
it('should handle very long session IDs and summaries', () => {
const longSessionId = 'x'.repeat(1000);
const longSummaryText = 'Very long summary that goes on and on...'.repeat(100);
const longSummary = createTestSummary(longSummaryText);
const context = new SessionContext(longSessionId, mockMessages, longSummary);
const longSessionId = 'x'.repeat(1000)
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
});
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
})
it('should handle messages with additional properties', () => {
const messagesWithExtraProps = [
createTestMessage('msg1', 'Hello', 'assistant', {
timestamp: '2023-01-01T00:00:00Z',
metadata: { important: true },
extra_field: 'extra_value'
extra_field: 'extra_value',
}),
];
const context = new SessionContext('test', messagesWithExtraProps);
]
const context = new SessionContext('test', messagesWithExtraProps)
const openAIMessages = context.toOpenAI('assistant');
const openAIMessages = context.toOpenAI('assistant')
expect(openAIMessages).toEqual([
{ role: 'assistant', content: 'Hello', name: 'assistant' },
]);
});
])
})
it('should handle case-sensitive peer names', () => {
const caseMessages = [
createTestMessage('msg1', 'Hello', 'Assistant'),
createTestMessage('msg2', 'Hi', 'ASSISTANT'),
createTestMessage('msg3', 'Hey', 'assistant'),
];
const context = new SessionContext('test', caseMessages);
]
const context = new SessionContext('test', caseMessages)
const openAIMessages = context.toOpenAI('assistant');
const openAIMessages = context.toOpenAI('assistant')
expect(openAIMessages).toEqual([
{ role: 'user', content: 'Hello', name: 'Assistant' }, // 'Assistant' != 'assistant'
{ role: 'user', content: 'Hi', name: 'ASSISTANT' }, // 'ASSISTANT' != 'assistant'
{ role: 'assistant', content: 'Hey', name: 'assistant' }, // exact match
]);
});
])
})
it('should handle messages without id field', () => {
const messagesWithoutId = [
createTestMessage('', 'Message without ID', 'assistant'),
createTestMessage('', 'Another message', 'user'),
];
const context = new SessionContext('test', messagesWithoutId);
]
const context = new SessionContext('test', messagesWithoutId)
expect(context.length).toBe(2);
expect(context.length).toBe(2)
expect(context.toOpenAI('assistant')).toEqual([
{ role: 'assistant', content: 'Message without ID', name: 'assistant' },
{ role: 'user', content: 'Another message', name: 'user' },
]);
});
});
});
])
})
})
})

View File

@ -4,7 +4,7 @@
"": {
"name": "@honcho-ai/sdk",
"dependencies": {
"@honcho-ai/core": "^1.4.1-alpha.0",
"@honcho-ai/core": "^1.5.0",
"@types/node": "^24.0.1",
"zod": "4.0.0",
},
@ -108,7 +108,7 @@
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
"@honcho-ai/core": ["@honcho-ai/core@1.4.1-alpha.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-cRp3ug8cBe9E7NZDgCXkI0EHIgm8x5Jq/nQB0xA5629GuniS99yq6M786I//FjOZhB2YEZoOxd4Zkg412uk5Yg=="],
"@honcho-ai/core": ["@honcho-ai/core@1.5.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-kjYhCO0S9Ll3DfVpK8MetjY1PlD9FG6QGtudlMnSwfTWGzXFjL/OafpoE3SZ3c9BVY48JIbW9IttFUZFekv/xg=="],
"@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=="],

View File

@ -1,6 +1,6 @@
{
"name": "@honcho-ai/sdk",
"version": "1.4.1",
"version": "1.5.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.4.1-alpha.0",
"@honcho-ai/core": "^1.5.0",
"@types/node": "^24.0.1",
"zod": "4.0.0"
},

View File

@ -119,7 +119,7 @@ app = FastAPI(
title="Honcho API",
summary="The Identity Layer for the Agentic World",
description="""Honcho is a platform for giving agents user-centric memory and social cognition""",
version="2.3.3",
version="2.4.0",
contact={
"name": "Plastic Labs",
"url": "https://honcho.dev",

24
uv.lock
View File

@ -673,7 +673,7 @@ wheels = [
[[package]]
name = "honcho"
version = "2.3.3"
version = "2.4.0"
source = { virtual = "." }
dependencies = [
{ name = "alembic" },
@ -764,7 +764,7 @@ dev = [
[[package]]
name = "honcho-ai"
version = "1.4.1"
version = "1.5.0"
source = { editable = "sdks/python" }
dependencies = [
{ name = "honcho-core" },
@ -780,7 +780,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "honcho-core", specifier = ">=1.4.1a0" },
{ name = "honcho-core", specifier = ">=1.5.0" },
{ name = "httpx", specifier = ">=0.28.0,<1" },
{ name = "pydantic", specifier = ">=2.0.0,<3" },
{ name = "typing-extensions", marker = "python_full_version < '3.12'", specifier = ">=4.12.0" },
@ -791,7 +791,7 @@ dev = [{ name = "ruff", specifier = ">=0.11.13" }]
[[package]]
name = "honcho-core"
version = "1.4.1a0"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@ -801,9 +801,9 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2d/2f/fbf3ca017ffcdaef92e26fe84b77268c1c0b26f95e282d8684f50932d81a/honcho_core-1.4.1a0.tar.gz", hash = "sha256:e0023f46c65500d727e3fb1275781ae1e0b162c226a01352afd45c421c5f350b", size = 130826, upload-time = "2025-10-01T03:23:27.143Z" }
sdist = { url = "https://files.pythonhosted.org/packages/75/cd/05d2a21afd037673637e390de411cd66ca56b07e524cd7ead65c655a2b49/honcho_core-1.5.0.tar.gz", hash = "sha256:4876195dad16db437117d40a1d5e34ff88974e8eca6d093f3653b4ac2bda3c6d", size = 132236, upload-time = "2025-10-08T18:30:15.777Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/5f/2aa746b7fd92690ef782dcacc56a5c47a19a999b9729597c04c7cc509e52/honcho_core-1.4.1a0-py3-none-any.whl", hash = "sha256:8b4c86e3ac3964ff56fe6a449b9fa23be49530609c34bdf35c9ecde7cd871bba", size = 121704, upload-time = "2025-10-01T03:23:25.66Z" },
{ url = "https://files.pythonhosted.org/packages/ef/4e/9ef193bba00c521be1152791d2ba37a98bdb58ece8f8ce7863b710a4f511/honcho_core-1.5.0-py3-none-any.whl", hash = "sha256:01db345371d6e80230b202c797a73f9f086dc244936522dbc39efb05f4cee306", size = 123210, upload-time = "2025-10-08T18:30:14.277Z" },
]
[[package]]
@ -1564,18 +1564,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145, upload-time = "2025-09-18T20:47:23.875Z" },
]
[[package]]
name = "proto-plus"
version = "1.26.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f4/ac/87285f15f7cce6d4a008f33f1757fb5a13611ea8914eb58c3d0d26243468/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012", size = 56142, upload-time = "2025-03-10T15:54:38.843Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/6d/280c4c2ce28b1593a19ad5239c8b826871fc6ec275c21afc8e1820108039/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", size = 50163, upload-time = "2025-03-10T15:54:37.335Z" },
]
[[package]]
name = "protobuf"
version = "5.29.5"