UNPKG

@mrtkrcm/acp-claude-code

Version:

ACP (Agent Client Protocol) bridge for Claude Code

278 lines 11.4 kB
import { createLogger } from './logger.js'; export class ContextOptimizer { sessionContexts = new Map(); contextSummaries = new Map(); logger; contextMonitor; // Pre-defined optimization strategies strategies = { aggressive: { name: 'aggressive', maxTokens: 150000, // 75% of limit prioritizeRecent: true, preserveTools: true, compressionRatio: 0.3, }, balanced: { name: 'balanced', maxTokens: 160000, // 80% of limit prioritizeRecent: true, preserveTools: true, compressionRatio: 0.5, }, conservative: { name: 'conservative', maxTokens: 180000, // 90% of limit prioritizeRecent: false, preserveTools: true, compressionRatio: 0.7, }, }; constructor(contextMonitor) { this.logger = createLogger('Context-Optimizer'); this.contextMonitor = contextMonitor; } /** * Add a new context chunk to a session */ addContextChunk(sessionId, chunk) { const chunks = this.sessionContexts.get(sessionId) || []; chunks.push(chunk); this.sessionContexts.set(sessionId, chunks); this.logger.debug(`Added context chunk to session ${sessionId}. Total chunks: ${chunks.length}`); } /** * Optimize context for a session based on current usage */ optimizeContext(sessionId, strategy = 'balanced') { const chunks = this.sessionContexts.get(sessionId) || []; const strategyConfig = this.strategies[strategy]; if (chunks.length === 0) { return { optimizedContext: '', stats: { originalTokens: 0, optimizedTokens: 0, chunksRemoved: 0, compressionRatio: 1, } }; } const originalTokens = chunks.reduce((sum, chunk) => sum + chunk.tokenCount, 0); // If we're under the limit, no optimization needed if (originalTokens <= strategyConfig.maxTokens) { const optimizedContext = chunks.map(chunk => chunk.content).join('\n'); return { optimizedContext, stats: { originalTokens, optimizedTokens: originalTokens, chunksRemoved: 0, compressionRatio: 1, } }; } this.logger.info(`Context optimization needed for session ${sessionId}. Original: ${originalTokens} tokens, Target: ${strategyConfig.maxTokens}`); // Apply optimization strategy const optimizedChunks = this.applyOptimizationStrategy(chunks, strategyConfig); const optimizedTokens = optimizedChunks.reduce((sum, chunk) => sum + chunk.tokenCount, 0); const optimizedContext = optimizedChunks.map(chunk => chunk.content).join('\n'); const compressionRatio = optimizedTokens / originalTokens; this.logger.info(`Context optimized: ${originalTokens}${optimizedTokens} tokens (${(compressionRatio * 100).toFixed(1)}% retained)`); return { optimizedContext, stats: { originalTokens, optimizedTokens, chunksRemoved: chunks.length - optimizedChunks.length, compressionRatio, } }; } applyOptimizationStrategy(chunks, strategy) { // Step 1: Sort chunks by importance and recency const sortedChunks = [...chunks].sort((a, b) => { // Always preserve system and critical tool results if (a.type === 'system' && b.type !== 'system') return -1; if (b.type === 'system' && a.type !== 'system') return 1; if (strategy.preserveTools) { if (a.type === 'tool_result' && b.type !== 'tool_result') return -1; if (b.type === 'tool_result' && a.type !== 'tool_result') return 1; } // Then by importance and recency const importanceScore = b.importance - a.importance; if (Math.abs(importanceScore) > 0.1) { return importanceScore; } return strategy.prioritizeRecent ? b.timestamp - a.timestamp : a.timestamp - b.timestamp; }); // Step 2: Select chunks within token budget const selectedChunks = []; let currentTokens = 0; const targetTokens = strategy.maxTokens; for (const chunk of sortedChunks) { if (currentTokens + chunk.tokenCount <= targetTokens) { selectedChunks.push(chunk); currentTokens += chunk.tokenCount; } else if (selectedChunks.length === 0) { // Must include at least one chunk, even if it exceeds the limit selectedChunks.push(chunk); currentTokens = chunk.tokenCount; break; } } // Step 3: Resolve dependencies const resolvedChunks = this.resolveDependencies(selectedChunks, chunks); // Step 4: Apply compression if configured if (strategy.compressionRatio && strategy.compressionRatio < 1) { return this.compressChunks(resolvedChunks, strategy.compressionRatio); } return resolvedChunks.sort((a, b) => a.timestamp - b.timestamp); } resolveDependencies(selected, allChunks) { const selectedIds = new Set(selected.map(chunk => chunk.id)); const resolved = new Set(selected); const allChunksMap = new Map(allChunks.map(chunk => [chunk.id, chunk])); let changed = true; while (changed) { changed = false; for (const chunk of resolved) { if (chunk.dependencies) { for (const depId of chunk.dependencies) { if (!selectedIds.has(depId)) { const depChunk = allChunksMap.get(depId); if (depChunk) { resolved.add(depChunk); selectedIds.add(depId); changed = true; } } } } } } return Array.from(resolved); } compressChunks(chunks, ratio) { // Simple compression strategy: summarize older, less important chunks const compressed = []; const toCompress = []; for (const chunk of chunks) { // Don't compress system messages or recent high-importance chunks if (chunk.type === 'system' || chunk.importance > 0.8 || chunk.timestamp > Date.now() - 300000) { // Last 5 minutes compressed.push(chunk); } else { toCompress.push(chunk); } } if (toCompress.length > 0) { const summarized = this.createSummaryChunk(toCompress, ratio); compressed.push(summarized); } return compressed.sort((a, b) => a.timestamp - b.timestamp); } createSummaryChunk(chunks, ratio) { const originalContent = chunks.map(c => c.content).join('\n\n'); const originalTokens = chunks.reduce((sum, c) => sum + c.tokenCount, 0); const targetTokens = Math.floor(originalTokens * ratio); // Simple summarization - in practice, this could use an LLM const summary = this.extractivelyCompress(originalContent, targetTokens); return { id: `summary_${Date.now()}`, content: `[SUMMARY OF ${chunks.length} MESSAGES]\n${summary}`, type: 'system', timestamp: Date.now(), importance: 0.5, tokenCount: Math.ceil(summary.length / 4), }; } extractivelyCompress(content, targetTokens) { const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 0); const targetChars = targetTokens * 4; // Rough token-to-char conversion // Score sentences by length and position const scoredSentences = sentences.map((sentence, index) => ({ sentence: sentence.trim(), score: sentence.length * (1 - index / sentences.length), // Prefer longer, earlier sentences })); scoredSentences.sort((a, b) => b.score - a.score); let summary = ''; for (const { sentence } of scoredSentences) { if (summary.length + sentence.length > targetChars) break; summary += sentence + '. '; } return summary || content.substring(0, targetChars); } /** * Analyze context usage and recommend optimization */ analyzeContextHealth(sessionId) { const chunks = this.sessionContexts.get(sessionId) || []; const totalTokens = chunks.reduce((sum, chunk) => sum + chunk.tokenCount, 0); const now = Date.now(); const stats = { totalTokens, chunkCount: chunks.length, avgChunkSize: chunks.length > 0 ? totalTokens / chunks.length : 0, oldestChunk: chunks.length > 0 ? now - Math.min(...chunks.map(c => c.timestamp)) : 0, }; const recommendations = []; let status = 'healthy'; if (totalTokens > 180000) { status = 'critical'; recommendations.push('Immediate context optimization required'); recommendations.push('Consider starting a new session'); } else if (totalTokens > 160000) { status = 'warning'; recommendations.push('Context optimization recommended'); recommendations.push('Consider using aggressive optimization strategy'); } if (chunks.length > 100) { recommendations.push('High chunk count - consider summarization'); } if (stats.oldestChunk > 3600000) { // 1 hour recommendations.push('Context contains very old chunks - consider cleanup'); } return { status, recommendations, stats }; } /** * Clear context for a session */ clearSessionContext(sessionId) { this.sessionContexts.delete(sessionId); this.contextSummaries.delete(sessionId); this.logger.info(`Cleared context for session ${sessionId}`); } /** * Get optimization statistics */ getOptimizationStats() { let totalChunks = 0; let totalTokens = 0; let sessionsNeedingOptimization = 0; for (const chunks of this.sessionContexts.values()) { totalChunks += chunks.length; const sessionTokens = chunks.reduce((sum, chunk) => sum + chunk.tokenCount, 0); totalTokens += sessionTokens; if (sessionTokens > 160000) { sessionsNeedingOptimization++; } } return { totalSessions: this.sessionContexts.size, totalChunks, totalTokens, sessionsNeedingOptimization, }; } } //# sourceMappingURL=context-optimizer.js.map