"use strict";
// =============================================================================
// Pure Agent - Context Compressor
// Ported from Hermes agent/context_compressor.py
// 4-phase compression: tool pruning → boundary detection → LLM summary → assembly
// =============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.ContextCompressor = void 0;
const utils_1 = require("./utils");
const logger_1 = require("./logger");
const constants_1 = require("./constants");
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const SUMMARY_PREFIX = '[CONTEXT COMPACTION -- REFERENCE ONLY] Earlier turns were compacted. The summary below preserves key facts, decisions, and state. Respond to the latest user message, not the summary.';
const PRUNED_PLACEHOLDER = '[Old tool output cleared to save context space]';
const MIN_SUMMARY_TOKENS = 2000;
const SUMMARY_RATIO = 0.20;
const SUMMARY_TOKENS_CEILING = 12000;
const FALLBACK_SUMMARY_MAX_CHARS = 8000;
const FALLBACK_TURN_MAX_CHARS = 700;
const CHARS_PER_TOKEN = 4;
class ContextCompressor {
    _contextLength;
    _protectFirstN;
    _protectLastN;
    _compressionCount = 0;
    _previousSummary = null;
    _ineffectiveCount = 0;
    _lastSavingsPct = 0;
    constructor(contextLength, protectFirstN = constants_1.COMPRESSION_PROTECT_FIRST_N, protectLastN = constants_1.COMPRESSION_PROTECT_LAST_N) {
        this._contextLength = contextLength;
        this._protectFirstN = protectFirstN;
        this._protectLastN = protectLastN;
    }
    // -------------------------------------------------------------------------
    // Should compress?
    // -------------------------------------------------------------------------
    shouldCompress(messages) {
        const tokens = (0, utils_1.estimateMessagesTokens)(messages);
        const threshold = this._contextLength * constants_1.COMPRESSION_THRESHOLD_RATIO;
        if (tokens < threshold)
            return false;
        // Anti-thrashing: if last 2 compressions saved <10%, stop
        if (this._ineffectiveCount >= 2) {
            (0, logger_1.getLogger)().warn('Compression ineffective (last 2 saved <10%), skipping');
            return false;
        }
        return true;
    }
    // -------------------------------------------------------------------------
    // Main compress method
    // -------------------------------------------------------------------------
    async compress(messages, llmCall) {
        const originalTokens = (0, utils_1.estimateMessagesTokens)(messages);
        (0, logger_1.getLogger)().info(`Compressing: ${messages.length} messages, ~${originalTokens} tokens`);
        // Phase 1: Cheap tool output pruning
        const pruned = this._pruneToolOutputs(messages);
        // Phase 2: Boundary detection
        const { head, middle, tail } = this._detectBoundaries(pruned);
        if (middle.length === 0) {
            (0, logger_1.getLogger)().info('Nothing to compress (middle is empty)');
            return { messages: pruned, summary: '', removedCount: 0, savings: 0 };
        }
        // Phase 3: Generate summary
        let summary;
        if (llmCall) {
            summary = await this._generateSummary(middle, llmCall);
        }
        else {
            summary = this._buildFallbackSummary(middle);
        }
        // Phase 4: Assembly
        const assembled = this._assemble(head, summary, tail);
        const newTokens = (0, utils_1.estimateMessagesTokens)(assembled);
        const savings = originalTokens > 0 ? Math.round((1 - newTokens / originalTokens) * 100) : 0;
        this._compressionCount++;
        this._previousSummary = summary;
        // Anti-thrashing tracking
        if (savings < 10) {
            this._ineffectiveCount++;
        }
        else {
            this._ineffectiveCount = 0;
        }
        this._lastSavingsPct = savings;
        (0, logger_1.getLogger)().info(`Compressed: ${messages.length} -> ${assembled.length} messages, savings: ${savings}%`);
        return {
            messages: assembled,
            summary,
            removedCount: middle.length,
            savings,
        };
    }
    // -------------------------------------------------------------------------
    // Phase 1: Prune old tool outputs
    // -------------------------------------------------------------------------
    _pruneToolOutputs(messages) {
        const result = [];
        const tailStart = Math.max(0, messages.length - this._protectLastN);
        const seenHashes = new Map(); // hash -> first occurrence index
        for (let i = 0; i < messages.length; i++) {
            const msg = messages[i];
            // Protect head and tail
            if (i < this._protectFirstN + 1 || i >= tailStart) {
                result.push(msg);
                continue;
            }
            // Only prune tool results
            if (msg.role !== 'tool') {
                result.push(msg);
                continue;
            }
            const content = (0, utils_1.getTextContent)(msg.content);
            // Dedup: if same content seen before, replace with placeholder
            const hash = simpleHash(content);
            if (seenHashes.has(hash)) {
                result.push({ ...msg, content: PRUNED_PLACEHOLDER });
                continue;
            }
            seenHashes.set(hash, i);
            // Summarize long tool results
            if (content.length > 200) {
                const toolName = msg.name || 'tool';
                const summary = summarizeToolResult(toolName, content);
                result.push({ ...msg, content: summary });
            }
            else {
                result.push(msg);
            }
        }
        return result;
    }
    // -------------------------------------------------------------------------
    // Phase 2: Boundary detection
    // -------------------------------------------------------------------------
    _detectBoundaries(messages) {
        const n = messages.length;
        // Head: system prompt + first N messages
        const headEnd = Math.min(this._protectFirstN + 1, n); // +1 for system prompt
        // Tail: last N messages
        const tailStart = Math.max(headEnd, n - this._protectLastN);
        // Ensure we don't split tool_call/tool_result pairs
        const alignedTailStart = this._alignBoundary(messages, tailStart, headEnd);
        return {
            head: messages.slice(0, headEnd),
            middle: messages.slice(headEnd, alignedTailStart),
            tail: messages.slice(alignedTailStart),
        };
    }
    _alignBoundary(messages, idx, minIdx) {
        // Don't split a tool result from its preceding tool call
        if (idx > minIdx && idx < messages.length) {
            const msg = messages[idx];
            if (msg.role === 'tool' && idx > 0) {
                // Move forward past tool results
                while (idx < messages.length && messages[idx].role === 'tool') {
                    idx++;
                }
            }
            // Also check if the message before the boundary is an assistant with tool_calls
            // but the tool results are in the middle
            if (idx > minIdx) {
                const prev = messages[idx - 1];
                if (prev.role === 'assistant' && prev.tool_calls && prev.tool_calls.length > 0) {
                    // Check if tool results follow in the middle section
                    let hasOrphan = false;
                    for (let j = idx; j < messages.length; j++) {
                        if (messages[j].role === 'tool') {
                            hasOrphan = true;
                            break;
                        }
                        if (messages[j].role === 'user')
                            break;
                    }
                    if (hasOrphan) {
                        // Move boundary to include the tool results
                        while (idx < messages.length && messages[idx].role === 'tool') {
                            idx++;
                        }
                    }
                }
            }
        }
        return Math.max(idx, minIdx);
    }
    // -------------------------------------------------------------------------
    // Phase 3: Generate summary
    // -------------------------------------------------------------------------
    async _generateSummary(middle, llmCall) {
        const serialized = this._serializeForSummary(middle);
        const budget = Math.max(MIN_SUMMARY_TOKENS, Math.min((0, utils_1.estimateTokens)(serialized) * SUMMARY_RATIO, SUMMARY_TOKENS_CEILING));
        const prompt = [
            {
                role: 'system',
                content: `You are a conversation summarizer. Summarize the following conversation segment concisely, preserving key facts, decisions, and context needed for continuation.

Structure your summary with these sections:
- **Active Task**: What is being worked on
- **Completed Actions**: What was done (tool calls, files modified)
- **Key Decisions**: Important choices made
- **Relevant Files**: File paths mentioned or modified
- **Remaining Work**: What still needs to be done
- **Critical Context**: Any blockers, errors, or constraints

Keep the summary under ${budget} tokens. Be factual and specific.`,
            },
            {
                role: 'user',
                content: this._previousSummary
                    ? `Previous summary:\n${this._previousSummary}\n\nNew conversation to merge:\n${serialized}`
                    : `Conversation to summarize:\n${serialized}`,
            },
        ];
        try {
            const result = await llmCall(prompt);
            return result || this._buildFallbackSummary(middle);
        }
        catch (err) {
            (0, logger_1.getLogger)().warn(`Summary generation failed: ${err.message}, using fallback`);
            return this._buildFallbackSummary(middle);
        }
    }
    _serializeForSummary(messages) {
        const parts = [];
        for (const msg of messages) {
            const content = msg.content || '';
            const truncated = content.length > FALLBACK_TURN_MAX_CHARS
                ? content.slice(0, FALLBACK_TURN_MAX_CHARS) + '...'
                : content;
            if (msg.role === 'user') {
                parts.push(`[USER]: ${truncated}`);
            }
            else if (msg.role === 'assistant') {
                const tools = msg.tool_calls?.map(tc => tc.function.name).join(', ');
                parts.push(`[ASSISTANT${tools ? ` (tools: ${tools})` : ''}]: ${truncated}`);
            }
            else if (msg.role === 'tool') {
                parts.push(`[TOOL RESULT ${msg.name || ''}]: ${truncated}`);
            }
        }
        return parts.join('\n\n');
    }
    // -------------------------------------------------------------------------
    // Phase 3b: Fallback summary (no LLM)
    // -------------------------------------------------------------------------
    _buildFallbackSummary(middle) {
        const sections = {
            'Completed Actions': [],
            'Key Decisions': [],
            'Relevant Files': [],
            'Remaining Work': [],
        };
        const filePattern = /(?:\/[\w.-]+)+\.\w+/g;
        for (const msg of middle) {
            const content = (0, utils_1.getTextContent)(msg.content);
            const preview = content.length > 300 ? content.slice(0, 300) + '...' : content;
            if (msg.role === 'user') {
                sections['Remaining Work'].push(`- User asked: ${preview.slice(0, 150)}`);
            }
            else if (msg.role === 'assistant') {
                const tools = msg.tool_calls?.map(tc => tc.function.name).join(', ');
                if (tools) {
                    sections['Completed Actions'].push(`- Used tools: ${tools}`);
                }
                // Extract file paths
                const files = content.match(filePattern);
                if (files) {
                    for (const f of files) {
                        if (!sections['Relevant Files'].some(l => l.includes(f))) {
                            sections['Relevant Files'].push(`- ${f}`);
                        }
                    }
                }
            }
            else if (msg.role === 'tool') {
                const toolName = msg.name || 'tool';
                const isError = content.includes('"error"');
                if (isError) {
                    sections['Key Decisions'].push(`- ${toolName} encountered an error`);
                }
            }
        }
        const parts = [SUMMARY_PREFIX, ''];
        for (const [section, items] of Object.entries(sections)) {
            if (items.length > 0) {
                parts.push(`**${section}:**`);
                // Cap each section
                parts.push(...items.slice(0, 10));
                parts.push('');
            }
        }
        // Cap total size
        let result = parts.join('\n');
        if (result.length > FALLBACK_SUMMARY_MAX_CHARS) {
            result = result.slice(0, FALLBACK_SUMMARY_MAX_CHARS) + '\n... [truncated]';
        }
        return result;
    }
    // -------------------------------------------------------------------------
    // Phase 4: Assembly
    // -------------------------------------------------------------------------
    _assemble(head, summary, tail) {
        const result = [...head];
        // Insert summary as a system message
        if (summary) {
            result.push({
                role: 'system',
                content: `${SUMMARY_PREFIX}\n\n${summary}`,
            });
        }
        // Add tail messages
        result.push(...tail);
        // Fix orphaned tool pairs
        return this._sanitizeToolPairs(result);
    }
    // -------------------------------------------------------------------------
    // Fix orphaned tool_call/tool_result pairs
    // -------------------------------------------------------------------------
    _sanitizeToolPairs(messages) {
        const toolCallIds = new Set();
        const toolResultIds = new Set();
        // Collect IDs
        for (const msg of messages) {
            if (msg.role === 'assistant' && msg.tool_calls) {
                for (const tc of msg.tool_calls) {
                    toolCallIds.add(tc.id);
                }
            }
            if (msg.role === 'tool' && msg.tool_call_id) {
                toolResultIds.add(msg.tool_call_id);
            }
        }
        // Remove orphaned tool results
        const result = [];
        for (const msg of messages) {
            if (msg.role === 'tool' && msg.tool_call_id && !toolCallIds.has(msg.tool_call_id)) {
                continue; // Orphaned tool result
            }
            result.push(msg);
        }
        // Add stub results for orphaned tool calls
        const finalResult = [];
        for (const msg of result) {
            finalResult.push(msg);
            if (msg.role === 'assistant' && msg.tool_calls) {
                for (const tc of msg.tool_calls) {
                    if (!toolResultIds.has(tc.id)) {
                        finalResult.push({
                            role: 'tool',
                            tool_call_id: tc.id,
                            content: '[Result lost due to context compression]',
                        });
                    }
                }
            }
        }
        return finalResult;
    }
    // -------------------------------------------------------------------------
    // Reset
    // -------------------------------------------------------------------------
    reset() {
        this._compressionCount = 0;
        this._previousSummary = null;
        this._ineffectiveCount = 0;
        this._lastSavingsPct = 0;
    }
}
exports.ContextCompressor = ContextCompressor;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function simpleHash(text) {
    let hash = 0;
    for (let i = 0; i < Math.min(text.length, 500); i++) {
        const chr = text.charCodeAt(i);
        hash = ((hash << 5) - hash) + chr;
        hash |= 0;
    }
    return hash.toString(36);
}
function summarizeToolResult(toolName, content) {
    const lines = content.split('\n').length;
    const chars = content.length;
    // Extract first meaningful line
    const firstLine = content.split('\n').find(l => l.trim().length > 0) || '';
    switch (toolName) {
        case 'terminal':
            const exitMatch = content.match(/exit_code["\s:]+(\d+)/);
            const exitCode = exitMatch ? exitMatch[1] : '?';
            return `[terminal] exit ${exitCode}, ${lines} lines output`;
        case 'read_file':
            return `[read_file] ${lines} lines, ${chars} chars`;
        case 'write_file':
            return `[write_file] ${chars} bytes written`;
        case 'search_files':
            const matchCount = (content.match(/"line_number"/g) || []).length;
            return `[search_files] ${matchCount} matches`;
        case 'patch':
            return content.includes('"success": true') ? '[patch] applied' : '[patch] failed';
        case 'web_search':
            return `[web_search] results returned`;
        case 'web_extract':
            return `[web_extract] ${chars} chars extracted`;
        default:
            return `[${toolName}] ${chars} chars output`;
    }
}
//# sourceMappingURL=context-compressor.js.map