@goodandready/dsh-moa 0.2.14 → 0.2.16

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.
@@ -191,8 +191,11 @@ export function buildPeerCritiquePrompt(userPrompt, myProposal, otherProposals =
191
191
  const othersText = otherProposals
192
192
  .map((p, idx) => {
193
193
  const label = isBlind ? `Candidate ${idx + 1}` : (p.label || `Candidate ${idx + 1}`)
194
- return `### ${label} Alternative Proposal:
195
- ${p.text}`
194
+ let text = p.text || ''
195
+ if (text.length > 3000) {
196
+ text = stripOrSummarizeCode(text)
197
+ }
198
+ return `### ${label} Alternative Proposal:\n${text}`
196
199
  })
197
200
  .join('\n\n---\n\n')
198
201
 
@@ -224,7 +227,7 @@ export function buildCuratorSynthesisPrompt(userPrompt, referenceOutputs = [], j
224
227
  ? ` [Files created: ${r.files.map((f) => f.relativePath).join(', ')}]`
225
228
  : ''
226
229
  let textContent = r.text
227
- if (referenceOutputs.length >= 3 && textContent.length > 3000) {
230
+ if (textContent.length > 3000) {
228
231
  textContent = stripOrSummarizeCode(textContent)
229
232
  }
230
233
  const syntaxNote = r.syntaxWarning ? ` [⚠️ Syntax Warning: ${r.syntaxWarning}]` : ''
@@ -291,7 +294,7 @@ export function buildSynthesisPrompt(userPrompt, referenceOutputs = [], judgeCri
291
294
  ? ` [Files created: ${r.files.map((f) => f.relativePath).join(', ')}]`
292
295
  : ''
293
296
  let textContent = r.text
294
- if (referenceOutputs.length >= 3 && textContent.length > 3000) {
297
+ if (textContent.length > 3000) {
295
298
  textContent = stripOrSummarizeCode(textContent)
296
299
  }
297
300
  const syntaxNote = r.syntaxWarning ? ` [⚠️ Syntax Warning: ${r.syntaxWarning}]` : ''
package/lib/moa-runner.js CHANGED
@@ -1,255 +1,30 @@
1
+ import { bestEffort } from './best-effort.js'
1
2
  /**
2
3
  * DeepSeek Harness Mixture of Agents (MoA) — Runner Engine
3
- *
4
- * Implements the full ensemble pipeline:
5
- * - Parallel fan-out to reference models with transient retry & quorum straggler mitigation
6
- * - Prompt caching aligned message structures
7
- * - Curator synthesis & antipatterns evaluation
8
- * - Aggregator fallback chain for resilience & malformed output recovery
9
- * - Live token streaming for aggregator
10
- * - File promotion & Live Canvas sandbox preview
4
+ * Parallel fan-out, Consilium Round 2 peer critique, aggregator synthesis & streaming.
11
5
  */
12
6
 
13
7
  import path from 'node:path'
14
8
  import crypto from 'node:crypto'
15
- import { extractFileBlocks, collectProjectContext, isRefinementTask, writeCandidateWorkspace, promoteCandidateWorkspace, cleanMoaWorkspaces, verifyFileSyntax } from './file-workspace.js'
16
- import { estimateTokenCost } from './pricing.js'
17
- import { recordMoaRun, recordMoaRunAsync } from './history.js'
9
+ import { extractFileBlocks, collectProjectContext, formatProjectContext, isRefinementTask, writeCandidateWorkspace, promoteCandidateWorkspace, cleanMoaWorkspaces, verifyFileSyntax } from './file-workspace.js'
10
+ import { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
11
+ import { recordMoaRun, recordMoaRunAsync, candidatesForHistory } from './history.js'
12
+ import { createPromotedPreview } from './live-canvas.js'
18
13
  import { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildCuratorSynthesisPrompt, buildSynthesisPrompt, buildPeerCritiquePrompt, ROLE_PERSONA_PROMPTS, SYSTEM_ROLE_PROPOSER, ANTIPATTERNS_RUBRIC } from './moa-prompts.js'
19
14
  import { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
20
15
 
21
16
  // Re-exports for consumers & backward compatibility
22
17
  export { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildCuratorSynthesisPrompt, buildSynthesisPrompt, ANTIPATTERNS_RUBRIC } from './moa-prompts.js'
23
18
  export { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
24
- export { estimateTokenCost } from './pricing.js'
19
+ export { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
25
20
 
26
- export const DEFAULT_PROMPT = 'Please propose an optimal, well-structured, production-ready solution with full code and explanations.'
27
- export const REFERENCE_SYSTEM_PROMPT = SYSTEM_ROLE_PROPOSER
21
+ const DEFAULT_PROMPT = 'Please propose an optimal, well-structured, production-ready solution with full code and explanations.'
28
22
 
29
23
  /**
30
24
  * Invokes LLM call with transient retry for recoverable network/rate-limit errors.
31
25
  */
32
- export async function callWithTransientRetry(callLlmFn, callArgs, maxRetries = 0, retryDelayMs = 1200) {
33
- let attempt = 0
34
- while (true) {
35
- try {
36
- return await callLlmFn(callArgs)
37
- } catch (err) {
38
- attempt++
39
- const msg = err?.message || String(err)
40
- const isTransient = /429|rate limit|502|503|504|econnreset|etimedout|socket hang up/i.test(msg)
41
- if (attempt <= maxRetries && isTransient) {
42
- await new Promise((r) => setTimeout(r, retryDelayMs))
43
- continue
44
- }
45
- throw err
46
- }
47
- }
48
- }
49
-
50
- /**
51
- * Dispatches queries to all reference models in parallel with transient retry and quorum straggler mitigation.
52
- */
53
- export async function runReferencesParallel(references, messages, options = {}, callLlm, onProgress) {
54
- if (!Array.isArray(references) || references.length === 0) {
55
- return []
56
- }
57
-
58
- const advisoryMessages = cleanAdvisoryMessages(messages)
59
- const systemPrompt = options.systemPrompt || REFERENCE_SYSTEM_PROMPT
60
- const timeoutMs = options.timeoutMs ?? ((options.timeoutSec ?? 60) * 1000)
61
- const maxRetries = options.maxRetries ?? 0
62
- const quorumEnabled = Boolean(options.quorumEnabled)
63
- const gracePeriodMs = (options.gracePeriodSec ?? 10) * 1000
64
-
65
- const total = references.length
66
- let finishedCount = 0
67
- const results = new Array(total)
68
- const abortControllers = references.map(() => new AbortController())
69
-
70
- let onTaskFinished = null
71
- const notifyFinished = () => {
72
- if (typeof onTaskFinished === 'function') onTaskFinished()
73
- }
74
-
75
- if (typeof onProgress === 'function') {
76
- onProgress(`⚡ *Launching ${total} candidate models in parallel...*\n`)
77
- }
78
-
79
- references.forEach((slot, i) => {
80
- const label = slotLabel(slot)
81
- const persona = slot?.role_persona && ROLE_PERSONA_PROMPTS[slot.role_persona]
82
- ? `\n\n${ROLE_PERSONA_PROMPTS[slot.role_persona]}`
83
- : ''
84
- const slotMessages = [{ role: 'system', content: systemPrompt + persona }, ...advisoryMessages]
85
-
86
- const runOne = async () => {
87
- try {
88
- const callPromise = callWithTransientRetry(
89
- callLlm,
90
- {
91
- provider: slot.provider,
92
- model: slot.model,
93
- messages: slotMessages,
94
- temperature: options.temperature ?? 0.6,
95
- maxTokens: options.maxTokens ?? 4096,
96
- timeoutMs,
97
- signal: abortControllers[i].signal,
98
- },
99
- maxRetries
100
- )
101
-
102
- const timeoutPromise = new Promise((_, reject) => {
103
- const timer = setTimeout(() => reject(new Error(`Timeout after ${Math.round(timeoutMs / 1000)}s`)), timeoutMs)
104
- timer.unref?.()
105
- abortControllers[i].signal.addEventListener('abort', () => clearTimeout(timer), { once: true })
106
- })
107
-
108
- const res = await Promise.race([callPromise, timeoutPromise])
109
- const text = typeof res === 'string' ? res : (res?.content || res?.text || '')
110
-
111
- const fallbackUsage = (typeof res === 'object' && res?.usage) ? res.usage : {
112
- inputTokens: Math.max(1, Math.round(slotMessages.map((m) => m.content).join('').length / 4)),
113
- outputTokens: Math.max(1, Math.round(text.length / 4)),
114
- }
115
- const costInfo = estimateTokenCost(slot, fallbackUsage, options.prices)
116
-
117
- finishedCount++
118
- if (typeof onProgress === 'function') {
119
- const costStr = costInfo.costUsd > 0 ? ` (~\$${costInfo.costUsd.toFixed(4)})` : ''
120
- onProgress(`✅ *Candidate ${i + 1}/${total} (${label}) finished${costStr}*\n`)
121
- }
122
-
123
- results[i] = {
124
- index: i + 1,
125
- slot,
126
- label,
127
- text,
128
- usage: costInfo,
129
- costUsd: costInfo.costUsd,
130
- ok: true,
131
- }
132
- } catch (err) {
133
- finishedCount++
134
- const errMsg = err?.message || String(err)
135
- console.warn(`[dsh-moa] Reference ${label} failed:`, errMsg)
136
- if (typeof onProgress === 'function') {
137
- onProgress(`⚠️ *Candidate ${i + 1}/${total} (${label}) error: ${errMsg}*\n`)
138
- }
139
- results[i] = {
140
- index: i + 1,
141
- slot,
142
- label,
143
- text: `[Model ${label} error: ${errMsg}]`,
144
- usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
145
- costUsd: 0,
146
- ok: false,
147
- error: errMsg,
148
- }
149
- } finally {
150
- notifyFinished()
151
- }
152
- }
153
-
154
- runOne()
155
- })
156
-
157
- // Straggler mitigation via Quorum + Grace Period
158
- if (quorumEnabled && total >= 3) {
159
- const quorumTarget = Math.max(2, Math.min(total - 1, Math.ceil(total * 0.6)))
160
- let graceTimer = null
161
-
162
- await new Promise((resolve) => {
163
- const checkQuorum = () => {
164
- if (finishedCount >= total) {
165
- if (graceTimer) clearTimeout(graceTimer)
166
- resolve()
167
- return
168
- }
169
-
170
- if (finishedCount >= quorumTarget && !graceTimer) {
171
- if (typeof onProgress === 'function') {
172
- onProgress(`⏳ *Quorum reached (${finishedCount}/${total}). Starting ${Math.round(gracePeriodMs / 1000)}s grace period for stragglers...*\n`)
173
- }
174
- graceTimer = setTimeout(() => {
175
- if (typeof onProgress === 'function' && finishedCount < total) {
176
- onProgress(`⏩ *Grace period expired. Proceeding with ${finishedCount}/${total} ready candidates.*\n`)
177
- }
178
- for (let k = 0; k < total; k++) {
179
- if (!results[k]) {
180
- try { abortControllers[k].abort(new Error('Quorum grace period timed out')) } catch {}
181
- }
182
- }
183
- resolve()
184
- }, gracePeriodMs)
185
- graceTimer.unref?.()
186
- }
187
- }
188
-
189
- onTaskFinished = checkQuorum
190
- checkQuorum()
191
- })
192
-
193
- for (let i = 0; i < total; i++) {
194
- if (!results[i]) {
195
- const slot = references[i]
196
- const label = slotLabel(slot)
197
- results[i] = {
198
- index: i + 1,
199
- slot,
200
- label,
201
- text: `[Model ${label} timed out (quorum grace period exceeded)]`,
202
- usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
203
- costUsd: 0,
204
- ok: false,
205
- error: 'Quorum grace period timed out',
206
- }
207
- }
208
- }
209
- return results
210
- }
211
-
212
- // Standard mode: wait for all tasks to complete
213
- await new Promise((resolve) => {
214
- const checkAll = () => {
215
- if (finishedCount >= total) resolve()
216
- }
217
- onTaskFinished = checkAll
218
- checkAll()
219
- })
220
-
221
- return results
222
- }
223
-
224
- function candidatesForHistory(referenceOutputs) {
225
- return (referenceOutputs || []).map((r) => ({
226
- provider: r.slot?.provider || '',
227
- model: r.slot?.model || '',
228
- files: (r.files || []).map((f) => f.relativePath),
229
- usage: r.usage || { inputTokens: 0, outputTokens: 0 },
230
- costUsd: r.costUsd || 0,
231
- }))
232
- }
233
-
234
- async function createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs) {
235
- if (!liveCanvas || typeof liveCanvas.createPreviewFromContent !== 'function') return null
236
- const htmlRel = (promotedFiles || []).find((f) => f.endsWith('.html') || f.endsWith('.htm'))
237
- if (!htmlRel) return null
238
- let content = null
239
- for (const r of referenceOutputs || []) {
240
- const block = (r?.files || []).find((f) => f.relativePath === htmlRel)
241
- if (block?.content) {
242
- content = block.content
243
- break
244
- }
245
- }
246
- if (!content) return null
247
- try {
248
- return await liveCanvas.createPreviewFromContent({ content, title: htmlRel, filePath: path.join(cwd, htmlRel) })
249
- } catch {
250
- return null
251
- }
252
- }
26
+ import { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel } from './moa-candidates.js'
27
+ export { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel }
253
28
 
254
29
  /**
255
30
  * Executes the full Mixture of Agents pipeline.
@@ -265,7 +40,9 @@ export async function runMoAPipeline({
265
40
  historyFilePath = null,
266
41
  liveCanvas = null,
267
42
  onStreamDelta = null,
43
+ signal = null,
268
44
  }) {
45
+ if (signal?.aborted) return { error: new Error('Turn aborted') }
269
46
  const startTime = Date.now()
270
47
 
271
48
  // 1. Resolve configurations
@@ -298,13 +75,14 @@ export async function runMoAPipeline({
298
75
  const runId = crypto.randomUUID()
299
76
 
300
77
  // 2. Collect project context for refinement tasks
301
- const isRefinement = isRefinementTask(userPrompt)
78
+ const collectedCtx = await collectProjectContext(cwd, 16000)
79
+ const isRefinement = Boolean(collectedCtx?.files?.length > 0 && isRefinementTask(userPrompt, collectedCtx.files))
302
80
  let projectContext = ''
303
81
  if (isRefinement) {
304
82
  if (typeof onProgress === 'function') {
305
83
  onProgress('🔍 *Reading project files for refinement context...*\n')
306
84
  }
307
- projectContext = await collectProjectContext(cwd, 16000)
85
+ projectContext = formatProjectContext(collectedCtx.files)
308
86
  }
309
87
 
310
88
  // 3. Build prompts & evaluate broad questionnaire needs
@@ -330,11 +108,17 @@ export async function runMoAPipeline({
330
108
  quorumEnabled: isQuorumEnabled,
331
109
  gracePeriodSec,
332
110
  maxRetries: candidateRetries,
111
+ signal,
333
112
  },
334
113
  callLlm,
335
114
  onProgress
336
115
  )
337
116
 
117
+ if (signal?.aborted) {
118
+ await cleanMoaWorkspaces(cwd)
119
+ return { error: new Error('Turn aborted') }
120
+ }
121
+
338
122
  // Fail fast if all candidates failed
339
123
  const successfulRefs = referenceOutputs.filter((r) => r.ok)
340
124
  if (successfulRefs.length === 0) {
@@ -422,7 +206,9 @@ export async function runMoAPipeline({
422
206
  totalCostUsd,
423
207
  durationMs,
424
208
  }, historyFilePath || undefined)
425
- } catch {}
209
+ } catch (histErr) {
210
+ console.warn('[dsh-moa] Failed to record questionnaire run in history:', histErr?.message || histErr)
211
+ }
426
212
 
427
213
  return {
428
214
  kind: 'questions',
@@ -475,7 +261,9 @@ export async function runMoAPipeline({
475
261
  totalCostUsd,
476
262
  durationMs,
477
263
  }, historyFilePath || undefined)
478
- } catch {}
264
+ } catch (histErr) {
265
+ console.warn('[dsh-moa] Failed to record fast synthesis run in history:', histErr?.message || histErr)
266
+ }
479
267
 
480
268
  return {
481
269
  kind: 'synthesis',
@@ -518,6 +306,7 @@ export async function runMoAPipeline({
518
306
  temperature: refTemp,
519
307
  maxTokens,
520
308
  timeoutMs: refTimeoutSec * 1000,
309
+ signal,
521
310
  }, candidateRetries)
522
311
  const refinedText = typeof res === 'string' ? res : (res?.content || res?.text || cand.text)
523
312
  cand.text = refinedText
@@ -525,6 +314,7 @@ export async function runMoAPipeline({
525
314
  if (r2Files.length > 0) {
526
315
  cand.files = r2Files
527
316
  cand.syntaxWarning = (verifyFileSyntax(r2Files) || []).map((w) => `${w.file}: ${w.error}`).join('; ')
317
+ await writeCandidateWorkspace(cwd, cand.index, r2Files)
528
318
  }
529
319
  if (typeof res === 'object' && res?.usage) {
530
320
  cand.usage.inputTokens = (cand.usage.inputTokens || 0) + (res.usage.inputTokens || 0)
@@ -533,7 +323,9 @@ export async function runMoAPipeline({
533
323
  const extraCost = estimateTokenCost(slot, res.usage, prices)
534
324
  cand.costUsd = Number(((cand.costUsd || 0) + extraCost.costUsd).toFixed(5))
535
325
  }
536
- } catch {}
326
+ } catch (r2CostErr) {
327
+ console.warn('[dsh-moa] Failed to estimate Round 2 cost:', r2CostErr?.message || r2CostErr)
328
+ }
537
329
  })
538
330
  await Promise.allSettled(r2Promises)
539
331
  }
@@ -567,6 +359,7 @@ export async function runMoAPipeline({
567
359
  temperature: aggTemp,
568
360
  maxTokens,
569
361
  timeoutMs: aggTimeoutSec * 1000,
362
+ signal,
570
363
  onStreamDelta: (delta) => {
571
364
  if (isStreamAggregator && typeof onStreamDelta === 'function') {
572
365
  onStreamDelta(delta)
@@ -626,15 +419,12 @@ export async function runMoAPipeline({
626
419
 
627
420
  const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs)
628
421
 
629
- const totalTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0) + aggUsage.totalTokens
630
- const totalCostUsd = Number((referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + aggUsage.costUsd).toFixed(5))
631
422
  const durationMs = Date.now() - startTime
632
-
423
+ const usageSummary = summarizeMoAUsage(referenceOutputs, aggUsage)
633
424
  const winningRef = referenceOutputs[winningIndex - 1]
634
425
  const winnerModel = winningRef?.label || slotLabel(referenceModels[0])
635
426
  const finalAggLabel = slotLabel(chosenJudge)
636
427
 
637
- // Record run to history
638
428
  try {
639
429
  recordMoaRun({
640
430
  id: runId,
@@ -646,8 +436,8 @@ export async function runMoAPipeline({
646
436
  winnerIndex: winningIndex,
647
437
  winnerModel,
648
438
  promotedFiles,
649
- totalTokens,
650
- totalCostUsd,
439
+ totalTokens: usageSummary.totalTokens,
440
+ totalCostUsd: usageSummary.totalCostUsd,
651
441
  durationMs,
652
442
  }, historyFilePath || undefined)
653
443
  } catch (histErr) {
@@ -670,12 +460,7 @@ export async function runMoAPipeline({
670
460
  runId,
671
461
  allowCandidateOverride,
672
462
  ...(livePreview ? { liveCanvas: livePreview } : {}),
673
- usage: {
674
- totalTokens,
675
- totalCostUsd,
676
- candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
677
- aggregator: aggUsage,
678
- },
463
+ usage: usageSummary,
679
464
  durationMs,
680
465
  }
681
466
  }
@@ -716,6 +501,7 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
716
501
  prices,
717
502
  historyFilePath,
718
503
  liveCanvas,
504
+ signal,
719
505
  })
720
506
  .catch((err) => ({ error: err }))
721
507
  .finally(() => {
package/lib/pricing.js CHANGED
@@ -205,4 +205,13 @@ export function estimateTokenCost(slot = {}, usage = {}, customPrices = {}, cach
205
205
  rates,
206
206
  }
207
207
  }
208
-
208
+
209
+
210
+
211
+ export function summarizeMoAUsage(referenceOutputs = [], aggUsage = null) {
212
+ const normAgg = aggUsage || { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
213
+ const totalTokens = (referenceOutputs || []).reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0) + normAgg.totalTokens
214
+ const totalCostUsd = Number(((referenceOutputs || []).reduce((acc, r) => acc + (r.costUsd || 0), 0) + normAgg.costUsd).toFixed(5))
215
+ const candidates = (referenceOutputs || []).map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd }))
216
+ return { totalTokens, totalCostUsd, candidates, aggregator: normAgg }
217
+ }