Files
flynn/src/context/compaction.ts
T
William Valentin 7a35b22458 feat: wire up all Phase 2-6 features into daemon and config
Integrate all new features into the shared infrastructure:
- Config schema: add memory, discord, slack, process, web_search schemas
- Daemon wiring: memory store init, tool registration, channel adapters
- Orchestrator: memory injection into system prompt, extraction on compaction
- Agent: add setSystemPrompt() for dynamic prompt updates
- Channel/tool index: export new adapters and tool factories
- Add @slack/bolt, discord.js, turndown, linkedom, @mozilla/readability deps
- Update state.json with Phase 3b completion (494 tests passing)
2026-02-06 14:24:39 -08:00

101 lines
3.5 KiB
TypeScript

import type { Message } from '../models/types.js';
import type { AgentOrchestrator } from '../backends/native/orchestrator.js';
import type { MemoryStore } from '../memory/store.js';
import { COMPACTION_SYSTEM_PROMPT, MEMORY_EXTRACTION_PROMPT } from '../backends/native/prompts.js';
import { estimateMessageTokens } from './tokens.js';
export interface CompactionConfig {
/** Percentage of context window that triggers compaction (default: 80). */
thresholdPct: number;
/** Number of recent turns (user+assistant pairs) to always keep intact. */
keepTurns: number;
/** Maximum tokens for the compaction summary response. */
summaryMaxTokens: number;
}
export interface CompactionResult {
/** The compacted messages: [summary, ...recentMessages]. */
messages: Message[];
/** Number of messages that were compacted (removed). */
compactedCount: number;
/** Estimated tokens before compaction. */
tokensBefore: number;
/** Estimated tokens after compaction. */
tokensAfter: number;
}
export const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {
thresholdPct: 80,
keepTurns: 4,
summaryMaxTokens: 1024,
};
export async function compactHistory(opts: {
messages: Message[];
orchestrator: AgentOrchestrator;
config: CompactionConfig;
memoryStore?: MemoryStore;
autoExtract?: boolean;
}): Promise<CompactionResult> {
const { messages, orchestrator, config } = opts;
const keepCount = config.keepTurns * 2;
if (messages.length <= keepCount) {
return {
messages,
compactedCount: 0,
tokensBefore: estimateMessageTokens(messages),
tokensAfter: estimateMessageTokens(messages),
};
}
const toCompact = messages.slice(0, -keepCount);
const toKeep = messages.slice(-keepCount);
const formattedConversation = toCompact.map((msg) => `${msg.role}: ${msg.content}`).join('\n\n');
const tier = orchestrator.getDelegationTier('compaction');
const result = await orchestrator.delegate({
tier,
systemPrompt: COMPACTION_SYSTEM_PROMPT,
message: formattedConversation,
maxTokens: config.summaryMaxTokens,
});
const summaryMessage: Message = {
role: 'assistant',
content: '[Summary of earlier conversation]\n\n' + result.content,
};
// Phase 2: Extract persistent facts and append to memory (if enabled)
if (opts.memoryStore && opts.autoExtract !== false) {
try {
const extractionTier = orchestrator.getDelegationTier('memory_extraction');
const extraction = await orchestrator.delegate({
tier: extractionTier,
systemPrompt: MEMORY_EXTRACTION_PROMPT,
message: `Extract persistent facts from this conversation:\n\n${formattedConversation}`,
maxTokens: 512,
});
// Only write if the extraction produced meaningful content
const extractedContent = extraction.content.trim();
if (extractedContent.length > 0 && !extractedContent.toLowerCase().includes('no facts')) {
opts.memoryStore.write('global', extractedContent, 'append');
console.log(`[Flynn:memory] Extracted ${extractedContent.length} chars of facts to global memory`);
}
} catch (error) {
// Memory extraction is best-effort — don't fail compaction if it errors
console.warn('[Flynn:memory] Failed to extract facts during compaction:', error);
}
}
return {
messages: [summaryMessage, ...toKeep],
compactedCount: toCompact.length,
tokensBefore: estimateMessageTokens(messages),
tokensAfter: estimateMessageTokens([summaryMessage, ...toKeep]),
};
}