// NOTE: output_schema 不支持 skill 节点——loader 现已直接拒绝 skill+output_schema 组合
// （见 loader.ts validateSteps，P3 修复：静默 no-op 改为加载期报错）。
// 仅 llm 节点支持 provider-native structured output；vote 通过 runVoter 调 execLlmStep（llm 虚拟步骤）。
/**
 * ============================================================
 *  plugins/workflow-runner/src/stepExecutors/skill.ts —— skill step 执行
 * ------------------------------------------------------------
 *  把一个具名 skill（skills/<name>/SKILL.md）当作"迷你 ReAct 子循环"
 *  跑一次：复用 src/loop.ts::runQuery，让模型在子 history 内 T-A-O-R。
 *
 *  实现要点：
 *    - 双源查找：cwd/skills/<name>/SKILL.md 优先，packageRoot/skills/ fallback；
 *      支持按目录名 or 按 frontmatter::name 匹配
 *    - 剥掉 frontmatter，body 作 system 指令；step.input 插值后作为 user
 *    - 子 history 与外层 ctx.history 隔离，避免污染主对话
 *    - 流式转发：runQuery yield 的事件直接 yield 出去（让 UI 看到子循环进度）
 *    - 收集 text 事件累计为 raw.text，供 capture 绑变量使用
 *    - maxTurns 可在 step 里覆盖，默认走 runQuery 自带（50）
 *
 *  capture：{ text: var_name } / { result: var_name }
 * ============================================================
 */
import { readdir, readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { getResourceSearchPaths, runQuery, } from '../../../../src/plugin-sdk.js';
import { interpolate } from '../expressions.js';
function stripFrontmatter(content) {
    return content.replace(/^---\n[\s\S]*?\n---\n?/, '');
}
function parseFrontmatterName(content) {
    const m = content.match(/^---\n([\s\S]*?)\n---/);
    if (!m)
        return null;
    const nm = m[1].match(/^name:\s*(.+)$/m);
    if (!nm)
        return null;
    return nm[1].trim().replace(/^["']|["']$/g, '');
}
async function findSkillFile(skillName) {
    const roots = getResourceSearchPaths('skills', import.meta.url);
    for (const root of roots) {
        let entries;
        try {
            entries = await readdir(root, { withFileTypes: true });
        }
        catch {
            continue;
        }
        for (const e of entries) {
            if (!e.isDirectory())
                continue;
            const skillFile = join(root, e.name, 'SKILL.md');
            if (e.name === skillName)
                return skillFile;
            try {
                const md = await readFile(skillFile, 'utf8');
                if (parseFrontmatterName(md) === skillName)
                    return skillFile;
            }
            catch {
                // 该子目录无 SKILL.md，跳过
            }
        }
    }
    return null;
}
export async function* execSkillStep(step, vars, ctx) {
    if (!step.skill)
        throw new Error(`step ${step.id}: 缺少 skill 字段`);
    const skillName = interpolate(step.skill, vars);
    const skillFile = await findSkillFile(skillName);
    if (!skillFile) {
        throw new Error(`step ${step.id}: 未找到 skill "${skillName}"（cwd/skills/ 与 packageRoot/skills/ 双源均未命中）`);
    }
    const md = await readFile(skillFile, 'utf8');
    const body = stripFrontmatter(md).trim();
    const userInput = step.input
        ? interpolate(step.input, vars)
        : `请按 SKILL "${skillName}" 的指示完成任务。`;
    const subHistory = [
        {
            role: 'system',
            content: `下面是要遵循的 SKILL 指示（来源：${skillFile}）：\n\n${body}`,
        },
    ];
    let text = '';
    for await (const ev of runQuery(userInput, {
        provider: ctx.provider,
        history: subHistory,
        signal: ctx.signal,
        maxTurns: step.maxTurns,
    })) {
        if (ev.type === 'text')
            text += ev.delta;
        if (ev.type === 'interrupted')
            throw new Error('用户中断');
        if (ev.type === 'error')
            throw new Error(ev.error);
        // 子 skill 内部那次 runQuery 的 turn_done 是它自己的终结标记，不是整个 workflow 的终结。
        // 不外泄——否则把 turn_done 当终结的消费者（-p --output-format json / gateway broker）
        // 会在 workflow 中途（如 critic 步之后、verify 步之前）误判结束、漏掉后续步骤。
        // workflow 的对外收口由 index.ts::runWorkflowFromCommand 末尾统一补发。
        if (ev.type === 'turn_done')
            continue;
        yield ev;
    }
    const trimmed = text.trim();
    return {
        raw: { text: trimmed, result: trimmed },
        preview: trimmed.length > 200 ? `${trimmed.slice(0, 200)}...` : trimmed,
    };
}
