/**
 * ============================================================
 *  src/loop.ts —— Agent 主循环（query loop）
 * ------------------------------------------------------------
 *  这是整个 agent 的"心脏"。一句话描述：
 *
 *    用户说话 → 调 LLM → LLM 想调工具就执行工具 → 把结果喂回去 → 继续调 LLM →
 *    直到 LLM 不再调工具，输出最终回答。
 *
 *  和 kakadeai 主仓库 query.ts 用同样的 AsyncGenerator 模式：
 *    - 函数是 async function*，可以 yield 各种事件给 UI
 *    - UI 通过 for await 拿到事件，把它们映射到 React state
 *
 *  循环每轮（turn）做的事：
 *    a. 自动压缩检查（防止 context 撑爆）
 *    b. 调 LLM，把流式响应组装成完整的 assistant message
 *    c. 如果 assistant 不调工具 → 结束，return
 *    d. 否则并行执行所有工具调用（OpenAI 协议要求 tool 消息按 tool_calls 顺序回填）
 *    e. 进入下一轮（让模型看到 tool_result 后继续推理）
 *
 *  失控保护：maxTurns（默认 50）。
 *  中断支持：AbortSignal 透传到 chat() 和 tool.call()。
 *
 *  "工作过程显示"事件：
 *    - stage_change：thinking / streaming / tool_executing / compacting
 *    - reasoning：思维链流式增量（仅字符串型 field）
 * ============================================================
 */
import crypto from 'node:crypto';
import { autoCompactIfNeeded } from './context/compact.js';
import { microCompact, incrementTurn, expireOldEntries } from './context/microCompactLite.js';
import { isPromptTooLongError, reactiveCompactIfApplicable, } from './context/reactiveCompact.js';
import { snipCompactIfNeeded } from './context/snipCompact.js';
import { countMessagesTokens, estimateToolsTokens } from './context/tokenCounter.js';
import { chat as defaultChat } from './llm/client.js';
import { ALL_TOOLS, executeTool as defaultExecuteTool, getToolByName } from './tools/index.js';
/**
 * 工具 schema 的估算 token 数（ALL_TOOLS 固定，模块加载时算一次即可）。
 * 每轮 LLM 请求都会带上整套工具 schema，但 countMessagesTokens 只数历史消息、
 * 漏了这部分 → 系统性低估。autoCompact 判定阈值时加上它才准。
 */
const TOOLS_SCHEMA_TOKENS = estimateToolsTokens(ALL_TOOLS);
/**
 * 执行一次"用户输入 → 模型回答完成"的完整流程。
 *
 * yield 出来的 LoopEvent 会被 UI 用来更新界面。
 */
