feat(session): add optional end-of-session summarization

This commit is contained in:
William Valentin
2026-02-16 13:18:42 -08:00
parent 01ee6ba53f
commit 3f627cc1ad
8 changed files with 307 additions and 0 deletions
+51
View File
@@ -22,6 +22,7 @@ const mockSessionManager = {
};
const mockModelClient = {
getClient: vi.fn(() => mockModelClient),
chat: vi.fn(async () => ({
content: 'test',
stopReason: 'end_turn',
@@ -103,6 +104,56 @@ describe('SessionBridge', () => {
expect(bridge.getAgent('conn-1')).toBeUndefined();
});
it('runs session-end summary on final disconnect when enabled', async () => {
mockSession.getHistory.mockReturnValueOnce([
{ role: 'user', content: 'Please remember my preference for concise updates.' },
{ role: 'assistant', content: 'Noted.' },
] as any);
const memoryStore = {
write: vi.fn(),
};
const bridge = new SessionBridge({
sessionManager: mockSessionManager as unknown as SessionBridgeConfig['sessionManager'],
modelClient: mockModelClient,
systemPrompt: 'test prompt',
toolRegistry: mockToolRegistry as unknown as SessionBridgeConfig['toolRegistry'],
toolExecutor: mockToolExecutor as unknown as SessionBridgeConfig['toolExecutor'],
memoryStore: memoryStore as unknown as SessionBridgeConfig['memoryStore'],
config: {
agents: {
primary_tier: 'default',
delegation: {
compaction: 'fast',
memory_extraction: 'fast',
classification: 'fast',
tool_summarisation: 'fast',
complex_reasoning: 'complex',
},
max_delegation_depth: 3,
},
compaction: { enabled: false },
models: { default: { provider: 'anthropic', model: 'claude-3-haiku' } },
sessions: {
ttl: '30d',
end_summary: {
enabled: true,
tier: 'fast',
max_messages: 50,
max_input_chars: 20000,
max_tokens: 256,
write_to_memory: true,
memory_namespace: 'session/summaries',
},
},
} as unknown as SessionBridgeConfig['config'],
});
bridge.connect('conn-end-summary');
bridge.disconnect('conn-end-summary');
await new Promise(resolve => setTimeout(resolve, 0));
expect(memoryStore.write).toHaveBeenCalled();
});
it('tracks busy state', () => {
const bridge = createBridge();
bridge.connect('conn-1');
+24
View File
@@ -8,6 +8,7 @@ import { AgentOrchestrator, type DelegationConfig } from '../backends/native/orc
import type { ToolUseEvent } from '../backends/native/agent.js';
import type { MemoryStore } from '../memory/store.js';
import type { Config } from '../config/index.js';
import { summarizeSessionOnEnd, type SessionEndSummaryConfig } from '../session/endSummary.js';
export interface SessionBridgeConfig {
sessionManager: SessionManager;
@@ -59,6 +60,29 @@ export class SessionBridge {
const otherClients = Array.from(this.clients.values())
.filter(c => c.sessionId === client.sessionId && c.connectionId !== connectionId);
if (otherClients.length === 0) {
const agent = this.agents.get(client.sessionId);
const summaryConfig = this.config.config?.sessions?.end_summary;
if (agent && summaryConfig?.enabled) {
const history = agent.getHistory();
const mappedConfig: SessionEndSummaryConfig = {
enabled: summaryConfig.enabled,
tier: summaryConfig.tier,
maxMessages: summaryConfig.max_messages,
maxInputChars: summaryConfig.max_input_chars,
maxTokens: summaryConfig.max_tokens,
writeToMemory: summaryConfig.write_to_memory,
memoryNamespace: summaryConfig.memory_namespace,
};
void summarizeSessionOnEnd({
agent,
sessionId: client.sessionId,
history,
config: mappedConfig,
memoryStore: this.config.memoryStore,
}).catch((error) => {
console.warn('Session end summary failed:', error);
});
}
this.agents.delete(client.sessionId);
}
this.clients.delete(connectionId);