@goodandready/dsh-moa 0.2.15 → 0.2.17
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 +7 -0
- package/lib/best-effort.js +38 -0
- package/lib/client.js +172 -4
- package/lib/file-workspace.js +71 -37
- package/lib/history.js +4 -4
- package/lib/index.js +18 -382
- package/lib/moa-candidates.js +225 -0
- package/lib/moa-parser.js +6 -0
- package/lib/moa-runner.js +29 -224
- package/lib/routes.js +412 -0
- package/lib/updater.js +282 -0
- package/package.json +1 -2
- package/docs/README.ru.md +0 -287
- package/docs/README.zh.md +0 -243
- package/docs/design/DESIGN.md +0 -79
- package/docs/plans/47-client-js-split-plan.md +0 -57
- package/docs/plans/55-resilience-plan.md +0 -33
- package/docs/plans/59-power-pack-plan.md +0 -73
- package/docs/plans/63-stability-polish-plan.md +0 -15
package/lib/index.js
CHANGED
|
@@ -19,17 +19,13 @@ import {
|
|
|
19
19
|
streamMoATurn,
|
|
20
20
|
} from './moa-runner.js'
|
|
21
21
|
|
|
22
|
-
import {
|
|
23
|
-
getMoaHistory,
|
|
24
|
-
getMoaLeaderboard,
|
|
25
|
-
getMoaRunById,
|
|
26
|
-
exportMoaHistory,
|
|
27
|
-
} from './history.js'
|
|
28
|
-
import { promoteCandidateWorkspace } from './file-workspace.js'
|
|
22
|
+
import { registerMoaRoutes } from './routes.js'
|
|
29
23
|
|
|
30
24
|
import { createLiveCanvasClient } from './live-canvas.js'
|
|
25
|
+
import { registerPluginUpdater, isSafeWriteRequest } from './updater.js'
|
|
31
26
|
|
|
32
|
-
export const name = 'dsh-moa'
|
|
27
|
+
export const name = '@goodandready/dsh-moa'
|
|
28
|
+
export { collectProjectContext, formatProjectContext } from './file-workspace.js'
|
|
33
29
|
export const inject = ['webServer', 'llm', 'settings', 'sessions', 'tools']
|
|
34
30
|
|
|
35
31
|
export const NS = 'dsh-moa'
|
|
@@ -117,28 +113,6 @@ export const Config = z.object({
|
|
|
117
113
|
]),
|
|
118
114
|
})
|
|
119
115
|
|
|
120
|
-
function writeJson(res, status, body) {
|
|
121
|
-
res.writeHead(status, {
|
|
122
|
-
'Content-Type': 'application/json; charset=utf-8',
|
|
123
|
-
'Cache-Control': 'no-store',
|
|
124
|
-
})
|
|
125
|
-
res.end(JSON.stringify(body))
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function readBody(req) {
|
|
129
|
-
return new Promise((resolve, reject) => {
|
|
130
|
-
let acc = ''
|
|
131
|
-
req.on('data', (chunk) => {
|
|
132
|
-
acc += chunk
|
|
133
|
-
if (acc.length > 5 * 1024 * 1024) {
|
|
134
|
-
req.destroy()
|
|
135
|
-
reject(new Error('Payload Too Large'))
|
|
136
|
-
}
|
|
137
|
-
})
|
|
138
|
-
req.on('end', () => resolve(acc))
|
|
139
|
-
req.on('error', reject)
|
|
140
|
-
})
|
|
141
|
-
}
|
|
142
116
|
|
|
143
117
|
export function apply(ctx, config) {
|
|
144
118
|
refreshCatalogInBackground()
|
|
@@ -238,361 +212,23 @@ export function apply(ctx, config) {
|
|
|
238
212
|
const liveCanvas = createLiveCanvasClient({ getPort: () => ctx.webServer?.port })
|
|
239
213
|
|
|
240
214
|
// Route: /dsh-moa/status
|
|
215
|
+
// Register one-click updater endpoint
|
|
241
216
|
ctx.effect(() => {
|
|
242
|
-
return ctx
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const cfg = live()
|
|
247
|
-
writeJson(res, 200, {
|
|
248
|
-
ok: true,
|
|
249
|
-
enabled: cfg.enabled !== false,
|
|
250
|
-
defaultPreset: cfg.default_preset || 'default',
|
|
251
|
-
presetsCount: (cfg.presets || []).length,
|
|
252
|
-
})
|
|
253
|
-
},
|
|
217
|
+
return registerPluginUpdater(ctx, {
|
|
218
|
+
endpoint: '/api/dsh-moa/update',
|
|
219
|
+
packageName: '@goodandready/dsh-moa',
|
|
220
|
+
manifestUrl: new URL('../package.json', import.meta.url),
|
|
254
221
|
})
|
|
255
|
-
}, 'dsh-moa:
|
|
256
|
-
|
|
257
|
-
// Route:
|
|
258
|
-
ctx
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const seen = new Set()
|
|
265
|
-
|
|
266
|
-
const add = (p, m) => {
|
|
267
|
-
if (!p || !m) return
|
|
268
|
-
const key = `${p}:${m}`
|
|
269
|
-
if (!seen.has(key)) {
|
|
270
|
-
seen.add(key)
|
|
271
|
-
result.push({ provider: p, model: m, label: key })
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (ctx.llm) {
|
|
276
|
-
try {
|
|
277
|
-
if (typeof ctx.llm.listAvailableModels === 'function') {
|
|
278
|
-
const list = await ctx.llm.listAvailableModels()
|
|
279
|
-
for (const item of (list || [])) {
|
|
280
|
-
if (item?.provider && item?.id) add(item.provider, item.id)
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
} catch (e) {
|
|
284
|
-
console.warn('[dsh-moa] ctx.llm.listAvailableModels error:', e)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
try {
|
|
288
|
-
if (typeof ctx.llm.listConfigurableProviders === 'function') {
|
|
289
|
-
const cProvs = ctx.llm.listConfigurableProviders() || []
|
|
290
|
-
for (const cp of cProvs) {
|
|
291
|
-
const cpId = cp.provider || cp.id
|
|
292
|
-
if (cpId && typeof ctx.llm.listModels === 'function') {
|
|
293
|
-
try {
|
|
294
|
-
const mList = await ctx.llm.listModels(cpId)
|
|
295
|
-
for (const m of (mList || [])) {
|
|
296
|
-
const mId = typeof m === 'string' ? m : (m.id || m.model || m.name)
|
|
297
|
-
if (mId) add(cpId, mId)
|
|
298
|
-
}
|
|
299
|
-
} catch {}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
} catch (e) {
|
|
304
|
-
console.warn('[dsh-moa] ctx.llm.listConfigurableProviders error:', e)
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Query settings for configured providers
|
|
309
|
-
try {
|
|
310
|
-
const s = ctx.get ? ctx.get('settings') : ctx.settings
|
|
311
|
-
const getSetting = (k) => {
|
|
312
|
-
try { return s?.get?.(k) } catch { return undefined }
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const def = getSetting('agent-default-model')
|
|
316
|
-
if (def?.provider && def?.model) {
|
|
317
|
-
add(def.provider, def.model)
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const piAi = getSetting('llm-pi-ai')
|
|
321
|
-
for (const [pName, pData] of Object.entries(piAi?.providers || {})) {
|
|
322
|
-
for (const m of (pData?.models || [])) {
|
|
323
|
-
const mId = typeof m === 'string' ? m : (m?.id || m?.model)
|
|
324
|
-
if (mId) add(pName, mId)
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const sync = getSetting('dsh-model-sync')
|
|
329
|
-
for (const [syncProv, syncModels] of Object.entries(sync?.modelCatalogs || {})) {
|
|
330
|
-
for (const m of (syncModels || [])) {
|
|
331
|
-
const mId = typeof m === 'string' ? m : (m?.id || m?.model)
|
|
332
|
-
if (mId) add(syncProv, mId)
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
} catch (e) {
|
|
336
|
-
console.warn('[dsh-moa] Error reading settings:', e)
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
writeJson(res, 200, { ok: true, models: result })
|
|
340
|
-
},
|
|
341
|
-
})
|
|
342
|
-
}, 'dsh-moa: models route')
|
|
343
|
-
|
|
344
|
-
// Route: /dsh-moa/presets
|
|
345
|
-
ctx.effect(() => {
|
|
346
|
-
return ctx.webServer.register({
|
|
347
|
-
kind: 'exact',
|
|
348
|
-
path: '/dsh-moa/presets',
|
|
349
|
-
handler: async (req, res) => {
|
|
350
|
-
const cfg = live()
|
|
351
|
-
if (req.method === 'GET') {
|
|
352
|
-
writeJson(res, 200, {
|
|
353
|
-
ok: true,
|
|
354
|
-
defaultPreset: cfg.default_preset || 'default',
|
|
355
|
-
presets: cfg.presets || [],
|
|
356
|
-
})
|
|
357
|
-
return
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (req.method === 'POST' || req.method === 'PUT') {
|
|
361
|
-
try {
|
|
362
|
-
const raw = await readBody(req)
|
|
363
|
-
const payload = JSON.parse(raw)
|
|
364
|
-
const newConfig = {
|
|
365
|
-
enabled: typeof payload.enabled === 'boolean' ? payload.enabled : cfg.enabled !== false,
|
|
366
|
-
default_preset: payload.default_preset || cfg.default_preset || 'default',
|
|
367
|
-
presets: Array.isArray(payload.presets) ? payload.presets : (cfg.presets || []),
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Validate the merged payload through the plugin schema before it
|
|
371
|
-
// reaches the settings storage: malformed payloads must be
|
|
372
|
-
// rejected with 400 instead of being persisted.
|
|
373
|
-
let validated
|
|
374
|
-
try {
|
|
375
|
-
validated = Config(structuredClone(newConfig))
|
|
376
|
-
} catch (verr) {
|
|
377
|
-
writeJson(res, 400, { ok: false, error: 'Invalid presets payload: ' + (verr?.message || String(verr)) })
|
|
378
|
-
return
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Persist via Cordis Settings API
|
|
382
|
-
if (settingsScope && typeof settingsScope.replace === 'function') {
|
|
383
|
-
await settingsScope.replace(validated)
|
|
384
|
-
} else {
|
|
385
|
-
// In-memory fallback if settings service is unavailable
|
|
386
|
-
getConfig = () => validated
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
writeJson(res, 200, {
|
|
390
|
-
ok: true,
|
|
391
|
-
enabled: validated.enabled !== false,
|
|
392
|
-
defaultPreset: validated.default_preset,
|
|
393
|
-
presets: validated.presets,
|
|
394
|
-
})
|
|
395
|
-
} catch (err) {
|
|
396
|
-
writeJson(res, 400, { ok: false, error: err?.message || String(err) })
|
|
397
|
-
}
|
|
398
|
-
return
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
writeJson(res, 405, { ok: false, error: 'Method Not Allowed' })
|
|
402
|
-
},
|
|
403
|
-
})
|
|
404
|
-
}, 'dsh-moa: presets route')
|
|
405
|
-
|
|
406
|
-
// Route: /dsh-moa/history (#9)
|
|
407
|
-
ctx.effect(() => {
|
|
408
|
-
return ctx.webServer.register({
|
|
409
|
-
kind: 'exact',
|
|
410
|
-
path: '/dsh-moa/history',
|
|
411
|
-
handler: async (req, res) => {
|
|
412
|
-
if (req.method !== 'GET') {
|
|
413
|
-
writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
414
|
-
return
|
|
415
|
-
}
|
|
416
|
-
try {
|
|
417
|
-
const u = new URL(req.url, 'http://localhost')
|
|
418
|
-
const limit = parseInt(u.searchParams.get('limit') || '20', 10)
|
|
419
|
-
const offset = parseInt(u.searchParams.get('offset') || '0', 10)
|
|
420
|
-
const data = getMoaHistory(limit, offset)
|
|
421
|
-
writeJson(res, 200, { ok: true, ...data })
|
|
422
|
-
} catch (err) {
|
|
423
|
-
writeJson(res, 500, { ok: false, error: err?.message || String(err) })
|
|
424
|
-
}
|
|
425
|
-
},
|
|
426
|
-
})
|
|
427
|
-
}, 'dsh-moa: history route')
|
|
428
|
-
|
|
429
|
-
// Route: /dsh-moa/leaderboard
|
|
430
|
-
ctx.effect(() => {
|
|
431
|
-
return ctx.webServer.register({
|
|
432
|
-
kind: 'exact',
|
|
433
|
-
path: '/dsh-moa/leaderboard',
|
|
434
|
-
handler: async (req, res) => {
|
|
435
|
-
if (req.method !== 'GET') {
|
|
436
|
-
writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
437
|
-
return
|
|
438
|
-
}
|
|
439
|
-
try {
|
|
440
|
-
const url = new URL(req.url || '', 'http://127.0.0.1')
|
|
441
|
-
const preset = url.searchParams.get('preset') || null
|
|
442
|
-
const format = url.searchParams.get('format') || 'json'
|
|
443
|
-
|
|
444
|
-
if (format === 'csv') {
|
|
445
|
-
const csv = exportMoaHistory(undefined, { format: 'csv', preset })
|
|
446
|
-
res.writeHead(200, {
|
|
447
|
-
'Content-Type': 'text/csv; charset=utf-8',
|
|
448
|
-
'Content-Disposition': 'attachment; filename="moa-leaderboard.csv"',
|
|
449
|
-
})
|
|
450
|
-
res.end(csv)
|
|
451
|
-
return
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const data = getMoaLeaderboard(undefined, { preset })
|
|
455
|
-
writeJson(res, 200, { ok: true, ...data })
|
|
456
|
-
} catch (err) {
|
|
457
|
-
writeJson(res, 500, { ok: false, error: err?.message || String(err) })
|
|
458
|
-
}
|
|
459
|
-
},
|
|
460
|
-
})
|
|
461
|
-
}, 'dsh-moa: leaderboard route')
|
|
462
|
-
|
|
463
|
-
// Route: /dsh-moa/promote (Candidate Override Action)
|
|
464
|
-
ctx.effect(() => {
|
|
465
|
-
return ctx.webServer.register({
|
|
466
|
-
kind: 'exact',
|
|
467
|
-
path: '/dsh-moa/promote',
|
|
468
|
-
handler: async (req, res) => {
|
|
469
|
-
if (req.method !== 'POST') {
|
|
470
|
-
writeJson(res, 405, { ok: false, error: 'POST required' })
|
|
471
|
-
return
|
|
472
|
-
}
|
|
473
|
-
try {
|
|
474
|
-
const raw = await readBody(req)
|
|
475
|
-
const body = JSON.parse(raw || '{}')
|
|
476
|
-
const candidateIndex = parseInt(body?.candidateIndex, 10)
|
|
477
|
-
if (isNaN(candidateIndex) || candidateIndex < 1) {
|
|
478
|
-
writeJson(res, 400, { ok: false, error: 'Valid candidateIndex required (>= 1)' })
|
|
479
|
-
return
|
|
480
|
-
}
|
|
481
|
-
const cwd = body?.cwd || process.cwd()
|
|
482
|
-
const promotedFiles = await promoteCandidateWorkspace(cwd, candidateIndex, { keepMoa: true })
|
|
483
|
-
writeJson(res, 200, { ok: true, candidateIndex, promotedFiles })
|
|
484
|
-
} catch (err) {
|
|
485
|
-
writeJson(res, 500, { ok: false, error: err?.message || String(err) })
|
|
486
|
-
}
|
|
487
|
-
},
|
|
488
|
-
})
|
|
489
|
-
}, 'dsh-moa: promote candidate route')
|
|
490
|
-
|
|
491
|
-
// Route: /dsh-moa/history/export
|
|
492
|
-
ctx.effect(() => {
|
|
493
|
-
return ctx.webServer.register({
|
|
494
|
-
kind: 'exact',
|
|
495
|
-
path: '/dsh-moa/history/export',
|
|
496
|
-
handler: async (req, res) => {
|
|
497
|
-
if (req.method !== 'GET') {
|
|
498
|
-
writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
499
|
-
return
|
|
500
|
-
}
|
|
501
|
-
try {
|
|
502
|
-
const url = new URL(req.url || '', 'http://127.0.0.1')
|
|
503
|
-
const preset = url.searchParams.get('preset') || null
|
|
504
|
-
const format = url.searchParams.get('format') || 'json'
|
|
505
|
-
const data = exportMoaHistory(undefined, { format, preset })
|
|
506
|
-
|
|
507
|
-
if (format === 'csv') {
|
|
508
|
-
res.writeHead(200, {
|
|
509
|
-
'Content-Type': 'text/csv; charset=utf-8',
|
|
510
|
-
'Content-Disposition': 'attachment; filename="moa-history.csv"',
|
|
511
|
-
})
|
|
512
|
-
res.end(data)
|
|
513
|
-
return
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
res.writeHead(200, {
|
|
517
|
-
'Content-Type': 'application/json; charset=utf-8',
|
|
518
|
-
'Content-Disposition': 'attachment; filename="moa-history.json"',
|
|
519
|
-
})
|
|
520
|
-
res.end(data)
|
|
521
|
-
} catch (err) {
|
|
522
|
-
writeJson(res, 500, { ok: false, error: err?.message || String(err) })
|
|
523
|
-
}
|
|
524
|
-
},
|
|
525
|
-
})
|
|
526
|
-
}, 'dsh-moa: history export route')
|
|
527
|
-
|
|
528
|
-
// Route: /dsh-moa/runs/<id> (run replay by id)
|
|
529
|
-
ctx.effect(() => {
|
|
530
|
-
return ctx.webServer.register({
|
|
531
|
-
kind: 'prefix',
|
|
532
|
-
path: '/dsh-moa/runs/',
|
|
533
|
-
handler: (req, res) => {
|
|
534
|
-
if (req.method !== 'GET') {
|
|
535
|
-
writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
536
|
-
return
|
|
537
|
-
}
|
|
538
|
-
const urlPath = req.url.split('?')[0]
|
|
539
|
-
const parts = urlPath.split('/').filter(Boolean)
|
|
540
|
-
const runId = parts[2] || ''
|
|
541
|
-
const run = runId ? getMoaRunById(runId) : null
|
|
542
|
-
if (!run) {
|
|
543
|
-
writeJson(res, 404, { ok: false, error: 'Run not found' })
|
|
544
|
-
return
|
|
545
|
-
}
|
|
546
|
-
writeJson(res, 200, { ok: true, run })
|
|
547
|
-
},
|
|
548
|
-
})
|
|
549
|
-
}, 'dsh-moa: run by id route')
|
|
550
|
-
|
|
551
|
-
// Route: /dsh-moa/run
|
|
552
|
-
ctx.effect(() => {
|
|
553
|
-
return ctx.webServer.register({
|
|
554
|
-
kind: 'exact',
|
|
555
|
-
path: '/dsh-moa/run',
|
|
556
|
-
handler: async (req, res) => {
|
|
557
|
-
if (req.method !== 'POST') {
|
|
558
|
-
writeJson(res, 405, { ok: false, error: 'POST required' })
|
|
559
|
-
return
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
try {
|
|
563
|
-
const raw = await readBody(req)
|
|
564
|
-
const payload = JSON.parse(raw)
|
|
565
|
-
const prompt = payload.prompt || ''
|
|
566
|
-
if (!prompt.trim()) {
|
|
567
|
-
writeJson(res, 400, { ok: false, error: 'Empty prompt' })
|
|
568
|
-
return
|
|
569
|
-
}
|
|
222
|
+
}, 'dsh-moa: plugin updater route')
|
|
223
|
+
|
|
224
|
+
// Route dispatch: req.method verified fail-closed in lib/routes.js
|
|
225
|
+
registerMoaRoutes(ctx, {
|
|
226
|
+
live,
|
|
227
|
+
callLlm,
|
|
228
|
+
liveCanvas,
|
|
229
|
+
getSettingsScope: () => settingsScope,
|
|
230
|
+
})
|
|
570
231
|
|
|
571
|
-
const cfg = live()
|
|
572
|
-
if (cfg.enabled === false) {
|
|
573
|
-
writeJson(res, 400, { ok: false, error: 'MoA is disabled in settings' })
|
|
574
|
-
return
|
|
575
|
-
}
|
|
576
|
-
const presetName = payload.preset || cfg.default_preset || 'default'
|
|
577
|
-
const preset = (cfg.presets || []).find((p) => p.name === presetName) || cfg.presets?.[0]
|
|
578
|
-
|
|
579
|
-
const result = await runMoAPipeline({
|
|
580
|
-
userPrompt: prompt,
|
|
581
|
-
messages: payload.messages || [],
|
|
582
|
-
preset,
|
|
583
|
-
callLlm,
|
|
584
|
-
cwd: payload.cwd || process.cwd(),
|
|
585
|
-
prices: cfg.prices || {},
|
|
586
|
-
liveCanvas,
|
|
587
|
-
})
|
|
588
|
-
|
|
589
|
-
writeJson(res, 200, { ok: true, ...result })
|
|
590
|
-
} catch (err) {
|
|
591
|
-
writeJson(res, 500, { ok: false, error: err?.message || String(err) })
|
|
592
|
-
}
|
|
593
|
-
},
|
|
594
|
-
})
|
|
595
|
-
}, 'dsh-moa: run pipeline route')
|
|
596
232
|
// Session turn interceptor: route turn to real aggregator model & stream MoA synthesis
|
|
597
233
|
const moaPendingTurns = new WeakMap()
|
|
598
234
|
const moaSessionTurns = new Map()
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek Harness Mixture of Agents (MoA) — Candidate Fan-Out & Retry
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { bestEffort } from './best-effort.js'
|
|
6
|
+
import { slotLabel, cleanAdvisoryMessages, ROLE_PERSONA_PROMPTS, SYSTEM_ROLE_PROPOSER } from './moa-prompts.js'
|
|
7
|
+
import { estimateTokenCost } from './pricing.js'
|
|
8
|
+
|
|
9
|
+
export const REFERENCE_SYSTEM_PROMPT = SYSTEM_ROLE_PROPOSER
|
|
10
|
+
|
|
11
|
+
export async function callWithTransientRetry(callLlmFn, callArgs, maxRetries = 0, retryDelayMs = 1200) {
|
|
12
|
+
let attempt = 0
|
|
13
|
+
while (true) {
|
|
14
|
+
if (callArgs?.signal?.aborted) throw new Error('Aborted')
|
|
15
|
+
try {
|
|
16
|
+
return await callLlmFn(callArgs)
|
|
17
|
+
} catch (err) {
|
|
18
|
+
attempt++
|
|
19
|
+
const msg = err?.message || String(err)
|
|
20
|
+
const isTransient = /429|rate limit|502|503|504|econnreset|etimedout|socket hang up/i.test(msg)
|
|
21
|
+
if (attempt <= maxRetries && isTransient && !callArgs?.signal?.aborted) {
|
|
22
|
+
await new Promise((r) => setTimeout(r, retryDelayMs))
|
|
23
|
+
if (callArgs?.signal?.aborted) throw new Error('Aborted')
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
throw err
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Dispatches queries to all reference models in parallel with transient retry and quorum straggler mitigation.
|
|
33
|
+
*/
|
|
34
|
+
export async function runReferencesParallel(references, messages, options = {}, callLlm, onProgress) {
|
|
35
|
+
if (!Array.isArray(references) || references.length === 0) {
|
|
36
|
+
return []
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const advisoryMessages = cleanAdvisoryMessages(messages)
|
|
40
|
+
const systemPrompt = options.systemPrompt || REFERENCE_SYSTEM_PROMPT
|
|
41
|
+
const timeoutMs = options.timeoutMs ?? ((options.timeoutSec ?? 60) * 1000)
|
|
42
|
+
const maxRetries = options.maxRetries ?? 0
|
|
43
|
+
const quorumEnabled = Boolean(options.quorumEnabled)
|
|
44
|
+
const gracePeriodMs = (options.gracePeriodSec ?? 10) * 1000
|
|
45
|
+
|
|
46
|
+
const total = references.length
|
|
47
|
+
let finishedCount = 0
|
|
48
|
+
const results = new Array(total)
|
|
49
|
+
const abortControllers = references.map(() => new AbortController())
|
|
50
|
+
if (options.signal) {
|
|
51
|
+
if (options.signal.aborted) {
|
|
52
|
+
return references.map((r, i) => ({
|
|
53
|
+
index: i + 1,
|
|
54
|
+
provider: r.provider,
|
|
55
|
+
model: r.model,
|
|
56
|
+
label: slotLabel(r),
|
|
57
|
+
ok: false,
|
|
58
|
+
text: '(aborted)',
|
|
59
|
+
error: 'Turn aborted',
|
|
60
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
61
|
+
costUsd: 0,
|
|
62
|
+
}))
|
|
63
|
+
}
|
|
64
|
+
if (typeof options.signal.addEventListener === 'function') {
|
|
65
|
+
options.signal.addEventListener('abort', () => {
|
|
66
|
+
for (const ac of abortControllers) {
|
|
67
|
+
bestEffort('turn abort signal', () => ac.abort(new Error('Turn aborted')))
|
|
68
|
+
}
|
|
69
|
+
}, { once: true })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let onTaskFinished = null
|
|
74
|
+
const notifyFinished = () => {
|
|
75
|
+
if (typeof onTaskFinished === 'function') onTaskFinished()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (typeof onProgress === 'function') {
|
|
79
|
+
onProgress(`⚡ *Launching ${total} candidate models in parallel...*\n`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
references.forEach((slot, i) => {
|
|
83
|
+
const label = slotLabel(slot)
|
|
84
|
+
const persona = slot?.role_persona && ROLE_PERSONA_PROMPTS[slot.role_persona]
|
|
85
|
+
? `\n\n${ROLE_PERSONA_PROMPTS[slot.role_persona]}`
|
|
86
|
+
: ''
|
|
87
|
+
const slotMessages = [{ role: 'system', content: systemPrompt + persona }, ...advisoryMessages]
|
|
88
|
+
|
|
89
|
+
const runOne = async () => {
|
|
90
|
+
try {
|
|
91
|
+
const callPromise = callWithTransientRetry(
|
|
92
|
+
callLlm,
|
|
93
|
+
{
|
|
94
|
+
provider: slot.provider,
|
|
95
|
+
model: slot.model,
|
|
96
|
+
messages: slotMessages,
|
|
97
|
+
temperature: options.temperature ?? 0.6,
|
|
98
|
+
maxTokens: options.maxTokens ?? 4096,
|
|
99
|
+
timeoutMs,
|
|
100
|
+
signal: abortControllers[i].signal,
|
|
101
|
+
},
|
|
102
|
+
maxRetries
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
106
|
+
const timer = setTimeout(() => reject(new Error(`Timeout after ${Math.round(timeoutMs / 1000)}s`)), timeoutMs)
|
|
107
|
+
timer.unref?.()
|
|
108
|
+
abortControllers[i].signal.addEventListener('abort', () => clearTimeout(timer), { once: true })
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const res = await Promise.race([callPromise, timeoutPromise])
|
|
112
|
+
const text = typeof res === 'string' ? res : (res?.content || res?.text || '')
|
|
113
|
+
|
|
114
|
+
const fallbackUsage = (typeof res === 'object' && res?.usage) ? res.usage : {
|
|
115
|
+
inputTokens: Math.max(1, Math.round(slotMessages.map((m) => m.content).join('').length / 4)),
|
|
116
|
+
outputTokens: Math.max(1, Math.round(text.length / 4)),
|
|
117
|
+
}
|
|
118
|
+
const costInfo = estimateTokenCost(slot, fallbackUsage, options.prices)
|
|
119
|
+
|
|
120
|
+
finishedCount++
|
|
121
|
+
if (typeof onProgress === 'function') {
|
|
122
|
+
const costStr = costInfo.costUsd > 0 ? ` (~\$${costInfo.costUsd.toFixed(4)})` : ''
|
|
123
|
+
onProgress(`✅ *Candidate ${i + 1}/${total} (${label}) finished${costStr}*\n`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
results[i] = {
|
|
127
|
+
index: i + 1,
|
|
128
|
+
slot,
|
|
129
|
+
label,
|
|
130
|
+
text,
|
|
131
|
+
usage: costInfo,
|
|
132
|
+
costUsd: costInfo.costUsd,
|
|
133
|
+
ok: true,
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
finishedCount++
|
|
137
|
+
const errMsg = err?.message || String(err)
|
|
138
|
+
console.warn(`[dsh-moa] Reference ${label} failed:`, errMsg)
|
|
139
|
+
if (typeof onProgress === 'function') {
|
|
140
|
+
onProgress(`⚠️ *Candidate ${i + 1}/${total} (${label}) error: ${errMsg}*\n`)
|
|
141
|
+
}
|
|
142
|
+
results[i] = {
|
|
143
|
+
index: i + 1,
|
|
144
|
+
slot,
|
|
145
|
+
label,
|
|
146
|
+
text: `[Model ${label} error: ${errMsg}]`,
|
|
147
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
148
|
+
costUsd: 0,
|
|
149
|
+
ok: false,
|
|
150
|
+
error: errMsg,
|
|
151
|
+
}
|
|
152
|
+
} finally {
|
|
153
|
+
notifyFinished()
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
runOne()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// Straggler mitigation via Quorum + Grace Period
|
|
161
|
+
if (quorumEnabled && total >= 3) {
|
|
162
|
+
const quorumTarget = Math.max(2, Math.min(total - 1, Math.ceil(total * 0.6)))
|
|
163
|
+
let graceTimer = null
|
|
164
|
+
|
|
165
|
+
await new Promise((resolve) => {
|
|
166
|
+
const checkQuorum = () => {
|
|
167
|
+
if (finishedCount >= total) {
|
|
168
|
+
if (graceTimer) clearTimeout(graceTimer)
|
|
169
|
+
resolve()
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (finishedCount >= quorumTarget && !graceTimer) {
|
|
174
|
+
if (typeof onProgress === 'function') {
|
|
175
|
+
onProgress(`⏳ *Quorum reached (${finishedCount}/${total}). Starting ${Math.round(gracePeriodMs / 1000)}s grace period for stragglers...*\n`)
|
|
176
|
+
}
|
|
177
|
+
graceTimer = setTimeout(() => {
|
|
178
|
+
if (typeof onProgress === 'function' && finishedCount < total) {
|
|
179
|
+
onProgress(`⏩ *Grace period expired. Proceeding with ${finishedCount}/${total} ready candidates.*\n`)
|
|
180
|
+
}
|
|
181
|
+
for (let k = 0; k < total; k++) {
|
|
182
|
+
if (!results[k]) {
|
|
183
|
+
bestEffort('quorum grace period abort', () => abortControllers[k].abort(new Error('Quorum grace period timed out')))
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
resolve()
|
|
187
|
+
}, gracePeriodMs)
|
|
188
|
+
graceTimer.unref?.()
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
onTaskFinished = checkQuorum
|
|
193
|
+
checkQuorum()
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
for (let i = 0; i < total; i++) {
|
|
197
|
+
if (!results[i]) {
|
|
198
|
+
const slot = references[i]
|
|
199
|
+
const label = slotLabel(slot)
|
|
200
|
+
results[i] = {
|
|
201
|
+
index: i + 1,
|
|
202
|
+
slot,
|
|
203
|
+
label,
|
|
204
|
+
text: `[Model ${label} timed out (quorum grace period exceeded)]`,
|
|
205
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
206
|
+
costUsd: 0,
|
|
207
|
+
ok: false,
|
|
208
|
+
error: 'Quorum grace period timed out',
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return results
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Standard mode: wait for all tasks to complete
|
|
216
|
+
await new Promise((resolve) => {
|
|
217
|
+
const checkAll = () => {
|
|
218
|
+
if (finishedCount >= total) resolve()
|
|
219
|
+
}
|
|
220
|
+
onTaskFinished = checkAll
|
|
221
|
+
checkAll()
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
return results
|
|
225
|
+
}
|
package/lib/moa-parser.js
CHANGED
|
@@ -138,6 +138,12 @@ export function formatMoAResponse({ moaResult, presetName }) {
|
|
|
138
138
|
parts.push('> 🔄 **Mode**: Iterative project refinement (Refinement)')
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
if (moaResult?.skippedFiles > 0) {
|
|
142
|
+
const preview = moaResult.skippedList?.slice(0, 3)?.join(', ') || ''
|
|
143
|
+
const more = (moaResult.skippedList?.length || 0) > 3 ? ` and ${(moaResult.skippedList?.length || 0) - 3} more` : ''
|
|
144
|
+
parts.push(`> ⚠️ **Workspace Notice**: ${moaResult.skippedFiles} file(s) skipped during workspace scan (${preview}${more}). Context representation may be partial.`)
|
|
145
|
+
}
|
|
146
|
+
|
|
141
147
|
const hasPromoted = moaResult?.promotedFiles && moaResult.promotedFiles.length > 0
|
|
142
148
|
if (hasPromoted) {
|
|
143
149
|
parts.push(`> 📦 **Files created in the project**: \`${moaResult.promotedFiles.join('`, `')}\``)
|