@mrtkrcm/acp-claude-code
Version:
ACP (Agent Client Protocol) bridge for Claude Code
278 lines • 11.4 kB
JavaScript
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