w-dispatch-ai 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/w-dispatch-ai.umd.js +2 -2
- package/dist/w-dispatch-ai.umd.js.map +1 -1
- package/docs/WDispatchAi.mjs.html +1 -1
- package/docs/adapters.mjs.html +1 -1
- package/docs/dfTimeoutMs.mjs.html +1 -1
- package/docs/dispatchAi.mjs.html +1 -1
- package/docs/dispatchAiFallback.mjs.html +71 -6
- package/docs/dispatchAiWkf.mjs.html +1 -1
- package/docs/dispatchAntigravity.mjs.html +1 -1
- package/docs/dispatchApiOpenaiCompat.mjs.html +1 -1
- package/docs/dispatchClaude.mjs.html +1 -1
- package/docs/dispatchCodex.mjs.html +1 -1
- package/docs/dispatchOpencode.mjs.html +1 -1
- package/docs/getCliArgs.mjs.html +1 -1
- package/docs/getErrorResult.mjs.html +1 -1
- package/docs/global.html +91 -11
- package/docs/index.html +1 -1
- package/docs/resolveProviders.mjs.html +1 -1
- package/docs/wkf_callAiWithFallback.mjs.html +13 -7
- package/docs/wkf_extractJsonLoose.mjs.html +1 -1
- package/docs/wkf_runFanout.mjs.html +11 -8
- package/docs/wkf_runFanoutPipeline.mjs.html +2 -2
- package/docs/wkf_runRolePipeline.mjs.html +1 -1
- package/package.json +1 -1
- package/src/dispatchAiFallback.mjs +70 -5
- package/src/wkf/callAiWithFallback.mjs +12 -6
- package/src/wkf/runFanout.mjs +10 -7
- package/src/wkf/runFanoutPipeline.mjs +1 -1
- package/test/unit-callAiWithFallback.test.mjs +15 -0
- package/test/unit-dispatchAiFallback.test.mjs +95 -0
- package/test/unit-dispatchAiWkf.test.mjs +8 -0
- package/test/unit-dispatchApiOpenaiCompat.test.mjs +28 -0
- package/test/unit-runFanout.test.mjs +46 -0
package/src/wkf/runFanout.mjs
CHANGED
|
@@ -50,9 +50,9 @@ ${candidates.map((c, i) => `【候選 ${i + 1}】\n${JSON.stringify(c)}`).join('
|
|
|
50
50
|
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
51
51
|
* @param {Object} opt.providers 輸入provider定義表物件(名稱 → 條目),透傳callAiWithFallback
|
|
52
52
|
* @param {String} opt.task 輸入前段各名額共用之任務提示詞字串
|
|
53
|
-
* @param {Array} opt.agents 輸入前段名額規格陣列,各元素{ use, fallback, maxRetries?, timeoutMs? }等(
|
|
54
|
-
* @param {Object} opt.integrate 輸入整合名額規格物件{ use, fallback, prompt?, ... },prompt可為(candidates)=>String
|
|
55
|
-
* @param {Function} [opt.check=null]
|
|
53
|
+
* @param {Array} opt.agents 輸入前段名額規格陣列,各元素{ use, fallback, check?, maxRetries?, timeoutMs? }等(check可覆寫頂層檢核;除use/fallback/check外之鍵覆寫該名額呼叫設定)
|
|
54
|
+
* @param {Object} opt.integrate 輸入整合名額規格物件{ use, fallback, prompt?, check?, ... },prompt可為(candidates)=>String自訂整合提示詞(省略用預設模板);check為終稿專屬檢核(終稿判準常與候選不同,如須含固定段落),未給則沿用頂層check
|
|
55
|
+
* @param {Function} [opt.check=null] 輸入檢核函數(json)=>Boolean,作為候選與終稿之共用預設,名額規格與integrate可各自帶check覆寫,預設null
|
|
56
56
|
* @param {String} [opt.schema=''] 輸入輸出格式示意字串,供預設整合模板嵌入,預設''
|
|
57
57
|
* @param {Number} [opt.minCandidates=2] 輸入進入整合所需之最少成功候選數正整數,未達門檻以首位候選為成果,預設2
|
|
58
58
|
* @param {Object} [opt.callOpt={}] 輸入透傳callAiWithFallback之共用設定(cwd、store、onEvent、timeoutMs、promptPrefix等),預設{}
|
|
@@ -114,9 +114,11 @@ async function runFanout(opt = {}) {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
//前段: 並行多開, 個別失敗不炸整輪
|
|
117
|
+
//各名額可自帶check覆寫頂層(候選與終稿判準本可不同); 自spec抽出而非留在overrides,
|
|
118
|
+
//否則會被後方明給之check靜默覆蓋(曾為死鍵)
|
|
117
119
|
let rsAgents = await Promise.all(agents.map((spec) => {
|
|
118
|
-
let { use, fallback, ...overrides } = spec
|
|
119
|
-
return callAiWithFallback(task, { ...callOpt, ...overrides, providers, spec: { use, fallback }, check })
|
|
120
|
+
let { use, fallback, check: checkAgent, ...overrides } = spec
|
|
121
|
+
return callAiWithFallback(task, { ...callOpt, ...overrides, providers, spec: { use, fallback }, check: isfun(checkAgent) ? checkAgent : check })
|
|
120
122
|
}))
|
|
121
123
|
let candidates = rsAgents.filter((r) => r.ok).map((r) => r.json)
|
|
122
124
|
|
|
@@ -134,9 +136,10 @@ async function runFanout(opt = {}) {
|
|
|
134
136
|
if (!integrate || !isestr(get(integrate, 'use', ''))) {
|
|
135
137
|
return { ok: false, result: null, integrated: false, agents: rsAgents, candidates, totalMs: Date.now() - t0, error: 'integrate spec (with use) is required' }
|
|
136
138
|
}
|
|
137
|
-
|
|
139
|
+
//整合可帶獨立check(終稿判準常較候選嚴, 如須含固定小標題), 未給則沿用頂層check
|
|
140
|
+
let { use, fallback, prompt: intPromptFn, check: checkInt, ...intOverrides } = integrate
|
|
138
141
|
let intPrompt = isfun(intPromptFn) ? intPromptFn(candidates) : defaultIntegratePrompt(candidates, opt)
|
|
139
|
-
let rInt = await callAiWithFallback(intPrompt, { ...callOpt, ...intOverrides, providers, spec: { use, fallback }, check })
|
|
142
|
+
let rInt = await callAiWithFallback(intPrompt, { ...callOpt, ...intOverrides, providers, spec: { use, fallback }, check: isfun(checkInt) ? checkInt : check })
|
|
140
143
|
|
|
141
144
|
return {
|
|
142
145
|
ok: rInt.ok,
|
|
@@ -29,7 +29,7 @@ import runRolePipeline from './runRolePipeline.mjs'
|
|
|
29
29
|
* @param {Array} opt.agents 輸入前段名額規格陣列(同runFanout)
|
|
30
30
|
* @param {Object} opt.integrate 輸入前段整合名額規格物件(同runFanout)
|
|
31
31
|
* @param {Array} opt.stages 輸入後段階段規格陣列(同runRolePipeline),各階段以ctx.input取得前段成果
|
|
32
|
-
* @param {Function} [opt.check=null]
|
|
32
|
+
* @param {Function} [opt.check=null] 輸入前段共用檢核函數(前段名額規格與integrate可各自帶check覆寫,同runFanout),後段各階段自帶check,預設null
|
|
33
33
|
* @param {String} [opt.schema=''] 輸入輸出格式示意字串(供前段預設整合模板),預設''
|
|
34
34
|
* @param {Number} [opt.minCandidates=2] 輸入前段整合門檻正整數,預設2
|
|
35
35
|
* @param {Object} [opt.callOpt={}] 輸入透傳兩段之共用呼叫設定,預設{}
|
|
@@ -135,6 +135,21 @@ describe('callAiWithFallback', function() {
|
|
|
135
135
|
assert.strict.deepEqual(r, rr)
|
|
136
136
|
})
|
|
137
137
|
|
|
138
|
+
it('minAttemptMs等fallback層設定可經工作流層原樣轉傳(曾因白名單漏鍵而失效)', async function() {
|
|
139
|
+
//budgetMs=60000(高於預設門檻20000)+minAttemptMs=600000(遠大於budget):
|
|
140
|
+
//minAttemptMs有被轉送才會立即budget exhausted; 漏鍵時會照樣執行並成功
|
|
141
|
+
let t = await callAiWithFallback('abc', {
|
|
142
|
+
providers,
|
|
143
|
+
spec: { use: 'p-claude' },
|
|
144
|
+
promptPrefix: '',
|
|
145
|
+
budgetMs: 60000,
|
|
146
|
+
minAttemptMs: 600000,
|
|
147
|
+
})
|
|
148
|
+
let r = [t.ok, t.error, t.tried.map((x) => x.outcome)]
|
|
149
|
+
let rr = [false, 'budget exhausted', ['budget-out']]
|
|
150
|
+
assert.strict.deepEqual(r, rr)
|
|
151
|
+
})
|
|
152
|
+
|
|
138
153
|
it('多金鑰條目經由keys輪替且keyId帶索引', async function() {
|
|
139
154
|
let t = await callAiWithFallback('abc', { providers, spec: { use: 'p-oc' }, promptPrefix: '' })
|
|
140
155
|
let auth = JSON.parse(t.json.env.OPENCODE_AUTH_CONTENT)
|
|
@@ -316,6 +316,101 @@ describe('dispatchAiFallback', function() {
|
|
|
316
316
|
assert.strict.deepEqual(r, rr)
|
|
317
317
|
})
|
|
318
318
|
|
|
319
|
+
it('cooldownMs啟用時, 逾時觸發冷卻且次輪該條目降至鏈尾(只降序不移除)', async function() {
|
|
320
|
+
let stored = { cursors: {}, cooling: {} }
|
|
321
|
+
let store = {
|
|
322
|
+
get: () => stored,
|
|
323
|
+
set: (s) => {
|
|
324
|
+
stored = s
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
let pHang = { id: 'cd-hang', kind: 'claude', exe: fake.exe, extraArgs: ['--fake-sleep=9000'], timeoutMs: 500 }
|
|
328
|
+
let pOk = { id: 'cd-ok', kind: 'claude', exe: fake.exe }
|
|
329
|
+
//第1輪: hang在鏈首 → 逾時跳組並觸發冷卻 → ok遞補
|
|
330
|
+
let r1 = await dispatchAiFallback('abc', { providers: [pHang, pOk], store, cooldownMs: 300000 })
|
|
331
|
+
//第2輪: hang冷卻中 → 降至鏈尾, ok先上且成功, hang完全未被嘗試
|
|
332
|
+
let r2 = await dispatchAiFallback('abc', { providers: [pHang, pOk], store, cooldownMs: 300000 })
|
|
333
|
+
let r = [
|
|
334
|
+
r1.providerId,
|
|
335
|
+
r1.tried.map((x) => x.providerId),
|
|
336
|
+
typeof stored.cooling['cd-hang'],
|
|
337
|
+
r2.providerId,
|
|
338
|
+
r2.tried.map((x) => x.providerId), //只有cd-ok, 未浪費一次逾時
|
|
339
|
+
]
|
|
340
|
+
let rr = ['cd-ok', ['cd-hang', 'cd-ok'], 'number', 'cd-ok', ['cd-ok']]
|
|
341
|
+
assert.strict.deepEqual(r, rr)
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('冷卻中的條目於前面全敗時仍會被嘗試, 且成功即解除冷卻', async function() {
|
|
345
|
+
let stored = { cursors: {}, cooling: { 'cd2-a': Date.now() } } //預置: a冷卻中
|
|
346
|
+
let store = {
|
|
347
|
+
get: () => stored,
|
|
348
|
+
set: (s) => {
|
|
349
|
+
stored = s
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
let r1 = await dispatchAiFallback('abc', {
|
|
353
|
+
providers: [
|
|
354
|
+
{ id: 'cd2-a', kind: 'claude', exe: fake.exe }, //冷卻中 → 降至鏈尾
|
|
355
|
+
{ id: 'cd2-b', kind: 'claude', exe: fake.exe, extraArgs: ['--fake-exit=1'] }, //鏈首但必敗
|
|
356
|
+
],
|
|
357
|
+
store,
|
|
358
|
+
cooldownMs: 300000,
|
|
359
|
+
})
|
|
360
|
+
let r = [
|
|
361
|
+
r1.ok,
|
|
362
|
+
r1.providerId, //b先上失敗 → 降序後的a仍被嘗試且成功
|
|
363
|
+
r1.tried.map((x) => x.providerId),
|
|
364
|
+
stored.cooling['cd2-a'], //成功即解除
|
|
365
|
+
]
|
|
366
|
+
let rr = [true, 'cd2-a', ['cd2-b', 'cd2-a'], undefined]
|
|
367
|
+
assert.strict.deepEqual(r, rr)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
it('冷卻過期後恢復原順序並清除過期紀錄', async function() {
|
|
371
|
+
let stored = { cursors: {}, cooling: { 'cd3-a': Date.now() - 1000 } } //1秒前觸發
|
|
372
|
+
let store = {
|
|
373
|
+
get: () => stored,
|
|
374
|
+
set: (s) => {
|
|
375
|
+
stored = s
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
let r1 = await dispatchAiFallback('abc', {
|
|
379
|
+
providers: [
|
|
380
|
+
{ id: 'cd3-a', kind: 'claude', exe: fake.exe },
|
|
381
|
+
{ id: 'cd3-b', kind: 'claude', exe: fake.exe },
|
|
382
|
+
],
|
|
383
|
+
store,
|
|
384
|
+
cooldownMs: 500, //視窗0.5秒, 已過期
|
|
385
|
+
})
|
|
386
|
+
let r = [r1.providerId, stored.cooling['cd3-a']]
|
|
387
|
+
let rr = ['cd3-a', undefined] //恢復鏈首, 過期紀錄被清除
|
|
388
|
+
assert.strict.deepEqual(r, rr)
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('cooldownMs未啟用(預設)時不重排也不寫入cooling', async function() {
|
|
392
|
+
let stored = { cursors: {}, cooling: { 'cd4-a': Date.now() } }
|
|
393
|
+
let store = {
|
|
394
|
+
get: () => stored,
|
|
395
|
+
set: (s) => {
|
|
396
|
+
stored = s
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
let r1 = await dispatchAiFallback('abc', {
|
|
400
|
+
providers: [
|
|
401
|
+
{ id: 'cd4-a', kind: 'claude', exe: fake.exe, extraArgs: ['--fake-sleep=9000'], timeoutMs: 500 },
|
|
402
|
+
{ id: 'cd4-b', kind: 'claude', exe: fake.exe },
|
|
403
|
+
],
|
|
404
|
+
store,
|
|
405
|
+
})
|
|
406
|
+
let r = [
|
|
407
|
+
r1.tried.map((x) => x.providerId), //cooling有預置紀錄仍不重排(未啟用)
|
|
408
|
+
typeof stored.cooling['cd4-a'] === 'number' && stored.cooling['cd4-b'] === undefined, //逾時亦不新增紀錄
|
|
409
|
+
]
|
|
410
|
+
let rr = [['cd4-a', 'cd4-b'], true]
|
|
411
|
+
assert.strict.deepEqual(r, rr)
|
|
412
|
+
})
|
|
413
|
+
|
|
319
414
|
it('失敗事件與tried帶被拒回覆(stdout)與錯誤輸出(stderr)供診斷', async function() {
|
|
320
415
|
let evs = []
|
|
321
416
|
let t = await dispatchAiFallback('abc', {
|
|
@@ -86,6 +86,14 @@ describe('dispatchAiWkf', function() {
|
|
|
86
86
|
assert.strict.deepEqual(r, rr)
|
|
87
87
|
})
|
|
88
88
|
|
|
89
|
+
it('defaults之minAttemptMs經callAi生效(README第一層教學之回歸測試)', async function() {
|
|
90
|
+
let wkf = dispatchAiWkf({ providers, defaults: { promptPrefix: '', budgetMs: 60000, minAttemptMs: 600000 } })
|
|
91
|
+
let t = await wkf.callAi('abc', { spec: { use: 'p-a' } })
|
|
92
|
+
let r = [t.ok, t.error]
|
|
93
|
+
let rr = [false, 'budget exhausted']
|
|
94
|
+
assert.strict.deepEqual(r, rr)
|
|
95
|
+
})
|
|
96
|
+
|
|
89
97
|
it('runFanout綁定providers與defaults(defaults併入callOpt)', async function() {
|
|
90
98
|
let wkf = dispatchAiWkf({ providers, defaults: { promptPrefix: '' } })
|
|
91
99
|
let t = await wkf.runFanout({
|
|
@@ -245,6 +245,34 @@ describe('dispatchApiOpenaiCompat', function() {
|
|
|
245
245
|
assert.strict.deepEqual(r, rr)
|
|
246
246
|
})
|
|
247
247
|
|
|
248
|
+
it('HTTP 429觸發冷卻: 次輪該條目降至鏈尾', async function() {
|
|
249
|
+
let stored = { cursors: {}, cooling: {} }
|
|
250
|
+
let store = {
|
|
251
|
+
get: () => stored,
|
|
252
|
+
set: (s) => {
|
|
253
|
+
stored = s
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
//flaky-429對同一金鑰首次回429; maxRetries預設0故第1輪即失敗並觸發冷卻
|
|
257
|
+
let p429 = { id: 'cd-api-429', kind: 'api-openai-compat', baseURL: svr.url, model: 'flaky-429', keys: ['sk-good-cd429'] }
|
|
258
|
+
let pEcho = { id: 'cd-api-echo', kind: 'api-openai-compat', baseURL: svr.url, model: 'echo', keys: ['sk-good-cd-e'] }
|
|
259
|
+
let r1 = await dispatchAiFallback('abc', { providers: [p429, pEcho], store, cooldownMs: 300000 })
|
|
260
|
+
let r2 = await dispatchAiFallback('abc', { providers: [p429, pEcho], store, cooldownMs: 300000 })
|
|
261
|
+
let r = [
|
|
262
|
+
r1.providerId,
|
|
263
|
+
r1.tried.map((x) => [x.providerId, x.error || null]),
|
|
264
|
+
typeof stored.cooling['cd-api-429'],
|
|
265
|
+
r2.tried.map((x) => x.providerId), //429者已降尾, echo先上即成功
|
|
266
|
+
]
|
|
267
|
+
let rr = [
|
|
268
|
+
'cd-api-echo',
|
|
269
|
+
[['cd-api-429', 'HTTP 429'], ['cd-api-echo', null]],
|
|
270
|
+
'number',
|
|
271
|
+
['cd-api-echo'],
|
|
272
|
+
]
|
|
273
|
+
assert.strict.deepEqual(r, rr)
|
|
274
|
+
})
|
|
275
|
+
|
|
248
276
|
it('可作為工作流provider, 回覆JSON經解析與檢核', async function() {
|
|
249
277
|
let wkf = dispatchAiWkf({
|
|
250
278
|
providers: {
|
|
@@ -156,6 +156,52 @@ describe('runFanout', function() {
|
|
|
156
156
|
assert.strict.deepEqual(r, rr)
|
|
157
157
|
})
|
|
158
158
|
|
|
159
|
+
it('integrate可帶獨立check(終稿判準), 未給則沿用頂層check', async function() {
|
|
160
|
+
//頂層check放行所有echo物件; integrate.check要求stdin含候選標記(僅整合者符合)
|
|
161
|
+
let t1 = await runFanout({
|
|
162
|
+
providers,
|
|
163
|
+
task: 'abc',
|
|
164
|
+
agents: [{ use: 'p-a' }, { use: 'p-b' }],
|
|
165
|
+
integrate: { use: 'p-int', check: (j) => j.stdin.includes('【候選 1】') },
|
|
166
|
+
check: (j) => Array.isArray(j.args),
|
|
167
|
+
callOpt,
|
|
168
|
+
})
|
|
169
|
+
//integrate.check不通過時整合失敗, 但候選不受影響(判準不再互相牽制)
|
|
170
|
+
let t2 = await runFanout({
|
|
171
|
+
providers,
|
|
172
|
+
task: 'abc',
|
|
173
|
+
agents: [{ use: 'p-a' }, { use: 'p-b' }],
|
|
174
|
+
integrate: { use: 'p-int', check: () => false },
|
|
175
|
+
check: (j) => Array.isArray(j.args),
|
|
176
|
+
callOpt,
|
|
177
|
+
})
|
|
178
|
+
let r = [
|
|
179
|
+
[t1.ok, t1.integrated],
|
|
180
|
+
[t2.ok, t2.integrated, t2.candidates.length, t2.error.indexOf('integrate failed:') === 0],
|
|
181
|
+
]
|
|
182
|
+
let rr = [
|
|
183
|
+
[true, true],
|
|
184
|
+
[false, false, 2, true],
|
|
185
|
+
]
|
|
186
|
+
assert.strict.deepEqual(r, rr)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('名額規格可帶獨立check覆寫頂層(曾為被靜默覆蓋之死鍵)', async function() {
|
|
190
|
+
let t = await runFanout({
|
|
191
|
+
providers,
|
|
192
|
+
task: 'abc',
|
|
193
|
+
agents: [
|
|
194
|
+
{ use: 'p-a', check: () => false }, //此名額專屬判準: 必不過
|
|
195
|
+
{ use: 'p-b' }, //沿用頂層(未給即預設放行)
|
|
196
|
+
],
|
|
197
|
+
integrate: { use: 'p-int' },
|
|
198
|
+
callOpt,
|
|
199
|
+
})
|
|
200
|
+
let r = [t.ok, t.integrated, t.agents[0].ok, t.agents[1].ok, t.candidates.length]
|
|
201
|
+
let rr = [true, false, false, true, 1] //單稿放行(integrated:false)
|
|
202
|
+
assert.strict.deepEqual(r, rr)
|
|
203
|
+
})
|
|
204
|
+
|
|
159
205
|
it('defaultIntegratePrompt嵌入候選數與schema', function() {
|
|
160
206
|
let s = defaultIntegratePrompt([{ a: 1 }, { b: 2 }], { schema: '{"x":"..."}' })
|
|
161
207
|
let r = [s.includes('2 個獨立執行'), s.includes('{"x":"..."}'), s.includes('【候選 2】')]
|