export async function* runQuery(userInput, options) {
    const { provider, history, signal, sessionState } = options;
    const maxTurns = options.maxTurns ?? 50;
    // DI fallback：测试可传 stub，生产走真实模块单例
    const chatFn = options.chat ?? defaultChat;
    const executeToolFn = options.executeTool ?? defaultExecuteTool;
    // 1. 用户消息入栈（v2：填 timestamp，让 MessageList 的 turn 边界横线
    //    能拿到首条 user 的真实时间，否则 fallback Date.now() 会丢失"提交时刻"语义）
    history.push({
        role: 'user',
        content: userInput,
        timestamp: Date.now(),
        id: crypto.randomUUID(),
    });
    // 立即通知 UI：用户消息已入栈，触发 bump 让 <Static> 马上把它 commit 进 scrollback，
    // 不必等整轮 assistant 落定（保证用户输入第一时间置顶，符合 T-A-O-R 展示顺序）。
    yield { type: 'user_message_committed' };
    // 反应式压缩自救：本 runQuery 内最多触发 MAX_REACTIVE_PER_QUERY 次（防本轮死循环）；
    // 跨 runQuery 的连续失败由 reactiveCompact.ts 的 circuit breaker（consecutiveFailures）兜底。
    let reactiveCount = 0;
    const MAX_REACTIVE_PER_QUERY = 3;
    // 上一轮 LLM 返回的真实 prompt token 数（done.promptTokens；provider 开 include_usage 才有）。
    // 作为 autoCompact 的权威判据，校正本地估算的系统性低估；压缩后清空（历史已变，旧值失真）。
    let lastPromptTokens;
    let turn = 0;
    while (turn < maxTurns) {
        turn++;
        incrementTurn(sessionState?.microCompact);
        expireOldEntries(sessionState?.microCompact);
        if (signal?.aborted) {
            // 中断标记帮助模型理解上下文
            history.push({
                role: 'user',
                content: '(用户按下了 ESC/Ctrl+C 中断了任务)',
                timestamp: Date.now(),
                id: crypto.randomUUID(),
            });
            // code='aborted'：让 `-p --output-format json` 把终结原因映射成 stop_reason='interrupted'
            yield { type: 'error', error: '已被用户中断', code: 'aborted' };
            return;
        }
        // 2. 自动压缩（判据 = max(上一轮真实 usage, 历史估算 + 工具 schema)，
        //    阈值 provider 无关、按 contextWindow × compactRatio 比例触发）
        try {
            const compact = await autoCompactIfNeeded(history, provider, {
                actualPromptTokens: lastPromptTokens,
                toolsTokens: TOOLS_SCHEMA_TOKENS,
            });
            if (compact.compacted) {
                yield { type: 'compact_start' };
                yield { type: 'stage_change', stage: 'compacting' };
                // in-place 替换 history（保持调用方持有的引用有效）
                history.length = 0;
                history.push(...compact.messages);
                // 历史已压缩变小，旧的真实 usage 不再代表当前请求 → 清空，下一轮先用估算判据
                lastPromptTokens = undefined;
                yield { type: 'compact_done', before: compact.before, after: compact.after };
                // 压缩完会回到 LLM 调用，stage 退回 thinking
                yield { type: 'stage_change', stage: 'thinking' };
            }
        }
        catch (e) {
            // 压缩失败不要让整个对话挂掉：先 snip 砍最老消息释放空间再继续，
            // 避免下一步 LLM 直接撞 prompt-too-long（code='compact_failed' 非致命，不 return）。
            try {
                const before = countMessagesTokens(history);
                const snipped = snipCompactIfNeeded(history, { force: true, snipPercent: 0.3 });
                if (snipped.messagesRemoved > 0) {
                    history.length = 0;
                    history.push(...snipped.messages);
                    lastPromptTokens = undefined;
                    yield { type: 'compact_done', before, after: countMessagesTokens(history) };
                }
            }
            catch {
                /* snip 也失败 → 真的只能硬上 */
            }
            yield {
                type: 'error',
                error: `自动压缩失败（已尝试 snip 兜底后继续）：${e.message}`,
                code: 'compact_failed',
            };
        }
        // 3. 调 LLM 并组装 assistant 消息
        let assistantText = '';
        /** 按 index 累积 tool_calls（OpenAI 协议下 tool_calls 是分片返回的） */
        const toolCallsByIndex = [];
        /**
         * 思维链累积器。三个 provider 字段名各管一份；最后哪个非空就挂哪个，
         * 多个非空就都挂上（不会冲突，下一轮请求时整个 message 透传给 API）。
         */
        let reasoningContent = '';
        let reasoningString = '';
        const reasoningDetails = [];
        /** 首个 text_delta 时切到 streaming，再来不重复 yield */
        let stageStreamingYielded = false;
        /**
         * R2：本轮 LLM 流的终止原因（来自 client 的 done.stopReason）。
         * 'length' = 被 provider 因 max_tokens 截断 —— 收尾时据此给 warning，
         * 避免「半截答案」被静默当成正常 end_turn。
         */
        let lastFinishReason = 'unknown';
        // 进入 LLM 流前：等首 token / reasoning，本质是 thinking
        yield { type: 'stage_change', stage: 'thinking' };
        try {
            for await (const ev of chatFn({
                provider,
                messages: history,
                tools: ALL_TOOLS,
                signal,
            })) {
                if (ev.type === 'text_delta') {
                    if (!stageStreamingYielded) {
                        stageStreamingYielded = true;
                        yield { type: 'stage_change', stage: 'streaming' };
                    }
                    assistantText += ev.delta;
                    yield { type: 'text', delta: ev.delta };
                }
                else if (ev.type === 'tool_call_delta') {
                    mergeToolCallDelta(toolCallsByIndex, ev);
                }
                else if (ev.type === 'reasoning_delta') {
                    if (ev.field === 'reasoning_content' && ev.delta) {
                        reasoningContent += ev.delta;
                        yield { type: 'reasoning', delta: ev.delta };
                    }
                    else if (ev.field === 'reasoning' && ev.delta) {
                        reasoningString += ev.delta;
                        yield { type: 'reasoning', delta: ev.delta };
                    }
                    else if (ev.field === 'reasoning_details' && ev.items) {
                        // reasoning_details 是结构化数组，没有 delta 字符串，跳过 yield
                        reasoningDetails.push(...ev.items);
                    }
                }
                else if (ev.type === 'done') {
                    // R2：记下流的终止原因。'length'（max_tokens 截断）会在本轮收尾时触发 warning。
                    lastFinishReason = ev.stopReason;
                    // 真实 prompt token（provider 开 include_usage 才有）→ 喂给下一轮 autoCompact 做权威判据
                    if (typeof ev.promptTokens === 'number')
                        lastPromptTokens = ev.promptTokens;
                }
            }
        }
        catch (e) {
            if (e.name === 'AbortError') {
                // 用户主动中断：记录标记消息，不报红色错误
                history.push({
                    role: 'user',
                    content: '(用户按下了 ESC/Ctrl+C 中断了任务)',
                    timestamp: Date.now(),
                    id: crypto.randomUUID(),
                });
                yield { type: 'interrupted' };
                return;
            }
            // prompt_too_long 反应式自救：压缩历史 → 同 turn 重发 LLM
            // LLM 看到压缩后的 9 段摘要 + 近期 verbatim 消息，能接着干活，
            // 不会丢失中途的工具调用上下文。
            if (isPromptTooLongError(e) && reactiveCount < MAX_REACTIVE_PER_QUERY) {
                reactiveCount++;
                yield { type: 'compact_start' };
                yield { type: 'stage_change', stage: 'compacting' };
                const result = await reactiveCompactIfApplicable(history, provider, e, sessionState?.reactive);
                // 仅当确实压下去了（after < before）才重试；否则（没压动 / 未恢复）退回错误路径，杜绝死循环。
                const shrank = (result.after ?? 0) < (result.before ?? 0);
                if (result.recovered && shrank) {
                    history.length = 0;
                    history.push(...result.messages);
                    lastPromptTokens = undefined; // 历史已变小，旧真实 usage 失真
                    yield {
                        type: 'compact_done',
                        before: result.before ?? 0,
                        after: result.after ?? 0,
                    };
                    // 压缩完回到 LLM 调用
                    yield { type: 'stage_change', stage: 'thinking' };
                    turn--; // 不消耗 turn 配额，本轮重新走
                    continue;
                }
                // reactive 没压动或未恢复 → 退回正常错误路径
            }
            // code='llm_error'：终结性错误，stop_reason='error'
            yield {
                type: 'error',
                error: `LLM 调用失败：${e.message}`,
                code: 'llm_error',
            };
            return;
        }
        // S2 防御：OpenAI 协议保证 tool_call 的 index 连续；万一某 provider 跳号产生稀疏数组，
        // 这里压实掉空洞，避免后续遍历 / 回填遇到 undefined（assistantMsg.tool_calls 与执行都用它）。
        {
            const dense = toolCallsByIndex.filter((tc) => tc != null);
            if (dense.length !== toolCallsByIndex.length) {
                toolCallsByIndex.length = 0;
                toolCallsByIndex.push(...dense);
            }
        }
        const assistantMsg = {
            role: 'assistant',
            content: assistantText.length > 0 ? assistantText : null,
            ...(toolCallsByIndex.length > 0 ? { tool_calls: toolCallsByIndex } : {}),
            ...(reasoningContent ? { reasoning_content: reasoningContent } : {}),
            ...(reasoningString ? { reasoning: reasoningString } : {}),
            ...(reasoningDetails.length > 0 ? { reasoning_details: reasoningDetails } : {}),
            timestamp: Date.now(),
            id: crypto.randomUUID(),
        };
        history.push(assistantMsg);
        yield { type: 'assistant_message', message: assistantMsg };
        // 4. 没有工具调用 → 整轮交互结束
        if (toolCallsByIndex.length === 0) {
            // R2：最终回答被 provider 因 max_tokens 截断（finish_reason='length'）时，
            // 不能静默当正常 end_turn 收尾 —— 否则 -p 宿主会拿到半截答案却以为成功。
            // 走通用 warning（text/json 都落 stderr，不污染 json 那一行 stdout 契约）。
            if (lastFinishReason === 'length') {
                yield {
                    type: 'warning',
                    message: '⚠️ 模型回答可能被截断（finish_reason=length，触顶 max_tokens），本轮回答或不完整。' +
                        '可调大模型的 max_tokens 上限，或让模型分段输出。',
                };
            }
            yield { type: 'turn_done' };
            return;
        }
        // 5. 执行工具调用
        //
        // R1：按 isConcurrencySafe 决定并发策略 —— 工具早就声明了这个字段，此前却没有
        // 任何执行路径消费它（全程无条件并行），这里让它真正生效：
        //   - 本轮工具「全部并发安全」（只读类 Read/Grep/Glob/WebSearch/WebFetch）且不止一个
        //     → 并行：用事件队列把多个 worker 的 tool_start/tool_end 按真实完成顺序交错 yield。
        //   - 只要有一个写类/未知工具（Edit/Write/MultiEdit/Bash/WebBrowser）→ 整轮串行：
        //     彻底杜绝两个写操作并行落到同一文件导致的 lost-update / fileState TOCTOU。
        // 两种路径下，history 里的 tool 消息都严格按 toolCallsByIndex 索引顺序回填
        // （OpenAI 协议要求 tool 消息与上一条 assistant 的 tool_calls 一一对应）。
        yield { type: 'stage_change', stage: 'tool_executing' };
        // 执行单个 tool call → WorkerOutcome（内部吞掉 abort/异常，永不 reject）。
        const runOne = async (tc) => {
            try {
                const result = await executeToolFn(tc.function.name, tc.function.arguments, signal);
                return { tc, result };
            }
            catch (e) {
                if (e.name === 'AbortError') {
                    return { tc, result: { ok: false, error: 'aborted' } };
                }
                return { tc, result: { ok: false, error: e.message } };
            }
        };
        const startEvent = (tc) => ({
            type: 'tool_start',
            toolCallId: tc.id,
            toolName: tc.function.name,
            argsPreview: previewArgs(tc.function.arguments),
            argsFriendly: friendlyToolDescription(tc.function.name, tc.function.arguments),
        });
        const endEvent = (o) => ({
            type: 'tool_end',
            toolCallId: o.tc.id,
            toolName: o.tc.function.name,
            ok: o.result.ok,
            content: o.result.ok
                ? o.result.content
                : o.result.error === 'aborted'
                    ? '(已中断)'
                    : `Error: ${o.result.error}`,
        });
        const allConcurrencySafe = toolCallsByIndex.every((tc) => {
            const tool = getToolByName(tc.function.name);
            return tool?.isConcurrencySafe === true;
        });
        let settled;
        if (allConcurrencySafe && toolCallsByIndex.length > 1) {
            // —— 并行：事件队列把各 worker 的 start/end 按真实完成顺序交错 yield 给 UI。
            const queue = [];
            let signalNew = null;
            const enqueue = (ev) => {
                queue.push(ev);
                signalNew?.();
                signalNew = null;
            };
            const workers = toolCallsByIndex.map(async (tc) => {
                enqueue(startEvent(tc));
                const o = await runOne(tc);
                enqueue(endEvent(o));
                return o;
            });
            const allDone = Promise.all(workers);
            let finished = false;
            void allDone.then(() => {
                finished = true;
                signalNew?.();
                signalNew = null;
            });
            while (!finished || queue.length > 0) {
                while (queue.length > 0)
                    yield queue.shift();
                if (finished)
                    break;
                await new Promise((r) => {
                    signalNew = r;
                });
            }
            settled = await allDone;
        }
        else {
            // —— 串行：逐个执行（含写类工具，或只有一个工具）。事件直接顺序 yield，无需队列。
            settled = [];
            for (const tc of toolCallsByIndex) {
                yield startEvent(tc);
                const o = await runOne(tc);
                yield endEvent(o);
                settled.push(o);
            }
        }
        // 严格按 toolCallsByIndex 顺序把 tool 消息回填 history（settled 已保持该顺序）。
        let anyAborted = false;
        for (const { tc, result } of settled) {
            if (!result.ok && result.error === 'aborted')
                anyAborted = true;
            const rawContent = result.ok ? result.content : `Error: ${result.error}`;
            // 微压缩仅对成功结果走（失败结果通常很短，不需要压缩）
            const content = result.ok
                ? microCompact(tc.function.name, rawContent, sessionState?.microCompact)
                : rawContent;
            history.push({
                role: 'tool',
                content,
                tool_call_id: tc.id,
                timestamp: Date.now(),
                id: crypto.randomUUID(),
            });
        }
        if (anyAborted || signal?.aborted) {
            // 工具被中断时，外层 yield interrupted 并 return
            yield { type: 'interrupted' };
            return;
        }
        // 6. 继续 while 让模型看到 tool_result 后继续推理
    }
    // code='max_turns'：撞最大轮数上限而终结，stop_reason='max_turns'
    yield {
        type: 'error',
        error: `达到最大轮数 ${maxTurns}，提前结束（防止失控）。如果合理可以提高 maxTurns。`,
        code: 'max_turns',
    };
}
// ---------------- 辅助函数 ----------------
/** 把一条 tool_call_delta 合并到累积数组上 */
function mergeToolCallDelta(acc, ev) {
    let slot = acc[ev.index];
    if (!slot) {
        slot = {
            id: '',
            type: 'function',
            function: { name: '', arguments: '' },
        };
        acc[ev.index] = slot;
    }
    if (ev.id)
        slot.id = ev.id;
    if (ev.name)
        slot.function.name += ev.name;
    if (ev.argumentsDelta)
        slot.function.arguments += ev.argumentsDelta;
}
/** 给 UI 展示用的参数预览（只取前 60 字符） */
function previewArgs(rawJson) {
    const oneLine = rawJson.replace(/\s+/g, ' ').trim();
    if (oneLine.length <= 60)
        return oneLine;
    return oneLine.slice(0, 60) + '...';
}
/**
 * 把工具名 + raw arguments JSON 转换成人话描述。
 * UI 优先用这个显示在 spinner 行（如 "Reading src/foo.ts"）。
 * 已知工具走 switch；未知工具或 JSON parse 失败 fallback 到 `${toolName}(...)`。
 */
