/**
 * ============================================================
 *  plugins/workflow-runner/src/miniReAct.ts —— 简化版 ReAct 子循环
 * ------------------------------------------------------------
 *  本循环只做最纯净的 ReAct：调 LLM → 执行 tool_call → 喂回 → 再调 → 直到模型
 *  不再调工具或达到 maxTurns。**有三个调用方**（都要「有界、白名单、不污染主
 *  history、直接拿 StepResult」）：
 *    - execLlmStep 的 context_files 分支（llm.ts，maxTurns 默认 10）
 *    - agent: 节点（stepExecutors/agent.ts，默认 12）
 *    - onError:'recover' 恢复 agent（recovery.ts，默认 12）
 *
 *  **流式**：本循环边跑边 yield UI 事件——text（答案）/ reasoning（思维链）/
 *  tool_start/tool_end（工具）/ **stage_change（thinking→streaming→tool_executing，
 *  对齐 src/loop.ts::runQuery）**。stage_change 是 TUI LiveArea 显示的开关（idle 时
 *  LiveArea 整块隐藏），没有它则上述内层事件在 TUI 上发了也看不见。output_schema
 *  模式下**仍切 stage、但不回显 text**（避免把 JSON 原文当答案刷屏，对齐 llm.ts）。
 *
 *  为什么不复用 src/loop.ts::runQuery？
 *    - runQuery 依赖太重：history 持久化、自动压缩、反应式压缩自救、
 *      microCompact、reasoning 累积、sessionState 注入、SIGINT 标记消息等
 *    - workflow 子步骤是"短小内嵌"，不需要这些工程化保障
 *    - 独立实现可以精准控制：只用白名单工具、强 schema、重复调用 halt
 *
 *  关键决策：
 *    1. **每轮都传 responseFormat**（如果有 output_schema）。OpenAI 兼容
 *       provider 对 tool_calls + json_schema 混用通常允许：tool_calls 走 function
 *       协议，最终 assistant 的 text 是 JSON。这样比"双轮探测"简单可靠。
 *    2. **重复工具调用检测**：同 name 同 args 出现 3 次直接 halt，避免模型卡死。
 *    3. **executeTool 自带 Zod 校验和错误包装**，本循环只需透传 tool_call_id 配对。
 *    4. **maxTurns 默认 10**（vs runQuery 的 50），workflow 子步骤天生该收敛快。
 * ============================================================
 */
import { chat, executeTool, } from '../../../src/plugin-sdk.js';
import { extractJsonPayload } from './jsonPayload.js';
/**
 * 执行一次 ReAct 子循环。**边跑边 yield UI 事件（stage_change / text / reasoning /
 * tool_start / tool_end），同时 return 最终 StepResult**——让 recover / agent 节点 /
 * llm+context_files 三处子 agent 的内层思考在 TUI / webchat 实时可见（之前是纯 async
 * 不 yield，"哑"的；且漏发 stage_change 时 TUI LiveArea 整块隐藏，等于没显示）。
 * 调用方用 `const result = yield* runMiniReAct(...)` 既透传事件、又拿到 StepResult。
 *
 * 抛错场景：
 *   - 用户中断（signal.aborted）
 *   - 重复工具调用 >= 3 次
 *   - 用尽 maxTurns 仍未收敛
 *   - output_schema 解析失败 / 非对象
 */
