"use strict";
// =============================================================================
// Pure Agent - Conversation Loop (Complete)
// Ported from Hermes agent/conversation_loop.py
// Full error recovery: 20+ strategies, tool validation, truncation continuation
// =============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.runConversation = runConversation;
const types_1 = require("./types");
const system_prompt_1 = require("./system-prompt");
const tool_registry_1 = require("./tool-registry");
const logger_1 = require("./logger");
const utils_1 = require("./utils");
const context_compressor_1 = require("./context-compressor");
const conv_files_1 = require("./conv-files");
const error_classifier_1 = require("./error-classifier");
const constants_1 = require("./constants");
async function streamLLMCall(agent, messages, signal, maxTokens, onDelta) {
    const body = {
        model: agent.model,
        messages,
        max_tokens: maxTokens,
        temperature: agent.temperature,
        stream: true,
    };
    if (agent.extraBody)
        Object.assign(body, agent.extraBody);
    if (agent.tools.length > 0) {
        body.tools = agent.tools;
        body.tool_choice = 'auto';
    }
    const headers = { 'Content-Type': 'application/json' };
    if (agent.apiKey)
        headers['Authorization'] = `Bearer ${agent.apiKey}`;
    if (agent.customHeaders)
        Object.assign(headers, agent.customHeaders);
    const response = await fetch(agent.baseUrl, { method: 'POST', headers, body: JSON.stringify(body), signal });
    if (!response.ok) {
        const errorText = await response.text().catch(() => 'Unknown error');
        const err = new Error(`LLM API error (${response.status}): ${errorText}`);
        err.status = response.status;
        err.body = errorText;
        throw err;
    }
    if (!response.body)
        throw new Error('No response body from LLM API');
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let content = '';
    const toolCallAccumulator = new Map();
    let finishReason = null;
    let reasoningContent = '';
    let usage;
    let buffer = '';
    try {
        while (true) {
            const { done, value } = await Promise.race([
                reader.read(),
                new Promise((_, reject) => {
                    if (signal.aborted)
                        reject(new Error('Aborted'));
                    signal.addEventListener('abort', () => reject(new Error('Aborted')), { once: true });
                    setTimeout(() => reject(new Error('Stream read timeout')), constants_1.STREAM_READ_TIMEOUT_MS);
                }),
            ]);
            if (done)
                break;
            buffer += decoder.decode(value, { stream: true });
            const lines = buffer.split('\n');
            buffer = lines.pop() || '';
            for (const line of lines) {
                const trimmed = line.trim();
                if (!trimmed || trimmed === 'data: [DONE]')
                    continue;
                if (!trimmed.startsWith('data: '))
                    continue;
                let chunk;
                try {
                    chunk = JSON.parse(trimmed.slice(6));
                }
                catch {
                    continue;
                }
                const choice = chunk.choices?.[0];
                if (!choice)
                    continue;
                const delta = choice.delta;
                if (delta?.content) {
                    content += delta.content;
                    onDelta?.(delta.content);
                }
                if (delta?.reasoning_content)
                    reasoningContent += delta.reasoning_content;
                if (delta?.tool_calls) {
                    for (const tc of delta.tool_calls) {
                        const idx = tc.index;
                        if (!toolCallAccumulator.has(idx)) {
                            toolCallAccumulator.set(idx, { id: tc.id || `call_${(0, utils_1.generateId)()}`, type: 'function', function: { name: '', arguments: '' } });
                        }
                        const existing = toolCallAccumulator.get(idx);
                        if (tc.id)
                            existing.id = tc.id;
                        if (tc.function?.name)
                            existing.function.name += tc.function.name;
                        if (tc.function?.arguments)
                            existing.function.arguments += tc.function.arguments;
                    }
                }
                if (choice.finish_reason)
                    finishReason = choice.finish_reason;
                if (chunk.usage) {
                    usage = { promptTokens: chunk.usage.prompt_tokens, completionTokens: chunk.usage.completion_tokens, totalTokens: chunk.usage.total_tokens };
                }
            }
        }
    }
    finally {
        reader.releaseLock();
    }
    const toolCalls = Array.from(toolCallAccumulator.entries()).sort(([a], [b]) => a - b).map(([, tc]) => tc);
    return { content, toolCalls, finishReason, reasoningContent, usage };
}
// ---------------------------------------------------------------------------
// Tool dispatch
// ---------------------------------------------------------------------------
function shouldParallelizeToolBatch(toolCalls) {
    if (toolCalls.length <= 1)
        return false;
    return toolCalls.every(tc => types_1.READONLY_TOOLS.has(tc.function.name));
}
async function dispatchToolCall(agent, toolCall) {
    const startTime = Date.now();
    const { name, arguments: argsStr } = toolCall.function;
    let args;
    try {
        args = typeof argsStr === 'string' ? JSON.parse(argsStr) : argsStr;
    }
    catch {
        return { toolCallId: toolCall.id, name, result: JSON.stringify({ error: `Failed to parse tool arguments` }), isError: true, duration: Date.now() - startTime };
    }
    args = coerceToolArgs(name, args);
    const permission = types_1.TOOL_PERMISSIONS[name] || types_1.ToolPermission.WRITE;
    if (permission >= types_1.ToolPermission.DANGEROUS && agent.toolApprovalCallback) {
        try {
            const approved = await agent.toolApprovalCallback(name, args);
            if (!approved)
                return { toolCallId: toolCall.id, name, result: JSON.stringify({ error: 'Denied by user' }), isError: true, duration: Date.now() - startTime };
        }
        catch (err) {
            return { toolCallId: toolCall.id, name, result: JSON.stringify({ error: `Approval error: ${(0, utils_1.sanitizeError)(err)}` }), isError: true, duration: Date.now() - startTime };
        }
    }
    const context = { signal: agent._abortController.signal, sessionId: agent.sessionId, workingDir: agent.workingDir, taskId: 'default' };
    const result = await tool_registry_1.registry.dispatch(name, args, context);
    return { toolCallId: toolCall.id, name, result, isError: result.includes('"error"'), duration: Date.now() - startTime };
}
function coerceToolArgs(toolName, args) {
    const entry = tool_registry_1.registry.getEntry(toolName);
    if (!entry)
        return args;
    const properties = entry.schema?.parameters?.properties;
    if (!properties)
        return args;
    const coerced = { ...args };
    for (const [key, propSchema] of Object.entries(properties)) {
        const value = coerced[key];
        if (value === undefined || value === null)
            continue;
        const prop = propSchema;
        if (prop.type === 'number' && typeof value === 'string') {
            const n = Number(value);
            if (!isNaN(n))
                coerced[key] = n;
        }
        if (prop.type === 'boolean' && typeof value === 'string') {
            if (value === 'true' || value === '1')
                coerced[key] = true;
            else if (value === 'false' || value === '0')
                coerced[key] = false;
        }
        if (prop.type === 'integer' && typeof value === 'string') {
            const n = parseInt(value, 10);
            if (!isNaN(n))
                coerced[key] = n;
        }
        if (prop.type === 'array' && !Array.isArray(value))
            coerced[key] = [value];
    }
    return coerced;
}
// ---------------------------------------------------------------------------
// Tool call validation and repair
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Unicode sanitization (strip surrogates, non-characters)
// ---------------------------------------------------------------------------
function sanitizeString(text) {
    // Remove lone surrogates (U+D800-U+DFFF) that aren't part of a pair
    return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, '');
}
function sanitizeMessages(messages) {
    let sanitized = 0;
    for (const msg of messages) {
        if (msg.content && typeof msg.content === 'string') {
            const clean = sanitizeString(msg.content);
            if (clean !== msg.content) {
                msg.content = clean;
                sanitized++;
            }
        }
    }
    return sanitized;
}
// ---------------------------------------------------------------------------
// Deep JSON argument repair
// ---------------------------------------------------------------------------
function deepRepairJson(raw) {
    if (!raw || raw.trim().length === 0)
        return '{}';
    let s = raw.trim();
    // 1. Remove markdown code fences
    s = s.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '');
    // 2. Try parsing as-is
    try {
        JSON.parse(s);
        return s;
    }
    catch { }
    // 3. Try fixing common issues
    // 3a. Single quotes → double quotes
    if (s.includes("'") && !s.includes('"')) {
        const fixed = s.replace(/'/g, '"');
        try {
            JSON.parse(fixed);
            return fixed;
        }
        catch { }
    }
    // 3b. Unquoted keys: {key: "value"} → {"key": "value"}
    try {
        const fixed = s.replace(/(\{|,)\s*([a-zA-Z_]\w*)\s*:/g, '$1"$2":');
        JSON.parse(fixed);
        return fixed;
    }
    catch { }
    // 3c. Trailing comma: {"a":1,} → {"a":1}
    try {
        const fixed = s.replace(/,\s*([\]}])/g, '$1');
        JSON.parse(fixed);
        return fixed;
    }
    catch { }
    // 3d. Missing closing brace/bracket
    const openBraces = (s.match(/\{/g) || []).length;
    const closeBraces = (s.match(/\}/g) || []).length;
    const openBrackets = (s.match(/\[/g) || []).length;
    const closeBrackets = (s.match(/\]/g) || []).length;
    if (openBraces > closeBraces) {
        const fixed = s + '}'.repeat(openBraces - closeBraces);
        try {
            JSON.parse(fixed);
            return fixed;
        }
        catch { }
    }
    if (openBrackets > closeBrackets) {
        const fixed = s + ']'.repeat(openBrackets - closeBrackets);
        try {
            JSON.parse(fixed);
            return fixed;
        }
        catch { }
    }
    // 3e. Extract JSON object/array from surrounding text
    const jsonMatch = s.match(/(\{[\s\S]*\}|\[[\s\S]*\])/);
    if (jsonMatch) {
        try {
            JSON.parse(jsonMatch[1]);
            return jsonMatch[1];
        }
        catch { }
    }
    // 3f. Last resort: wrap bare string value
    if (!s.startsWith('{') && !s.startsWith('[')) {
        const wrapped = JSON.stringify({ value: s });
        return wrapped;
    }
    return null;
}
function validateAndRepairToolCalls(toolCalls, validNames) {
    const valid = [];
    const invalid = [];
    let repaired = 0;
    let truncated = false;
    for (const tc of toolCalls) {
        // === Tool name repair ===
        if (!validNames.has(tc.function.name)) {
            const candidates = [
                tc.function.name.replace(/-/g, '_'),
                tc.function.name.replace(/_/g, '-'),
                tc.function.name.toLowerCase(),
                tc.function.name.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, ''),
            ];
            const found = candidates.find(c => validNames.has(c));
            if (found) {
                tc.function.name = found;
                repaired++;
            }
        }
        // === Arguments validation and deep repair ===
        if (!tc.function.arguments || tc.function.arguments.trim() === '') {
            tc.function.arguments = '{}';
        }
        else if (typeof tc.function.arguments !== 'string') {
            // Already parsed (dict/array) - serialize back
            tc.function.arguments = JSON.stringify(tc.function.arguments);
        }
        else {
            try {
                JSON.parse(tc.function.arguments);
            }
            catch {
                // Deep repair attempt
                const repaired_args = deepRepairJson(tc.function.arguments);
                if (repaired_args !== null) {
                    tc.function.arguments = repaired_args;
                    repaired++;
                }
                else {
                    // Check if truncated
                    const trimmed = tc.function.arguments.trimEnd();
                    if (!trimmed.endsWith('}') && !trimmed.endsWith(']')) {
                        truncated = true;
                    }
                    // Last resort: empty object
                    tc.function.arguments = '{}';
                    repaired++;
                }
            }
        }
        if (!validNames.has(tc.function.name)) {
            invalid.push(tc.function.name);
        }
        else {
            valid.push(tc);
        }
    }
    return { valid, invalid, repaired, truncated };
}
// ---------------------------------------------------------------------------
// Context compression
// ---------------------------------------------------------------------------
function compressContext(agent, messages, compressor) {
    if (!compressor) {
        // Fallback: simple head+tail preservation
        const head = messages.slice(0, 3);
        const tail = messages.slice(-6);
        const summary = `[Context compressed: ${messages.length - 9} messages removed]`;
        return [...head, { role: 'system', content: summary }, ...tail];
    }
    // Use the compressor synchronously (it does LLM-based summary if llmCall provided)
    // For now, use the pruning-only path (no async LLM call in this context)
    return compressor.compress(messages).then(r => r.messages);
}
// ---------------------------------------------------------------------------
// Continuation prompt
// ---------------------------------------------------------------------------
function getContinuationPrompt(isPartialStub) {
    if (isPartialStub) {
        return '[System: The previous response was cut off by a network error mid-stream. Continue exactly where you left off. Do not repeat anything.]';
    }
    return '[System: Your previous response was truncated by the output length limit. Continue exactly where you left off. Do not repeat anything.]';
}
// ---------------------------------------------------------------------------
// Load media from disk for messages that have file references
// ---------------------------------------------------------------------------
function loadMediaForLLM(messages, sessionId) {
    return messages.map(msg => {
        if (!Array.isArray(msg.content))
            return msg;
        const hasFileRefs = msg.content.some((b) => b._file);
        if (hasFileRefs) {
            return { ...msg, content: (0, conv_files_1.loadMediaAsBase64)(sessionId, msg.content) };
        }
        return msg;
    });
}
// ---------------------------------------------------------------------------
// Main conversation loop
// ---------------------------------------------------------------------------
async function* runConversation(agent, userMessage, options = {}) {
    const logger = (0, logger_1.getLogger)();
    const startTime = Date.now();
    if (options.systemMessage) {
        agent.systemMessage = options.systemMessage;
        (0, system_prompt_1.invalidateSystemPrompt)(agent);
    }
    const conversationHistory = options.conversationHistory || [];
    // Build user message (may be string or array for multimodal)
    const userMsg = {
        role: 'user',
        content: Array.isArray(userMessage) ? userMessage : String(userMessage),
    };
    const allMessages = [...conversationHistory, userMsg];
    const systemPrompt = (0, system_prompt_1.buildSystemPrompt)(agent);
    // Context info
    const estimatedTokens = (0, utils_1.estimateMessagesTokens)(allMessages);
    yield { event: 'context', data: { tokens: estimatedTokens, maxTokens: agent.contextWindow, percent: Math.round((estimatedTokens / agent.contextWindow) * 100), messages: allMessages.length, mode: 'full' } };
    // Context compressor
    const compressor = new context_compressor_1.ContextCompressor(agent.contextWindow);
    // Per-turn retry counters
    let apiCallCount = 0;
    let totalToolCalls = 0;
    let compressionAttempts = 0;
    let lengthContinueRetries = 0;
    let truncatedToolCallRetries = 0;
    let invalidToolRetries = 0;
    let invalidJsonRetries = 0;
    let emptyContentRetries = 0;
    let thinkingPrefillRetries = 0;
    let postToolEmptyRetried = false;
    let visionSupported = true;
    // One-shot error recovery flags
    let thinkingSigRetryAttempted = false;
    let imageShrinkRetryAttempted = false;
    let multimodalToolContentRetryAttempted = false;
    // Load media from disk for history messages that have file references
    const allInput = [...conversationHistory, userMsg];
    const sanitized = loadMediaForLLM(allInput, agent.sessionId);
    const turnMessages = [
        { role: 'system', content: systemPrompt },
        ...sanitized,
    ];
    while (apiCallCount < agent.maxIterations) {
        if (agent._abortController.signal.aborted) {
            yield { event: 'error', data: { message: 'Interrupted by user' } };
            return;
        }
        if (!agent.iterationBudget.consume()) {
            yield { event: 'error', data: { message: `Iteration budget exhausted (${agent.iterationBudget.maxTotal})` } };
            return;
        }
        apiCallCount++;
        const currentMaxTokens = agent.maxTokens;
        yield { event: 'progress', data: { stage: `Calling LLM (round ${apiCallCount})`, elapsed: Math.round((Date.now() - startTime) / 1000), round: apiCallCount } };
        // Unicode sanitization: strip surrogates before API call
        const sanitizedCount = sanitizeMessages(turnMessages);
        if (sanitizedCount > 0) {
            logger.info(`Sanitized ${sanitizedCount} messages (Unicode surrogates removed)`);
        }
        logger.info(`API call #${apiCallCount}, messages: ${turnMessages.length}, model: ${agent.model}`);
        try {
            logger.info(`Calling LLM: ${agent.baseUrl}`);
            const result = await streamLLMCall(agent, turnMessages, agent._abortController.signal, currentMaxTokens, (delta) => { agent.streamDeltaCallback?.(delta); });
            logger.info(`LLM response: content=${result.content?.length || 0} chars, toolCalls=${result.toolCalls.length}, finishReason=${result.finishReason}`);
            // Content already sent via streamDeltaCallback
            const finalResponse = result.content || '';
            // === Truncation handling (finish_reason = 'length') ===
            if (result.finishReason === 'length') {
                // Check for thinking-budget exhaustion
                const hasThinkTags = /<(?:think|thinking|reasoning)[^>]*>/i.test(finalResponse);
                const hasContentAfterThink = finalResponse.replace(/<think[\s\S]*?<\/think>/gi, '').trim().length > 0;
                if (hasThinkTags && !hasContentAfterThink && !result.toolCalls.length) {
                    yield { event: 'error', data: { message: 'Thinking budget exhausted — all tokens spent on reasoning' } };
                    return;
                }
                // Text-only truncation (no tool calls)
                if (result.toolCalls.length === 0) {
                    lengthContinueRetries++;
                    if (lengthContinueRetries <= constants_1.MAX_TRUNCATION_CONTINUATIONS) {
                        turnMessages.push({ role: 'assistant', content: finalResponse });
                        turnMessages.push({ role: 'user', content: getContinuationPrompt(false) });
                        agent.iterationBudget.refund();
                        // Boost max tokens for continuation
                        agent.maxTokens = Math.min(currentMaxTokens * (lengthContinueRetries + 1), 32768);
                        continue;
                    }
                    yield { event: 'done', data: {} };
                    return;
                }
                // Truncated tool calls - retry once
                if (result.toolCalls.length > 0) {
                    truncatedToolCallRetries++;
                    if (truncatedToolCallRetries <= 1) {
                        agent.iterationBudget.refund();
                        continue; // Retry without appending broken response
                    }
                    yield { event: 'error', data: { message: 'Response truncated due to output length limit' } };
                    return;
                }
            }
            // === Empty response handling ===
            if (!finalResponse && result.toolCalls.length === 0) {
                // Post-tool empty nudge
                if (turnMessages.length > 0 && turnMessages[turnMessages.length - 1].role === 'tool' && !postToolEmptyRetried) {
                    postToolEmptyRetried = true;
                    turnMessages.push({ role: 'assistant', content: null });
                    turnMessages.push({ role: 'user', content: 'You just executed tool calls but returned an empty response. Please process the tool results above and continue with the task.' });
                    agent.iterationBudget.refund();
                    continue;
                }
                // Thinking-only response
                if (result.reasoningContent && thinkingPrefillRetries < 2) {
                    thinkingPrefillRetries++;
                    turnMessages.push({ role: 'assistant', content: null, reasoning_content: result.reasoningContent });
                    agent.iterationBudget.refund();
                    continue;
                }
                // Generic empty retry
                if (emptyContentRetries < 3) {
                    emptyContentRetries++;
                    agent.iterationBudget.refund();
                    continue;
                }
                yield { event: 'done', data: { reasoning_content: result.reasoningContent || undefined, final_content: finalResponse || undefined } };
                return;
            }
            // Reset empty retries on successful content
            emptyContentRetries = 0;
            postToolEmptyRetried = false;
            // === Tool calls processing ===
            if (result.toolCalls.length > 0) {
                // Validate and repair tool calls
                const validation = validateAndRepairToolCalls(result.toolCalls, agent.validToolNames);
                // Check for truncated tool call arguments
                if (validation.truncated) {
                    truncatedToolCallRetries++;
                    if (truncatedToolCallRetries <= 1) {
                        logger.info('Tool call arguments truncated, retrying without appending broken response');
                        agent.iterationBudget.refund();
                        continue;
                    }
                    yield { event: 'error', data: { message: 'Response truncated due to output length limit (tool arguments)' } };
                    return;
                }
                if (validation.invalid.length > 0) {
                    invalidToolRetries++;
                    if (invalidToolRetries >= 3) {
                        yield { event: 'error', data: { message: `Invalid tool calls: ${validation.invalid.join(', ')}` } };
                        return;
                    }
                    // Inject error results for invalid tools and retry
                    turnMessages.push({ role: 'assistant', content: finalResponse || null, tool_calls: result.toolCalls });
                    for (const name of validation.invalid) {
                        const tc = result.toolCalls.find(t => t.function.name === name);
                        turnMessages.push({
                            role: 'tool',
                            tool_call_id: tc?.id || 'unknown',
                            content: JSON.stringify({ error: `Tool '${name}' does not exist. Available tools: ${Array.from(agent.validToolNames).join(', ')}` }),
                        });
                    }
                    agent.iterationBudget.refund();
                    continue;
                }
                invalidToolRetries = 0;
                // Deduplicate tool calls
                const seen = new Set();
                const dedupedToolCalls = validation.valid.filter(tc => {
                    const key = `${tc.function.name}:${tc.function.arguments}`;
                    if (seen.has(key))
                        return false;
                    seen.add(key);
                    return true;
                });
                totalToolCalls += dedupedToolCalls.length;
                // Add assistant message with tool calls
                turnMessages.push({ role: 'assistant', content: finalResponse || null, tool_calls: dedupedToolCalls });
                // Yield tool_calls event
                yield {
                    event: 'tool_calls',
                    data: { calls: dedupedToolCalls.map(tc => ({ id: tc.id, name: tc.function.name, arguments: safeJsonParse(tc.function.arguments) })) },
                };
                // Dispatch tools
                const parallel = shouldParallelizeToolBatch(dedupedToolCalls);
                const dispatchResults = [];
                if (parallel) {
                    const promises = dedupedToolCalls.map(tc => dispatchToolCall(agent, tc));
                    const results = await Promise.allSettled(promises);
                    for (const r of results) {
                        dispatchResults.push(r.status === 'fulfilled' ? r.value : {
                            toolCallId: 'unknown', name: 'unknown', result: JSON.stringify({ error: `Dispatch failed: ${r.reason}` }), isError: true, duration: 0,
                        });
                    }
                }
                else {
                    for (const tc of dedupedToolCalls) {
                        if (agent._abortController.signal.aborted)
                            break;
                        dispatchResults.push(await dispatchToolCall(agent, tc));
                    }
                }
                // Yield tool results and add to messages
                for (const dr of dispatchResults) {
                    yield { event: 'tool_result', data: { tool_call_id: dr.toolCallId, name: dr.name, result: dr.result, is_error: dr.isError } };
                    // Handle _multimodal envelope: store content array directly
                    let toolContent = dr.result;
                    try {
                        const parsed = JSON.parse(dr.result);
                        if (parsed._multimodal && Array.isArray(parsed.content)) {
                            // Store as array for the LLM to see images/audio/video directly
                            toolContent = parsed.content;
                            // Also send MEDIA tags to frontend for rendering
                            if (parsed.text_summary) {
                                yield { event: 'content', data: { delta: `\n[MEDIA:info:${parsed.text_summary}]\n` } };
                            }
                        }
                    }
                    catch { /* not JSON, use as-is */ }
                    turnMessages.push({ role: 'tool', tool_call_id: dr.toolCallId, content: toolContent });
                }
                // Check context overflow after tools
                const currentTokens = (0, utils_1.estimateMessagesTokens)(turnMessages);
                yield { event: 'context', data: { tokens: currentTokens, maxTokens: agent.contextWindow, percent: Math.round((currentTokens / agent.contextWindow) * 100), messages: turnMessages.length, mode: 'full' } };
                if (currentTokens > agent.contextWindow * constants_1.COMPRESSION_THRESHOLD_RATIO && compressionAttempts < constants_1.MAX_COMPRESSION_PASSES) {
                    compressionAttempts++;
                    yield { event: 'compressing', data: { reason: 'context_threshold' } };
                    const originalLen = turnMessages.length;
                    const compressed = await compressor.compress(turnMessages);
                    turnMessages.length = 0;
                    turnMessages.push(...compressed.messages);
                    (0, system_prompt_1.invalidateSystemPrompt)(agent);
                    yield { event: 'compressed', data: { summary: compressed.summary || null, removedCount: compressed.removedCount, tokens: (0, utils_1.estimateMessagesTokens)(turnMessages), maxTokens: agent.contextWindow, messages: turnMessages.length } };
                }
                continue;
            }
            // === No tool calls - final response ===
            yield { event: 'done', data: { reasoning_content: result.reasoningContent || undefined, final_content: finalResponse || undefined } };
            logger.info(`Conversation complete: ${apiCallCount} calls, ${totalToolCalls} tools, ${Math.round((Date.now() - startTime) / 1000)}s`);
            return;
        }
        catch (err) {
            if (agent._abortController.signal.aborted) {
                yield { event: 'error', data: { message: 'Interrupted by user' } };
                return;
            }
            const classified = err?.classified || (0, error_classifier_1.classifyApiError)(err, err?.status);
            logger.error(`API error (round ${apiCallCount}): [${classified.reason}] ${classified.message}`);
            // === Thinking signature: strip reasoning_details and retry ===
            if (classified.reason === error_classifier_1.FailoverReason.thinking_signature && !thinkingSigRetryAttempted) {
                thinkingSigRetryAttempted = true;
                for (const m of turnMessages) {
                    delete m.reasoning_content;
                    delete m.reasoning_details;
                }
                agent.iterationBudget.refund();
                continue;
            }
            // === Image rejection: strip images and retry ===
            if (visionSupported && (0, error_classifier_1.isImageRejection)(err, err?.status) && (0, error_classifier_1.hasImagesInMessages)(turnMessages)) {
                visionSupported = false;
                (0, error_classifier_1.stripImagesFromMessages)(turnMessages);
                logger.info('Image rejection detected, stripped images, retrying');
                agent.iterationBudget.refund();
                continue;
            }
            // === Multimodal tool content unsupported: strip images from tool messages ===
            if (classified.reason === error_classifier_1.FailoverReason.multimodal_tool_content_unsupported && !multimodalToolContentRetryAttempted) {
                multimodalToolContentRetryAttempted = true;
                (0, error_classifier_1.stripImagesFromMessages)(turnMessages);
                agent.iterationBudget.refund();
                continue;
            }
            // === Image too large: try to shrink ===
            if (classified.reason === error_classifier_1.FailoverReason.image_too_large && !imageShrinkRetryAttempted) {
                imageShrinkRetryAttempted = true;
                (0, error_classifier_1.stripImagesFromMessages)(turnMessages);
                agent.iterationBudget.refund();
                continue;
            }
            // === Context overflow: compress and retry ===
            if (classified.reason === error_classifier_1.FailoverReason.context_overflow) {
                compressionAttempts++;
                if (compressionAttempts > constants_1.MAX_COMPRESSION_PASSES) {
                    yield { event: 'error', data: { message: 'Context overflow: compression exhausted' } };
                    return;
                }
                yield { event: 'compressing', data: { reason: 'context_overflow' } };
                const originalLen = turnMessages.length;
                const compressed = await compressor.compress(turnMessages);
                turnMessages.length = 0;
                turnMessages.push(...compressed.messages);
                (0, system_prompt_1.invalidateSystemPrompt)(agent);
                yield { event: 'compressed', data: { summary: compressed.summary || null, removedCount: compressed.removedCount, tokens: (0, utils_1.estimateMessagesTokens)(turnMessages), maxTokens: agent.contextWindow, messages: turnMessages.length } };
                if (turnMessages.length < originalLen) {
                    await (0, utils_1.sleep)(2000, agent._abortController.signal).catch(() => { });
                    agent.iterationBudget.refund();
                    continue;
                }
                yield { event: 'error', data: { message: 'Context overflow: compression ineffective' } };
                return;
            }
            // === Payload too large (413): compress ===
            if (classified.reason === error_classifier_1.FailoverReason.payload_too_large) {
                compressionAttempts++;
                if (compressionAttempts > constants_1.MAX_COMPRESSION_PASSES) {
                    yield { event: 'error', data: { message: 'Payload too large: compression exhausted' } };
                    return;
                }
                yield { event: 'compressing', data: { reason: 'payload_too_large' } };
                const compressed = await compressor.compress(turnMessages);
                turnMessages.length = 0;
                turnMessages.push(...compressed.messages);
                (0, system_prompt_1.invalidateSystemPrompt)(agent);
                if (turnMessages.length < (turnMessages.length + compressed.removedCount)) {
                    await (0, utils_1.sleep)(2000, agent._abortController.signal).catch(() => { });
                    agent.iterationBudget.refund();
                    continue;
                }
                yield { event: 'error', data: { message: 'Payload too large' } };
                return;
            }
            // === Auth error (401/403): refresh token or fallback ===
            if (classified.reason === error_classifier_1.FailoverReason.auth) {
                // Try token refresh first
                const refreshed = await agent.tryRefreshAuth();
                if (refreshed) {
                    logger.info('Auth refreshed, retrying');
                    agent.iterationBudget.refund();
                    continue;
                }
                // Try fallback provider
                if (agent.hasFallback) {
                    if (agent.tryActivateFallback()) {
                        logger.info('Auth failed, switched to fallback provider');
                        agent.iterationBudget.refund();
                        continue;
                    }
                }
                yield { event: 'error', data: { message: `Authentication failed: ${classified.message}` } };
                return;
            }
            // === Rate limit / overloaded: backoff, then fallback ===
            if (classified.reason === error_classifier_1.FailoverReason.rate_limit || classified.reason === error_classifier_1.FailoverReason.overloaded) {
                // First try: backoff and retry
                if (apiCallCount <= 3) {
                    const delay = (0, utils_1.jitteredBackoff)(apiCallCount, constants_1.RATE_LIMIT_BASE_DELAY_MS, constants_1.RATE_LIMIT_MAX_DELAY_MS);
                    logger.info(`${classified.reason}, waiting ${delay}ms`);
                    yield { event: 'progress', data: { stage: `Rate limited, retrying in ${Math.round(delay / 1000)}s`, elapsed: Math.round((Date.now() - startTime) / 1000) } };
                    await (0, utils_1.sleep)(delay, agent._abortController.signal).catch(() => { });
                    agent.iterationBudget.refund();
                    continue;
                }
                // After 3 retries: try fallback provider
                if (agent.hasFallback) {
                    if (agent.tryActivateFallback()) {
                        logger.info(`${classified.reason} persistent, switched to fallback provider`);
                        apiCallCount = 0; // Reset counter for new provider
                        agent.iterationBudget.refund();
                        continue;
                    }
                }
                // No fallback: final backoff
                const delay = (0, utils_1.jitteredBackoff)(apiCallCount, constants_1.RATE_LIMIT_BASE_DELAY_MS, constants_1.RATE_LIMIT_MAX_DELAY_MS);
                await (0, utils_1.sleep)(delay, agent._abortController.signal).catch(() => { });
                agent.iterationBudget.refund();
                continue;
            }
            // === Billing (402): try fallback immediately ===
            if (classified.reason === error_classifier_1.FailoverReason.billing) {
                if (agent.hasFallback) {
                    if (agent.tryActivateFallback()) {
                        logger.info('Billing exhausted, switched to fallback provider');
                        agent.iterationBudget.refund();
                        continue;
                    }
                }
                yield { event: 'error', data: { message: `Billing/credits exhausted: ${classified.message}` } };
                return;
            }
            // === Server error (500/502): backoff, then fallback ===
            if (classified.reason === error_classifier_1.FailoverReason.server_error) {
                if (apiCallCount > 5 && agent.hasFallback) {
                    if (agent.tryActivateFallback()) {
                        logger.info('Server errors persistent, switched to fallback');
                        apiCallCount = 0;
                        agent.iterationBudget.refund();
                        continue;
                    }
                }
                const delay = (0, utils_1.jitteredBackoff)(apiCallCount, 3000, 30000);
                await (0, utils_1.sleep)(delay, agent._abortController.signal).catch(() => { });
                agent.iterationBudget.refund();
                continue;
            }
            // === Timeout: retry ===
            if (classified.reason === error_classifier_1.FailoverReason.timeout) {
                agent.iterationBudget.refund();
                continue;
            }
            // === Non-retryable errors ===
            yield { event: 'error', data: { message: `API Error: ${classified.message}` } };
            return;
        }
    }
    yield { event: 'error', data: { message: `Maximum iterations reached (${agent.maxIterations})` } };
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function safeJsonParse(str) {
    try {
        return JSON.parse(str);
    }
    catch {
        return str;
    }
}
//# sourceMappingURL=conversation-loop.js.map