feat: Release PR for v2.4.0 (#229)

This commit is contained in:
Vineeth Voruganti 2025-10-09 16:57:50 -04:00 committed by GitHub
parent 68acf38134
commit d094ef8e10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 4940 additions and 3835 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

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

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

View File

@ -673,7 +673,7 @@ wheels = [
[[package]]
name = "honcho"
version = "2.3.3"
version = "2.4.0"
source = { virtual = "." }
dependencies = [
{ name = "alembic" },