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
+88
View File
@@ -0,0 +1,88 @@
import type { AgentOrchestrator } from '../backends/native/orchestrator.js';
import type { Message } from '../models/types.js';
import { getMessageText } from '../models/media.js';
import type { MemoryStore } from '../memory/store.js';
import type { ModelTier } from '../models/router.js';
export interface SessionEndSummaryConfig {
enabled: boolean;
tier: ModelTier;
maxMessages: number;
maxInputChars: number;
maxTokens: number;
writeToMemory: boolean;
memoryNamespace: string;
}
export interface SessionEndSummaryResult {
summary: string;
namespace?: string;
}
const SESSION_END_SUMMARY_PROMPT = [
'Summarize this completed conversation for future continuity.',
'Focus on durable facts, decisions, preferences, unresolved tasks, and next steps.',
'Be concise and structured with short bullets.',
'Do not include private secrets.',
].join(' ');
function sanitizeSessionId(sessionId: string): string {
return sessionId
.replace(/:/g, '/')
.replace(/[^a-zA-Z0-9/_-]+/g, '_')
.replace(/_+/g, '_');
}
function formatHistory(messages: Message[], maxInputChars: number): string {
const text = messages
.map((msg) => `${msg.role}: ${getMessageText(msg)}`)
.join('\n\n');
if (text.length <= maxInputChars) {
return text;
}
return text.slice(text.length - maxInputChars);
}
export async function summarizeSessionOnEnd(opts: {
agent: AgentOrchestrator;
sessionId: string;
history: Message[];
config: SessionEndSummaryConfig;
memoryStore?: MemoryStore;
}): Promise<SessionEndSummaryResult | null> {
if (!opts.config.enabled || opts.history.length === 0) {
return null;
}
const selected = opts.history.slice(-opts.config.maxMessages);
const conversation = formatHistory(selected, opts.config.maxInputChars);
if (!conversation.trim()) {
return null;
}
const result = await opts.agent.delegate({
tier: opts.config.tier,
systemPrompt: SESSION_END_SUMMARY_PROMPT,
message: conversation,
maxTokens: opts.config.maxTokens,
});
const summary = result.content.trim();
if (!summary) {
return null;
}
let namespace: string | undefined;
if (opts.memoryStore && opts.config.writeToMemory) {
namespace = `${opts.config.memoryNamespace}/${sanitizeSessionId(opts.sessionId)}`;
const block = `## ${new Date().toISOString()}\n\n${summary}\n\n`;
opts.memoryStore.write(namespace, block, 'append');
}
return { summary, namespace };
}
export const sessionEndSummaryInternals = {
sanitizeSessionId,
formatHistory,
};