@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/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
- export async function callWithTransientRetry(callLlmFn, callArgs, maxRetries = 0, retryDelayMs = 1200) {
27
- let attempt = 0
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
  }