import type { AggressiveConfig, CompressionStats, Summarizer } from "./types.ts"; import { DEFAULT_AGGRESSIVE_CONFIG } from "./types.ts"; import { compressToolResult, compressAnthropicToolResultBlock, isAnthropicToolResultBlock, } from "./toolResultCompressor.ts"; import { applyAging } from "./progressiveAging.ts"; import { RuleBasedSummarizer } from "./summarizer.ts"; import { cavemanCompress } from "./caveman.ts"; import { applyLiteCompression } from "./lite.ts"; import { extractTextContent, replaceTextContent, type ChatMessageLike } from "./messageContent.ts"; const COMPRESSED_MARKER_RE = /^\[COMPRESSED:/; type ChatMessage = ChatMessageLike; interface AggressiveCompressionResult { messages: ChatMessage[]; stats: CompressionStats; } function setContent(msg: ChatMessage, newContent: string): ChatMessage { return replaceTextContent(msg, newContent) as ChatMessage; } function estimateTokens(text: string): number { return Math.ceil(text.length / 4); } export function compressAggressive( messages: ChatMessage[], config?: Partial, stats?: CompressionStats ): AggressiveCompressionResult { const cfg: AggressiveConfig = { ...DEFAULT_AGGRESSIVE_CONFIG, ...config, thresholds: { ...DEFAULT_AGGRESSIVE_CONFIG.thresholds, ...(config?.thresholds ?? {}) }, toolStrategies: { ...DEFAULT_AGGRESSIVE_CONFIG.toolStrategies, ...(config?.toolStrategies ?? {}), }, }; const summarizer: Summarizer = new RuleBasedSummarizer(); const resultStats: CompressionStats = stats ?? { originalTokens: 0, compressedTokens: 0, savingsPercent: 0, techniquesUsed: [], mode: "aggressive", timestamp: Date.now(), }; const originalTokens = messages.reduce( (sum, m) => sum + estimateTokens(extractTextContent(m.content)), 0 ); resultStats.originalTokens = originalTokens; let currentMessages = [...messages]; let summarizerSavings = 0; let toolResultSavings = 0; let agingSavings = 0; // Step 1: Tool-result compression try { const afterToolResult = currentMessages.map((msg) => { if (cfg.preserveSystemPrompt !== false && msg.role === "system") return msg; // OpenAI-shape: a dedicated tool/function message whose content is the result text. if (msg.role === "tool" || msg.role === "function") { const text = extractTextContent(msg.content); if (!text || COMPRESSED_MARKER_RE.test(text)) return msg; const result = compressToolResult(text, cfg.toolStrategies); if (result.strategy === "none" || result.saved <= 0) return msg; toolResultSavings += result.saved; return setContent(msg, result.compressed); } // Anthropic-shape: `tool_result` content blocks live inside a (typically user) // message's content array. Compress the text inside each block while preserving // the tool_use_id and block structure exactly (B-AGG-ANTHROPIC-TR). if (!Array.isArray(msg.content)) return msg; if (!msg.content.some(isAnthropicToolResultBlock)) return msg; let blockSavings = 0; const nextContent = msg.content.map((part) => { if (!isAnthropicToolResultBlock(part)) return part; const { block, saved } = compressAnthropicToolResultBlock(part, cfg.toolStrategies); blockSavings += saved; return block; }); if (blockSavings <= 0) return msg; toolResultSavings += blockSavings; return { ...msg, content: nextContent }; }); currentMessages = afterToolResult; } catch (err) { // Downgrade: skip tool-result compression, continue pipeline } // Step 2: Progressive aging try { const agingResult = applyAging( currentMessages, cfg.thresholds, summarizer, cfg.preserveSystemPrompt !== false ); agingSavings = agingResult.saved; currentMessages = agingResult.messages as ChatMessage[]; } catch (err) { // Downgrade: skip aging, continue with current messages } // Step 3: Fallback summarizer for remaining long messages if (cfg.summarizerEnabled) { try { currentMessages = currentMessages.map((msg) => { if (cfg.preserveSystemPrompt !== false && msg.role === "system") return msg; const text = extractTextContent(msg.content); if (!text || COMPRESSED_MARKER_RE.test(text)) return msg; if (text.length <= cfg.maxTokensPerMessage * 4) return msg; const summary = summarizer.summarize([msg], { maxLen: cfg.maxTokensPerMessage, preserveCode: true, }); if (summary && summary.length < text.length) { summarizerSavings += estimateTokens(text) - estimateTokens(summary); return setContent(msg, `[COMPRESSED:summary] ${summary}`); } return msg; }); } catch (err) { // Downgrade: skip fallback summarizer } } // Downgrade chain: if total savings < threshold, try caveman then lite const compressedTokens = currentMessages.reduce( (sum, m) => sum + estimateTokens(extractTextContent(m.content)), 0 ); resultStats.compressedTokens = compressedTokens; resultStats.savingsPercent = originalTokens > 0 ? ((originalTokens - compressedTokens) / originalTokens) * 100 : 0; if (resultStats.savingsPercent < cfg.minSavingsThreshold * 100) { try { const cavemanResult = cavemanCompress({ messages: currentMessages as unknown as Parameters[0]["messages"] }); if (cavemanResult?.compressed && cavemanResult.stats) { const cavemanSavings = cavemanResult.stats.savingsPercent ?? 0; if (cavemanSavings > resultStats.savingsPercent) { currentMessages = (cavemanResult.body?.messages ?? currentMessages) as ChatMessage[]; resultStats.compressedTokens = cavemanResult.stats.compressedTokens ?? compressedTokens; resultStats.savingsPercent = cavemanSavings; resultStats.techniquesUsed.push("caveman-fallback"); } } } catch (err) { // Caveman failed, try lite } try { const liteResult = applyLiteCompression( { messages: currentMessages }, { preserveSystemPrompt: cfg.preserveSystemPrompt !== false } ); if (liteResult?.compressed && liteResult.stats) { const liteSavings = liteResult.stats.savingsPercent ?? 0; if (liteSavings > resultStats.savingsPercent) { currentMessages = (liteResult.body?.messages ?? currentMessages) as ChatMessage[]; resultStats.compressedTokens = liteResult.stats.compressedTokens ?? compressedTokens; resultStats.savingsPercent = liteSavings; resultStats.techniquesUsed.push("lite-fallback"); } } } catch (err) { // Lite also failed, return current messages as-is } } resultStats.techniquesUsed.push( ...(toolResultSavings > 0 ? ["toolResult"] : []), ...(agingSavings > 0 ? ["aging"] : []), ...(summarizerSavings > 0 ? ["summarizer"] : []) ); resultStats.aggressive = { summarizerSavings, toolResultSavings, agingSavings, }; return { messages: currentMessages, stats: resultStats }; } export { DEFAULT_AGGRESSIVE_CONFIG };