@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/moa-runner.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bestEffort } from './best-effort.js'
|
|
1
2
|
/**
|
|
2
3
|
* DeepSeek Harness Mixture of Agents (MoA) — Runner Engine
|
|
3
4
|
* Parallel fan-out, Consilium Round 2 peer critique, aggregator synthesis & streaming.
|
|
@@ -17,229 +18,11 @@ export { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buil
|
|
|
17
18
|
export { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
|
|
18
19
|
export { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
|
|
19
20
|
|
|
20
|
-
export const DEFAULT_PROMPT = 'Please propose an optimal, well-structured, production-ready solution with full code and explanations.'
|
|
21
|
-
export const REFERENCE_SYSTEM_PROMPT = SYSTEM_ROLE_PROPOSER
|
|
22
|
-
|
|
23
21
|
/**
|
|
24
22
|
* Invokes LLM call with transient retry for recoverable network/rate-limit errors.
|
|
25
23
|
*/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
while (true) {
|
|
29
|
-
if (callArgs?.signal?.aborted) throw new Error('Aborted')
|
|
30
|
-
try {
|
|
31
|
-
return await callLlmFn(callArgs)
|
|
32
|
-
} catch (err) {
|
|
33
|
-
attempt++
|
|
34
|
-
const msg = err?.message || String(err)
|
|
35
|
-
const isTransient = /429|rate limit|502|503|504|econnreset|etimedout|socket hang up/i.test(msg)
|
|
36
|
-
if (attempt <= maxRetries && isTransient && !callArgs?.signal?.aborted) {
|
|
37
|
-
await new Promise((r) => setTimeout(r, retryDelayMs))
|
|
38
|
-
if (callArgs?.signal?.aborted) throw new Error('Aborted')
|
|
39
|
-
continue
|
|
40
|
-
}
|
|
41
|
-
throw err
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Dispatches queries to all reference models in parallel with transient retry and quorum straggler mitigation.
|
|
48
|
-
*/
|
|
49
|
-
export async function runReferencesParallel(references, messages, options = {}, callLlm, onProgress) {
|
|
50
|
-
if (!Array.isArray(references) || references.length === 0) {
|
|
51
|
-
return []
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const advisoryMessages = cleanAdvisoryMessages(messages)
|
|
55
|
-
const systemPrompt = options.systemPrompt || REFERENCE_SYSTEM_PROMPT
|
|
56
|
-
const timeoutMs = options.timeoutMs ?? ((options.timeoutSec ?? 60) * 1000)
|
|
57
|
-
const maxRetries = options.maxRetries ?? 0
|
|
58
|
-
const quorumEnabled = Boolean(options.quorumEnabled)
|
|
59
|
-
const gracePeriodMs = (options.gracePeriodSec ?? 10) * 1000
|
|
60
|
-
|
|
61
|
-
const total = references.length
|
|
62
|
-
let finishedCount = 0
|
|
63
|
-
const results = new Array(total)
|
|
64
|
-
const abortControllers = references.map(() => new AbortController())
|
|
65
|
-
if (options.signal) {
|
|
66
|
-
if (options.signal.aborted) {
|
|
67
|
-
return references.map((r, i) => ({
|
|
68
|
-
index: i + 1,
|
|
69
|
-
provider: r.provider,
|
|
70
|
-
model: r.model,
|
|
71
|
-
label: slotLabel(r),
|
|
72
|
-
ok: false,
|
|
73
|
-
text: '(aborted)',
|
|
74
|
-
error: 'Turn aborted',
|
|
75
|
-
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
76
|
-
costUsd: 0,
|
|
77
|
-
}))
|
|
78
|
-
}
|
|
79
|
-
if (typeof options.signal.addEventListener === 'function') {
|
|
80
|
-
options.signal.addEventListener('abort', () => {
|
|
81
|
-
for (const ac of abortControllers) {
|
|
82
|
-
try { ac.abort(new Error('Turn aborted')) } catch {}
|
|
83
|
-
}
|
|
84
|
-
}, { once: true })
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
let onTaskFinished = null
|
|
89
|
-
const notifyFinished = () => {
|
|
90
|
-
if (typeof onTaskFinished === 'function') onTaskFinished()
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (typeof onProgress === 'function') {
|
|
94
|
-
onProgress(`⚡ *Launching ${total} candidate models in parallel...*\n`)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
references.forEach((slot, i) => {
|
|
98
|
-
const label = slotLabel(slot)
|
|
99
|
-
const persona = slot?.role_persona && ROLE_PERSONA_PROMPTS[slot.role_persona]
|
|
100
|
-
? `\n\n${ROLE_PERSONA_PROMPTS[slot.role_persona]}`
|
|
101
|
-
: ''
|
|
102
|
-
const slotMessages = [{ role: 'system', content: systemPrompt + persona }, ...advisoryMessages]
|
|
103
|
-
|
|
104
|
-
const runOne = async () => {
|
|
105
|
-
try {
|
|
106
|
-
const callPromise = callWithTransientRetry(
|
|
107
|
-
callLlm,
|
|
108
|
-
{
|
|
109
|
-
provider: slot.provider,
|
|
110
|
-
model: slot.model,
|
|
111
|
-
messages: slotMessages,
|
|
112
|
-
temperature: options.temperature ?? 0.6,
|
|
113
|
-
maxTokens: options.maxTokens ?? 4096,
|
|
114
|
-
timeoutMs,
|
|
115
|
-
signal: abortControllers[i].signal,
|
|
116
|
-
},
|
|
117
|
-
maxRetries
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
121
|
-
const timer = setTimeout(() => reject(new Error(`Timeout after ${Math.round(timeoutMs / 1000)}s`)), timeoutMs)
|
|
122
|
-
timer.unref?.()
|
|
123
|
-
abortControllers[i].signal.addEventListener('abort', () => clearTimeout(timer), { once: true })
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
const res = await Promise.race([callPromise, timeoutPromise])
|
|
127
|
-
const text = typeof res === 'string' ? res : (res?.content || res?.text || '')
|
|
128
|
-
|
|
129
|
-
const fallbackUsage = (typeof res === 'object' && res?.usage) ? res.usage : {
|
|
130
|
-
inputTokens: Math.max(1, Math.round(slotMessages.map((m) => m.content).join('').length / 4)),
|
|
131
|
-
outputTokens: Math.max(1, Math.round(text.length / 4)),
|
|
132
|
-
}
|
|
133
|
-
const costInfo = estimateTokenCost(slot, fallbackUsage, options.prices)
|
|
134
|
-
|
|
135
|
-
finishedCount++
|
|
136
|
-
if (typeof onProgress === 'function') {
|
|
137
|
-
const costStr = costInfo.costUsd > 0 ? ` (~\$${costInfo.costUsd.toFixed(4)})` : ''
|
|
138
|
-
onProgress(`✅ *Candidate ${i + 1}/${total} (${label}) finished${costStr}*\n`)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
results[i] = {
|
|
142
|
-
index: i + 1,
|
|
143
|
-
slot,
|
|
144
|
-
label,
|
|
145
|
-
text,
|
|
146
|
-
usage: costInfo,
|
|
147
|
-
costUsd: costInfo.costUsd,
|
|
148
|
-
ok: true,
|
|
149
|
-
}
|
|
150
|
-
} catch (err) {
|
|
151
|
-
finishedCount++
|
|
152
|
-
const errMsg = err?.message || String(err)
|
|
153
|
-
console.warn(`[dsh-moa] Reference ${label} failed:`, errMsg)
|
|
154
|
-
if (typeof onProgress === 'function') {
|
|
155
|
-
onProgress(`⚠️ *Candidate ${i + 1}/${total} (${label}) error: ${errMsg}*\n`)
|
|
156
|
-
}
|
|
157
|
-
results[i] = {
|
|
158
|
-
index: i + 1,
|
|
159
|
-
slot,
|
|
160
|
-
label,
|
|
161
|
-
text: `[Model ${label} error: ${errMsg}]`,
|
|
162
|
-
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
163
|
-
costUsd: 0,
|
|
164
|
-
ok: false,
|
|
165
|
-
error: errMsg,
|
|
166
|
-
}
|
|
167
|
-
} finally {
|
|
168
|
-
notifyFinished()
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
runOne()
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
// Straggler mitigation via Quorum + Grace Period
|
|
176
|
-
if (quorumEnabled && total >= 3) {
|
|
177
|
-
const quorumTarget = Math.max(2, Math.min(total - 1, Math.ceil(total * 0.6)))
|
|
178
|
-
let graceTimer = null
|
|
179
|
-
|
|
180
|
-
await new Promise((resolve) => {
|
|
181
|
-
const checkQuorum = () => {
|
|
182
|
-
if (finishedCount >= total) {
|
|
183
|
-
if (graceTimer) clearTimeout(graceTimer)
|
|
184
|
-
resolve()
|
|
185
|
-
return
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (finishedCount >= quorumTarget && !graceTimer) {
|
|
189
|
-
if (typeof onProgress === 'function') {
|
|
190
|
-
onProgress(`⏳ *Quorum reached (${finishedCount}/${total}). Starting ${Math.round(gracePeriodMs / 1000)}s grace period for stragglers...*\n`)
|
|
191
|
-
}
|
|
192
|
-
graceTimer = setTimeout(() => {
|
|
193
|
-
if (typeof onProgress === 'function' && finishedCount < total) {
|
|
194
|
-
onProgress(`⏩ *Grace period expired. Proceeding with ${finishedCount}/${total} ready candidates.*\n`)
|
|
195
|
-
}
|
|
196
|
-
for (let k = 0; k < total; k++) {
|
|
197
|
-
if (!results[k]) {
|
|
198
|
-
try { abortControllers[k].abort(new Error('Quorum grace period timed out')) } catch {}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
resolve()
|
|
202
|
-
}, gracePeriodMs)
|
|
203
|
-
graceTimer.unref?.()
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
onTaskFinished = checkQuorum
|
|
208
|
-
checkQuorum()
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
for (let i = 0; i < total; i++) {
|
|
212
|
-
if (!results[i]) {
|
|
213
|
-
const slot = references[i]
|
|
214
|
-
const label = slotLabel(slot)
|
|
215
|
-
results[i] = {
|
|
216
|
-
index: i + 1,
|
|
217
|
-
slot,
|
|
218
|
-
label,
|
|
219
|
-
text: `[Model ${label} timed out (quorum grace period exceeded)]`,
|
|
220
|
-
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
221
|
-
costUsd: 0,
|
|
222
|
-
ok: false,
|
|
223
|
-
error: 'Quorum grace period timed out',
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return results
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Standard mode: wait for all tasks to complete
|
|
231
|
-
await new Promise((resolve) => {
|
|
232
|
-
const checkAll = () => {
|
|
233
|
-
if (finishedCount >= total) resolve()
|
|
234
|
-
}
|
|
235
|
-
onTaskFinished = checkAll
|
|
236
|
-
checkAll()
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
return results
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
24
|
+
import { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel } from './moa-candidates.js'
|
|
25
|
+
export { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel }
|
|
243
26
|
|
|
244
27
|
/**
|
|
245
28
|
* Executes the full Mixture of Agents pipeline.
|
|
@@ -293,11 +76,19 @@ export async function runMoAPipeline({
|
|
|
293
76
|
const collectedCtx = await collectProjectContext(cwd, 16000)
|
|
294
77
|
const isRefinement = Boolean(collectedCtx?.files?.length > 0 && isRefinementTask(userPrompt, collectedCtx.files))
|
|
295
78
|
let projectContext = ''
|
|
79
|
+
if (collectedCtx?.skippedFiles > 0 && typeof onProgress === 'function') {
|
|
80
|
+
const preview = collectedCtx.skippedList.slice(0, 3).join(', ')
|
|
81
|
+
const more = collectedCtx.skippedList.length > 3 ? ` (+${collectedCtx.skippedList.length - 3})` : ''
|
|
82
|
+
onProgress(`⚠️ *Workspace scan note: ${collectedCtx.skippedFiles} file(s) skipped (${preview}${more})*\n`)
|
|
83
|
+
}
|
|
296
84
|
if (isRefinement) {
|
|
297
85
|
if (typeof onProgress === 'function') {
|
|
298
86
|
onProgress('🔍 *Reading project files for refinement context...*\n')
|
|
299
87
|
}
|
|
300
|
-
projectContext = formatProjectContext(collectedCtx.files
|
|
88
|
+
projectContext = formatProjectContext(collectedCtx.files, {
|
|
89
|
+
skippedFiles: collectedCtx.skippedFiles,
|
|
90
|
+
skippedList: collectedCtx.skippedList,
|
|
91
|
+
})
|
|
301
92
|
}
|
|
302
93
|
|
|
303
94
|
// 3. Build prompts & evaluate broad questionnaire needs
|
|
@@ -355,6 +146,8 @@ export async function runMoAPipeline({
|
|
|
355
146
|
candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
|
|
356
147
|
aggregator: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
357
148
|
},
|
|
149
|
+
skippedFiles: collectedCtx?.skippedFiles || 0,
|
|
150
|
+
skippedList: collectedCtx?.skippedList || [],
|
|
358
151
|
durationMs: Date.now() - startTime,
|
|
359
152
|
}
|
|
360
153
|
}
|
|
@@ -421,7 +214,9 @@ export async function runMoAPipeline({
|
|
|
421
214
|
totalCostUsd,
|
|
422
215
|
durationMs,
|
|
423
216
|
}, historyFilePath || undefined)
|
|
424
|
-
} catch {
|
|
217
|
+
} catch (histErr) {
|
|
218
|
+
console.warn('[dsh-moa] Failed to record questionnaire run in history:', histErr?.message || histErr)
|
|
219
|
+
}
|
|
425
220
|
|
|
426
221
|
return {
|
|
427
222
|
kind: 'questions',
|
|
@@ -440,6 +235,8 @@ export async function runMoAPipeline({
|
|
|
440
235
|
candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
|
|
441
236
|
aggregator: qUsage,
|
|
442
237
|
},
|
|
238
|
+
skippedFiles: collectedCtx?.skippedFiles || 0,
|
|
239
|
+
skippedList: collectedCtx?.skippedList || [],
|
|
443
240
|
durationMs,
|
|
444
241
|
}
|
|
445
242
|
}
|
|
@@ -474,7 +271,9 @@ export async function runMoAPipeline({
|
|
|
474
271
|
totalCostUsd,
|
|
475
272
|
durationMs,
|
|
476
273
|
}, historyFilePath || undefined)
|
|
477
|
-
} catch {
|
|
274
|
+
} catch (histErr) {
|
|
275
|
+
console.warn('[dsh-moa] Failed to record fast synthesis run in history:', histErr?.message || histErr)
|
|
276
|
+
}
|
|
478
277
|
|
|
479
278
|
return {
|
|
480
279
|
kind: 'synthesis',
|
|
@@ -494,6 +293,8 @@ export async function runMoAPipeline({
|
|
|
494
293
|
candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
|
|
495
294
|
aggregator: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
496
295
|
},
|
|
296
|
+
skippedFiles: collectedCtx?.skippedFiles || 0,
|
|
297
|
+
skippedList: collectedCtx?.skippedList || [],
|
|
497
298
|
durationMs,
|
|
498
299
|
}
|
|
499
300
|
}
|
|
@@ -534,7 +335,9 @@ export async function runMoAPipeline({
|
|
|
534
335
|
const extraCost = estimateTokenCost(slot, res.usage, prices)
|
|
535
336
|
cand.costUsd = Number(((cand.costUsd || 0) + extraCost.costUsd).toFixed(5))
|
|
536
337
|
}
|
|
537
|
-
} catch {
|
|
338
|
+
} catch (r2CostErr) {
|
|
339
|
+
console.warn('[dsh-moa] Failed to estimate Round 2 cost:', r2CostErr?.message || r2CostErr)
|
|
340
|
+
}
|
|
538
341
|
})
|
|
539
342
|
await Promise.allSettled(r2Promises)
|
|
540
343
|
}
|
|
@@ -670,6 +473,8 @@ export async function runMoAPipeline({
|
|
|
670
473
|
allowCandidateOverride,
|
|
671
474
|
...(livePreview ? { liveCanvas: livePreview } : {}),
|
|
672
475
|
usage: usageSummary,
|
|
476
|
+
skippedFiles: collectedCtx?.skippedFiles || 0,
|
|
477
|
+
skippedList: collectedCtx?.skippedList || [],
|
|
673
478
|
durationMs,
|
|
674
479
|
}
|
|
675
480
|
}
|