import get from 'lodash-es/get.js'
import runFanout from './runFanout.mjs'
import runRolePipeline from './runRolePipeline.mjs'
// runFanoutPipeline.mjs — FanoutPipeline工作流(Fanout+RolePipeline): 多開收斂成果接串行角色鏈
//
// 【結構】先跑runFanout(多開 → 整合), 其成果作為runRolePipeline的input
// (各階段以ctx.input取用), 最末階段回傳即工作流成果。
//
// 【實測依據(2026-08-10評比)】Fanout+RolePipeline是品質天花板: 前段的多樣性擇優給出最豐底稿、
// 審計鏈再修幻覺與證據標注; 六模型的歷史最高品質全部出現在此組合(或與純RolePipeline並列)。
//
// 【部分接受】前段失敗即回(附前段完整明細, 含已成功候選);
// 後段失敗回傳前段成果與後段已完成階段——呼叫端可只重跑失敗段。
/**
* 執行FanoutPipeline工作流(Fanout+RolePipeline):多開+整合+串行角色鏈
*
* 特點:
* 前段同runFanout(agents各名額可自帶fallback、integrate單點整合);
* 後段同runRolePipeline(stages各階段可自帶AI/fallback/提示詞),其input即前段成果;
* 本函數不會reject
*
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {Object} opt.providers 輸入provider定義表物件(名稱 → 條目)
* @param {String} opt.task 輸入前段各名額共用之任務提示詞字串
* @param {Array} opt.agents 輸入前段名額規格陣列(同runFanout)
* @param {Object} opt.integrate 輸入前段整合名額規格物件(同runFanout)
* @param {Array} opt.stages 輸入後段階段規格陣列(同runRolePipeline),各階段以ctx.input取得前段成果
* @param {Function} [opt.check=null] 輸入前段共用檢核函數,後段各階段自帶check,預設null
* @param {String} [opt.schema=''] 輸入輸出格式示意字串(供前段預設整合模板),預設''
* @param {Number} [opt.minCandidates=2] 輸入前段整合門檻正整數,預設2
* @param {Object} [opt.callOpt={}] 輸入透傳兩段之共用呼叫設定,預設{}
* @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(布林值)、result(工作流成果)、A(前段runFanout完整結果)、B(後段runRolePipeline完整結果)、totalMs(總耗時毫秒)、error(錯誤訊息字串),本函數不會reject
* @example
* //need cli in system PATH
*
* import runFanoutPipeline from './src/wkf/runFanoutPipeline.mjs'
*
* let providers = {
* 'sonnet': { kind: 'claude', model: 'sonnet' },
* 'luna': { kind: 'codex', model: 'gpt-5.6-luna' },
* }
*
* let test = async () => {
*
* let r = await runFanoutPipeline({
* providers,
* task: '分析並只回覆JSON: {"essence":"..."}',
* agents: [{ use: 'sonnet' }, { use: 'luna' }],
* integrate: { use: 'sonnet' },
* stages: [
* { id: 'audit', use: 'luna', prompt: (ctx) => `審計此稿並修訂, 只回覆同格式JSON: ${JSON.stringify(ctx.input)}` },
* ],
* check: (j) => !!j.essence,
* })
* console.log(r.ok, r.A.integrated, r.B.order)
* // => true true [ 'audit' ]
*
* }
* await test()
* .catch((err) => {
* console.log(err)
* })
*
*/
async function runFanoutPipeline(opt = {}) {
let t0 = Date.now()
//前段: Fanout(多開 → 整合)
let rA = await runFanout({
providers: get(opt, 'providers', null),
task: get(opt, 'task', ''),
agents: get(opt, 'agents', null),
integrate: get(opt, 'integrate', null),
check: get(opt, 'check', null),
schema: get(opt, 'schema', ''),
minCandidates: get(opt, 'minCandidates', null),
callOpt: get(opt, 'callOpt', {}),
})
if (!rA.ok) {
return { ok: false, result: null, A: rA, B: null, totalMs: Date.now() - t0, error: `A failed: ${rA.error}` }
}
//後段: RolePipeline, input即前段成果
let rB = await runRolePipeline({
providers: get(opt, 'providers', null),
input: rA.result,
stages: get(opt, 'stages', null),
callOpt: get(opt, 'callOpt', {}),
})
return {
ok: rB.ok,
result: rB.ok ? rB.result : null,
A: rA, //前段成果與明細一律回傳——後段失敗時可據此只重跑後段
B: rB,
totalMs: Date.now() - t0,
error: rB.ok ? '' : `B failed: ${rB.error}`,
}
}
export default runFanoutPipeline