/**
 * OpenClaw-inspired Smart Context Compaction System
 *
 * Intelligently reduces conversation history while preserving essential context:
 * - Provider-specific message validation (Gemini/Anthropic/OpenAI have different rules)
 * - Channel-based history limits (DM vs group contexts)
 * - Token verification after compaction
 * - Multi-stage compaction strategies
 */
import { EventEmitter } from 'events';
// ============================================================================
// Provider-Specific Rules
// ============================================================================
const PROVIDER_RULES = {
    openai: {
        maxConsecutiveSameRole: 1,
        toolResultMustFollowCall: true,
        allowEmptyContent: true,
        maxContentLength: 128000,
        patterns: {},
    },
    anthropic: {
        maxConsecutiveSameRole: 1,
        toolResultMustFollowCall: true,
        allowEmptyContent: false,
        maxContentLength: 200000,
        patterns: {
            mustStartWith: 'user',
        },
    },
    gemini: {
        maxConsecutiveSameRole: 1,
        toolResultMustFollowCall: true,
        allowEmptyContent: false,
        maxContentLength: 1000000,
        patterns: {
            mustStartWith: 'user',
        },
    },
    grok: {
        maxConsecutiveSameRole: 1,
        toolResultMustFollowCall: true,
        allowEmptyContent: true,
        maxContentLength: 131072,
        patterns: {},
    },
    ollama: {
        maxConsecutiveSameRole: 10, // More lenient
        toolResultMustFollowCall: false,
        allowEmptyContent: true,
        maxContentLength: 32000,
        patterns: {},
    },
    lmstudio: {
        maxConsecutiveSameRole: 10,
        toolResultMustFollowCall: false,
        allowEmptyContent: true,
        maxContentLength: 32000,
        patterns: {},
    },
};
// ============================================================================
// Channel-Based History Limits
// ============================================================================
const CHANNEL_HISTORY_LIMITS = {
    dm: { maxTurns: 50, maxTokens: 32000 },
    group: { maxTurns: 20, maxTokens: 16000 },
    api: { maxTurns: 100, maxTokens: 64000 },
    cli: { maxTurns: 100, maxTokens: 64000 },
};
// ============================================================================
// Smart Compaction Engine
// ============================================================================
export class SmartCompactionEngine extends EventEmitter {
    config;
    rules;
    channelLimits;
    constructor(config) {
        super();
        this.config = {
            minMessages: 4,
            preserveSystem: true,
            preserveToolCalls: true,
            ...config,
        };
        this.rules = PROVIDER_RULES[config.provider];
        this.channelLimits = CHANNEL_HISTORY_LIMITS[config.channelType];
    }
    /**
     * Main compaction entry point
     */
    async compact(messages) {
        const startTime = Date.now();
        const originalTokens = this.estimateTokens(messages);
        this.emit('compaction:start', { messageCount: messages.length, tokens: originalTokens });
        // 1. Validate and sanitize messages
        let processed = this.sanitizeMessages(messages);
        // 2. Check if compaction is needed
        const targetTokens = this.config.targetTokens || this.config.maxTokens * 0.8;
        if (originalTokens <= targetTokens) {
            return {
                messages: processed,
                result: {
                    success: true,
                    originalTokens,
                    compactedTokens: originalTokens,
                    messagesRemoved: 0,
                    strategy: 'none',
                },
            };
        }
        // 3. Determine compaction strategy
        const strategy = this.determineStrategy(originalTokens, targetTokens);
        this.emit('compaction:strategy', { strategy });
        // 4. Apply compaction
        let result;
        switch (strategy) {
            case 'truncate':
                processed = this.truncateMessages(processed, targetTokens);
                break;
            case 'summarize':
                processed = await this.summarizeMessages(processed, targetTokens);
                break;
            case 'hybrid':
                processed = await this.hybridCompaction(processed, targetTokens);
                break;
            case 'aggressive':
                processed = await this.aggressiveCompaction(processed, targetTokens);
                break;
        }
        const compactedTokens = this.estimateTokens(processed);
        result = {
            success: compactedTokens <= this.config.maxTokens,
            originalTokens,
            compactedTokens,
            messagesRemoved: messages.length - processed.length,
            strategy,
        };
        if (!result.success) {
            result.error = `Compaction insufficient: ${compactedTokens} > ${this.config.maxTokens}`;
        }
        this.emit('compaction:complete', {
            ...result,
            durationMs: Date.now() - startTime,
        });
        return { messages: processed, result };
    }
    /**
     * Sanitize messages according to provider rules
     */
    sanitizeMessages(messages) {
        let sanitized = [...messages];
        // 1. Remove empty content if not allowed
        if (!this.rules.allowEmptyContent) {
            sanitized = sanitized.filter(m => m.content !== null && m.content !== '');
        }
        // 2. Merge consecutive same-role messages
        sanitized = this.mergeConsecutiveMessages(sanitized);
        // 3. Ensure tool results follow tool calls
        if (this.rules.toolResultMustFollowCall) {
            sanitized = this.validateToolCallOrder(sanitized);
        }
        // 4. Truncate overly long messages
        sanitized = sanitized.map(m => ({
            ...m,
            content: m.content && m.content.length > this.rules.maxContentLength
                ? m.content.slice(0, this.rules.maxContentLength) + '... [truncated]'
                : m.content,
        }));
        // 5. Ensure correct start/end patterns
        sanitized = this.ensureMessagePatterns(sanitized);
        return sanitized;
    }
    /**
     * Merge consecutive messages from the same role
     */
    mergeConsecutiveMessages(messages) {
        const result = [];
        for (const message of messages) {
            const last = result[result.length - 1];
            if (last && last.role === message.role && !message.tool_call_id && !last.tool_calls) {
                // Merge content
                last.content = `${last.content || ''}\n\n${message.content || ''}`.trim();
            }
            else {
                result.push({ ...message });
            }
        }
        return result;
    }
    /**
     * Validate tool call/result ordering
     */
    validateToolCallOrder(messages) {
        const result = [];
        const pendingToolCalls = new Map();
        for (const message of messages) {
            // Track tool calls
            if (message.tool_calls) {
                result.push(message);
                for (const tc of message.tool_calls) {
                    pendingToolCalls.set(tc.id, result.length - 1);
                }
                continue;
            }
            // Validate tool results
            if (message.role === 'tool' && message.tool_call_id) {
                const callIndex = pendingToolCalls.get(message.tool_call_id);
                if (callIndex !== undefined) {
                    result.push(message);
                    pendingToolCalls.delete(message.tool_call_id);
                }
                // Skip orphaned tool results
                continue;
            }
            result.push(message);
        }
        return result;
    }
    /**
     * Ensure message patterns match provider requirements
     */
    ensureMessagePatterns(messages) {
        let result = [...messages];
        const { patterns } = this.rules;
        // Ensure correct starting message
        if (patterns.mustStartWith === 'user') {
            // Find system message
            const systemIndex = result.findIndex(m => m.role === 'system');
            let systemMessage = null;
            if (systemIndex !== -1) {
                systemMessage = result[systemIndex];
                result.splice(systemIndex, 1);
            }
            // Ensure first non-system is user
            if (result.length > 0 && result[0].role !== 'user') {
                // Insert placeholder user message
                result.unshift({
                    role: 'user',
                    content: '[Conversation continued]',
                });
            }
            // Re-add system message at the start if present
            if (systemMessage && this.config.preserveSystem) {
                result.unshift(systemMessage);
            }
        }
        return result;
    }
    /**
     * Determine the best compaction strategy
     */
    determineStrategy(currentTokens, targetTokens) {
        const reduction = currentTokens - targetTokens;
        const reductionRatio = reduction / currentTokens;
        if (reductionRatio < 0.2) {
            return 'truncate';
        }
        else if (reductionRatio < 0.5) {
            return 'hybrid';
        }
        else if (reductionRatio < 0.7) {
            return 'summarize';
        }
        else {
            return 'aggressive';
        }
    }
    /**
     * Truncate oldest messages to fit token limit
     */
    truncateMessages(messages, targetTokens) {
        const result = [];
        let currentTokens = 0;
        // Always preserve system message
        const systemMessage = messages.find(m => m.role === 'system');
        if (systemMessage && this.config.preserveSystem) {
            result.push(systemMessage);
            currentTokens += this.estimateMessageTokens(systemMessage);
        }
        // Process messages from newest to oldest
        const nonSystem = messages.filter(m => m.role !== 'system').reverse();
        for (const message of nonSystem) {
            const messageTokens = this.estimateMessageTokens(message);
            if (currentTokens + messageTokens <= targetTokens) {
                result.unshift(message);
                currentTokens += messageTokens;
            }
            else if (result.length < (this.config.minMessages || 4)) {
                // Keep minimum messages even if over limit
                result.unshift(message);
                currentTokens += messageTokens;
            }
            else {
                break;
            }
        }
        // Ensure system message is at the start
        if (systemMessage && this.config.preserveSystem) {
            const sysIndex = result.findIndex(m => m.role === 'system');
            if (sysIndex > 0) {
                const [sys] = result.splice(sysIndex, 1);
                result.unshift(sys);
            }
        }
        return result;
    }
    /**
     * Summarize older messages
     */
    async summarizeMessages(messages, targetTokens) {
        // Keep recent messages
        const recentCount = Math.max(this.config.minMessages || 4, 10);
        const recent = messages.slice(-recentCount);
        const toSummarize = messages.slice(0, -recentCount);
        if (toSummarize.length === 0) {
            return messages;
        }
        // Create summary of older messages
        const summary = this.createSummary(toSummarize);
        // Build result
        const result = [];
        // Add system message
        const systemMessage = messages.find(m => m.role === 'system');
        if (systemMessage && this.config.preserveSystem) {
            result.push(systemMessage);
        }
        // Add summary as user message
        result.push({
            role: 'user',
            content: `[Previous conversation summary: ${summary}]`,
        });
        // Add assistant acknowledgment
        result.push({
            role: 'assistant',
            content: 'I understand the context from our previous conversation. How can I help you now?',
        });
        // Add recent messages (excluding system)
        result.push(...recent.filter(m => m.role !== 'system'));
        return result;
    }
    /**
     * Hybrid compaction: truncate + summarize
     */
    async hybridCompaction(messages, targetTokens) {
        // First, try truncation
        let result = this.truncateMessages(messages, targetTokens * 1.2);
        let tokens = this.estimateTokens(result);
        // If still over, summarize
        if (tokens > targetTokens) {
            result = await this.summarizeMessages(result, targetTokens);
        }
        return result;
    }
    /**
     * Aggressive compaction: maximum compression
     */
    async aggressiveCompaction(messages, targetTokens) {
        const result = [];
        // Keep only system message and recent 2 turns
        const systemMessage = messages.find(m => m.role === 'system');
        if (systemMessage && this.config.preserveSystem) {
            result.push(systemMessage);
        }
        // Create aggressive summary
        const summary = this.createSummary(messages.filter(m => m.role !== 'system'));
        result.push({
            role: 'user',
            content: `[Conversation context: ${summary}]\n\nPlease continue from where we left off.`,
        });
        result.push({
            role: 'assistant',
            content: 'I have the context. What would you like me to do next?',
        });
        return result;
    }
    /**
     * Create a summary of messages
     */
    createSummary(messages) {
        // Extract key points
        const points = [];
        for (const msg of messages) {
            if (!msg.content)
                continue;
            // Extract tool calls
            if (msg.tool_calls) {
                for (const tc of msg.tool_calls) {
                    points.push(`Used tool: ${tc.function.name}`);
                }
            }
            // Extract key content (first sentence or 100 chars)
            const content = msg.content.trim();
            if (content.length > 0) {
                const firstSentence = content.match(/^[^.!?]*[.!?]/)?.[0] || content.slice(0, 100);
                if (msg.role === 'user') {
                    points.push(`User asked about: ${firstSentence}`);
                }
                else if (msg.role === 'assistant') {
                    points.push(`Assistant: ${firstSentence}`);
                }
            }
        }
        // Limit summary length
        const uniquePoints = [...new Set(points)].slice(-10);
        return uniquePoints.join('. ');
    }
    /**
     * Estimate tokens for messages
     */
    estimateTokens(messages) {
        return messages.reduce((sum, m) => sum + this.estimateMessageTokens(m), 0);
    }
    /**
     * Estimate tokens for a single message
     */
    estimateMessageTokens(message) {
        let tokens = 4; // Base overhead per message
        if (message.content) {
            // Rough estimate: ~4 chars per token
            tokens += Math.ceil(message.content.length / 4);
        }
        if (message.tool_calls) {
            for (const tc of message.tool_calls) {
                tokens += 10; // Function call overhead
                tokens += Math.ceil(tc.function.arguments.length / 4);
            }
        }
        return tokens;
    }
    /**
     * Apply channel-specific limits
     */
    limitByChannel(messages) {
        const { maxTurns } = this.channelLimits;
        // Count turns (user-assistant pairs)
        let turnCount = 0;
        let lastRole = null;
        const messagesToKeep = [];
        const reversed = [...messages].reverse();
        for (const msg of reversed) {
            if (msg.role === 'system') {
                messagesToKeep.unshift(msg);
                continue;
            }
            if (msg.role !== lastRole && (msg.role === 'user' || msg.role === 'assistant')) {
                if (msg.role === 'user') {
                    turnCount++;
                }
            }
            if (turnCount <= maxTurns) {
                messagesToKeep.unshift(msg);
            }
            lastRole = msg.role;
        }
        return messagesToKeep;
    }
    /**
     * Update configuration
     */
    updateConfig(config) {
        this.config = { ...this.config, ...config };
        if (config.provider) {
            this.rules = PROVIDER_RULES[config.provider];
        }
        if (config.channelType) {
            this.channelLimits = CHANNEL_HISTORY_LIMITS[config.channelType];
        }
    }
    /**
     * Get current configuration
     */
    getConfig() {
        return { ...this.config };
    }
}
// ============================================================================
// Singleton & Convenience Functions
// ============================================================================
let compactionEngineInstance = null;
export function getSmartCompactionEngine(config) {
    if (!compactionEngineInstance && config) {
        compactionEngineInstance = new SmartCompactionEngine(config);
    }
    else if (config) {
        compactionEngineInstance.updateConfig(config);
    }
    return compactionEngineInstance;
}
export function resetSmartCompactionEngine() {
    compactionEngineInstance = null;
}
/**
 * Convenience function for quick compaction
 */
export async function compactMessages(messages, config) {
    const engine = new SmartCompactionEngine(config);
    return engine.compact(messages);
}
/**
 * Convenience function to check if compaction is needed
 */
export function needsCompaction(messages, maxTokens) {
    const engine = new SmartCompactionEngine({
        maxTokens,
        provider: 'grok',
        channelType: 'cli',
    });
    return engine.estimateTokens(messages) > maxTokens;
}
//# sourceMappingURL=smart-compaction.js.map