dispatchAiWkf.mjs

import get from 'lodash-es/get.js'
import isobj from 'wsemi/src/isobj.mjs'
import callAiWithFallback from './wkf/callAiWithFallback.mjs'
import runFanout from './wkf/runFanout.mjs'
import runRolePipeline from './wkf/runRolePipeline.mjs'
import runFanoutPipeline from './wkf/runFanoutPipeline.mjs'


// dispatchAiWkf.mjs — 工作流工廠: 注入provider定義表與共用預設, 回傳綁定版API
//
// 【用途】專案端只需注入一次providers(名稱 → dispatchAiFallback條目)與共用設定
//   (cwd、store、onEvent、timeoutMs…), 之後以名稱宣告工作流即可, 不必每次傳定義表。
//
// 【並行與游標之說明】多名額並行且共用同一store時, 游標read-modify-write
//   可能交錯, 造成金鑰輪替不完全均攤——只影響公平性、不影響正確性(每把金鑰仍有效),
//   故不加鎖; 要求嚴格均攤者可注入自帶佇列的store。
//
// 【本函數為同步工廠會throw】providers無效屬設定錯誤, 應於啟動期即失敗(fail fast),
//   與各dispatch函數「不reject」之約定不衝突——後者是執行期呼叫, 前者是組裝期設定。


/**
 * 建立AI工作流執行環境(工廠),注入provider定義表與共用預設後回傳綁定版API
 *
 * 特點:
 * providers為名稱對dispatchAiFallback條目之定義表,之後各工作流以名稱宣告主模型與遞補鏈;
 * defaults為共用呼叫設定,各工作流之callOpt與名額規格可逐項覆寫;
 * 回傳之各函數皆不reject;本工廠為同步函數,providers無效時throw(設定錯誤應於啟動期即失敗)
 *
 * @param {Object} opt 輸入設定物件
 * @param {Object} opt.providers 輸入provider定義表物件(名稱 → dispatchAiFallback條目:{ kind, model, keys, exe, provider, config, sandbox, extraArgs... })
 * @param {Object} [opt.defaults={}] 輸入共用呼叫設定物件(cwd、store、onEvent、timeoutMs、budgetMs、maxRetries、promptPrefix、parse等),預設{}
 * @returns {Object} 回傳綁定版API物件,內含callAi(單一名額呼叫)、runFanout(多開+整合)、runRolePipeline(串行角色鏈)、runFanoutPipeline(多開+整合+角色鏈)、providers(定義表原樣)
 * @example
 * //need cli in system PATH
 *
 * import dispatchAiWkf from './src/dispatchAiWkf.mjs'
 *
 * let wkf = dispatchAiWkf({
 *     providers: {
 *         'deepseek': { kind: 'opencode', model: 'opencode/deepseek-v4-flash-free', provider: 'opencode', keys: ['sk-xxx'] },
 *         'sonnet': { kind: 'claude', model: 'sonnet' },
 *         'luna': { kind: 'codex', model: 'gpt-5.6-luna' },
 *     },
 *     defaults: { timeoutMs: 300000 },
 * })
 *
 * let test = async () => {
 *
 *     //單一名額: 主模型+遞補鏈
 *     let r1 = await wkf.callAi('只回覆JSON: {"a":1}', { spec: { use: 'deepseek', fallback: ['sonnet'] }, check: (j) => j.a === 1 })
 *     console.log(r1.ok, r1.json)
 *     // => true { a: 1 }
 *
 *     //Fanout工作流: 多開執行+單點整合
 *     let r2 = await wkf.runFanout({
 *         task: '分析並只回覆JSON: {"essence":"..."}',
 *         agents: [{ use: 'deepseek', fallback: ['sonnet'] }, { use: 'sonnet' }],
 *         integrate: { use: 'luna' },
 *         check: (j) => !!j.essence,
 *     })
 *     console.log(r2.ok, r2.integrated)
 *     // => true true
 *
 * }
 * await test()
 *     .catch((err) => {
 *         console.log(err)
 *     })
 *
 */
function dispatchAiWkf(opt = {}) {
    let providers = get(opt, 'providers', null)
    if (!isobj(providers)) {
        throw new Error('dispatchAiWkf: opt.providers must be an object (name → provider entry)')
    }
    let defaults = get(opt, 'defaults', null)
    if (!isobj(defaults)) {
        defaults = {}
    }

    return {
        providers,

        //單一名額呼叫: callAi(prompt, { spec:{use,fallback}, check, ... })
        callAi: (prompt, o = {}) => callAiWithFallback(prompt, { ...defaults, ...o, providers }),

        //Fanout工作流: runFanout({ task, agents, integrate, check, schema, minCandidates, callOpt? })
        runFanout: (o = {}) => runFanout({ ...o, providers, callOpt: { ...defaults, ...get(o, 'callOpt', {}) } }),

        //RolePipeline工作流: runRolePipeline({ input, stages, callOpt? })
        runRolePipeline: (o = {}) => runRolePipeline({ ...o, providers, callOpt: { ...defaults, ...get(o, 'callOpt', {}) } }),

        //FanoutPipeline工作流: runFanoutPipeline({ task, agents, integrate, stages, check, schema, callOpt? })
        runFanoutPipeline: (o = {}) => runFanoutPipeline({ ...o, providers, callOpt: { ...defaults, ...get(o, 'callOpt', {}) } }),
    }
}


export default dispatchAiWkf