function friendlyToolDescription(toolName, rawArgsJson) {
    let args = {};
    try {
        args = JSON.parse(rawArgsJson) ?? {};
    }
    catch {
        return `${toolName}(...)`;
    }
    const truncate = (s, n) => {
        if (typeof s !== 'string')
            return '';
        return s.length > n ? s.slice(0, n) + '…' : s;
    };
    switch (toolName) {
        case 'Read': return `Reading ${truncate(args.file_path, 60)}`;
        case 'Edit': return `Editing ${truncate(args.file_path, 60)}`;
        case 'MultiEdit': {
            const n = Array.isArray(args.edits) ? args.edits.length : 0;
            return `Editing ${truncate(args.file_path, 50)}${n ? ` (${n} 处)` : ''}`;
        }
        case 'Write': return `Writing ${truncate(args.file_path, 60)}`;
        case 'Bash': return `Running \`${truncate(args.command, 40)}\``;
        case 'Grep': {
            const p = truncate(args.pattern, 30);
            const g = typeof args.glob === 'string' && args.glob.length > 0 ? ` in ${truncate(args.glob, 25)}` : '';
            return `Searching \`${p}\`${g}`;
        }
        case 'Glob': return `Globbing \`${truncate(args.pattern, 50)}\``;
        case 'WebFetch':
        case 'WebBrowser': {
            try {
                return `Fetching ${new URL(String(args.url)).host}`;
            }
            catch {
                return `Fetching ...`;
            }
        }
        case 'WebSearch': return `Searching \`${truncate(args.query, 50)}\``;
        default: return `${toolName}(...)`;
    }
}
// 导出供测试用（friendlyToolDescription 是 module-private 纯函数）
export { friendlyToolDescription };