export async function* runMiniReAct(args) {
    const { messages, tools, output_schema, provider, signal } = args;
    const maxTurns = args.maxTurns ?? 10;
    // 工作消息列表：拷贝一份，避免污染调用方
    const conv = [...messages];
    // schema 模式：把 schema 写进 system（同 llm.ts）。保证 provider 不支持 response_format
    // 而被 client.ts 退化掉时，模型仍知道确切字段结构、不至于瞎猜字段名。
    if (output_schema) {
        const hint = `\n\n最终只输出匹配以下 JSON Schema 的 JSON 对象本身（不要解释、不要 markdown 代码块）：\n${JSON.stringify(output_schema)}`;
        const sysIdx = conv.findIndex((m) => m.role === 'system');
        if (sysIdx >= 0) {
            conv[sysIdx] = { ...conv[sysIdx], content: `${conv[sysIdx].content ?? ''}${hint}` };
        }
        else {
            conv.unshift({ role: 'system', content: hint.trimStart() });
        }
    }
    let finalText = '';
    /** 上一轮是否仍有 tool_calls（用于检测 "用尽轮数还在调工具" 这一退出态） */
    let lastTurnHadToolCalls = false;
    /** 重复工具调用计数：key = `${name}:${arguments}` */
    const toolCallSignature = new Map();
    const responseFormat = output_schema
        ? {
            type: 'json_schema',
            json_schema: {
                name: 'minireact_output',
                schema: output_schema,
                strict: true,
            },
        }
        : undefined;
    for (let turn = 0; turn < maxTurns; turn++) {
        if (signal?.aborted)
            throw new Error('用户中断');
        // ---- 1. 调 LLM 并累积流式响应 ----
        let textAccum = '';
        const toolCallsAccum = [];
        // 进 LLM 流前本质是「等首 token」= thinking；首个 text_delta 时切 streaming（一次性）。
        // 对齐 src/loop.ts::runQuery —— 没有这两个 stage_change，TUI LiveArea 会一直 idle 隐藏，
        // 下面 yield 的 text/reasoning/tool 事件在 TUI 上等于白发。
        let stageStreamingYielded = false;
        yield { type: 'stage_change', stage: 'thinking' };
        for await (const ev of chat({
            provider,
            messages: conv,
            tools,
            responseFormat,
            signal,
        })) {
            if (ev.type === 'text_delta') {
                textAccum += ev.delta;
                if (!stageStreamingYielded) {
                    stageStreamingYielded = true;
                    yield { type: 'stage_change', stage: 'streaming' };
                }
                // output_schema 模式：切了 streaming 阶段但**不回显 JSON 原文**（不把结构化产出
                // 当答案刷屏，对齐 llm.ts:121 的克制）；非 schema 模式才把答案流式 yield 给 UI。
                if (!output_schema)
                    yield { type: 'text', delta: ev.delta };
            }
            else if (ev.type === 'tool_call_delta') {
                mergeToolCallDelta(toolCallsAccum, ev);
            }
            else if (ev.type === 'reasoning_delta') {
                // 思维链字符串增量透传给 UI（reasoning_details 是结构化数组、无 delta，跳过）
                if (ev.delta)
                    yield { type: 'reasoning', delta: ev.delta };
            }
            else if (ev.type === 'done' && ev.stopReason === 'aborted') {
                throw new Error('用户中断');
            }
        }
        // ---- 2. 把 assistant 消息推入 conv ----
        conv.push({
            role: 'assistant',
            content: textAccum.length > 0 ? textAccum : null,
            ...(toolCallsAccum.length > 0 ? { tool_calls: toolCallsAccum } : {}),
        });
        // ---- 3. 无 tool_calls → 结束循环 ----
        if (toolCallsAccum.length === 0) {
            finalText = textAccum.trim();
            lastTurnHadToolCalls = false;
            break;
        }
        lastTurnHadToolCalls = true;
        // ---- 4. 执行所有 tool_calls，结果回填为 tool 消息 ----
        // 进工具执行 = tool_executing 阶段（驱动 TUI LiveArea 显示运行中工具，对齐 loop.ts:340）。
        yield { type: 'stage_change', stage: 'tool_executing' };
        for (const tc of toolCallsAccum) {
            // 重复检测
            const sig = `${tc.function.name}:${tc.function.arguments}`;
            const count = (toolCallSignature.get(sig) ?? 0) + 1;
            toolCallSignature.set(sig, count);
            if (count >= 3) {
                throw new Error(`miniReAct: 检测到重复工具调用（${tc.function.name} 同参数已 ${count} 次），可能陷入循环，halt。`);
            }
            if (signal?.aborted)
                throw new Error('用户中断');
            // 执行前后 yield tool_start/tool_end，让 UI 看到子 agent 在用工具（事件形状对齐
            // src/loop.ts；miniReAct 串行执行，顺序 yield 即可，不需要 loop 的并行事件队列）。
            yield {
                type: 'tool_start',
                toolName: tc.function.name,
                toolCallId: tc.id,
                argsPreview: previewArgs(tc.function.arguments),
            };
            const result = await executeTool(tc.function.name, tc.function.arguments, signal);
            const toolContent = result.ok ? result.content : `工具错误: ${result.error}`;
            yield {
                type: 'tool_end',
                toolName: tc.function.name,
                toolCallId: tc.id,
                ok: result.ok,
                content: toolContent,
            };
            conv.push({
                role: 'tool',
                tool_call_id: tc.id,
                content: toolContent,
            });
        }
        // 继续下一轮
    }
    // 用尽 maxTurns 且最后一轮仍有 tool_calls
    if (lastTurnHadToolCalls) {
        throw new Error(`miniReAct: 用尽 ${maxTurns} 轮未得到最终回答（仍在调工具）`);
    }
    // ---- 5. 输出处理 ----
    if (output_schema) {
        // 同 llm.ts：先剥 <think> 思维链/围栏/前后散文再 parse（见 jsonPayload.ts 头注）。
        const payload = extractJsonPayload(finalText);
        let parsed;
        try {
            parsed = JSON.parse(payload);
        }
        catch (e) {
            throw new Error(`miniReAct: output_schema 要求 JSON 但解析失败: ${e.message}\n收到: ${finalText.slice(0, 200)}`);
        }
        if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
            throw new Error(`miniReAct: output_schema 要求对象，得到 ${Array.isArray(parsed) ? 'array' : typeof parsed}`);
        }
        return {
            raw: parsed,
            preview: `[json+tools] ${finalText.slice(0, 180)}${finalText.length > 180 ? '...' : ''}`,
        };
    }
    return {
        raw: { text: finalText, result: finalText },
        preview: finalText.length > 200 ? `${finalText.slice(0, 200)}...` : finalText,
    };
}
// ---------------- 辅助 ----------------
/** 把一条 tool_call_delta 合并到累积数组上（与 src/loop.ts mergeToolCallDelta 同语义） */
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;
}
/** 给 tool_start 事件用的参数预览（压成一行、截断到 60 字符），与 loop.ts previewArgs 同义。 */
function previewArgs(rawJson) {
    const oneLine = rawJson.replace(/\s+/g, ' ').trim();
    return oneLine.length <= 60 ? oneLine : `${oneLine.slice(0, 60)}...`;
}
