@goodandready/dsh-moa 0.2.61 → 0.2.63

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,19 +1,3 @@
1
- import { logger } from './logger.js'
2
- import crypto from 'node:crypto'
3
- import { extractFileBlocks, collectProjectContext, formatProjectContext, isRefinementTask, writeCandidateWorkspace, promoteCandidateWorkspace, cleanMoaWorkspaces, verifyFileSyntax } from './file-workspace.js'
4
- import { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
5
- import { recordMoaRunAsync, candidatesForHistory, isTestEnvironment } from './history.js'
6
- import { createPromotedPreview } from './live-canvas.js'
7
- import { slotLabel, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildSynthesisPrompt, buildPeerCritiquePrompt } from './moa-prompts.js'
8
- import { parseWinnerIndex, parseRecommendedAssembler } from './moa-parser.js'
9
- import { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel } from './moa-candidates.js'
10
- import { executeTestGateForCandidates } from './moa-test-gate.js'
11
- import { executeMultiJudgePanel, buildCompositeBlockDirectives, runMultiJudgePhase } from './moa-multi-judge.js'
12
- import { applyBudgetGuardrails, checkPhaseBudget, estimateRound2Cost, estimateDownstreamCost, calcSynthesisPromptTokens, createBudgetAbortPayload } from './moa-budget.js'
13
- import { extractPriorTurnBaseline, pruneMultiTurnMessages } from './moa-context.js'
14
- import { generateMoABenchmarkReport } from './moa-report.js'
15
- import { formatFileScoresDirective, parseFileScores } from './moa-workbench.js'
16
- // Re-exports for consumers & backward compatibility
17
1
  export { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildCuratorSynthesisPrompt, buildSynthesisPrompt, ANTIPATTERNS_RUBRIC } from './moa-prompts.js'
18
2
  export { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
19
3
  export { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
@@ -21,573 +5,4 @@ export { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel
21
5
  export { executeTestGateForCandidates, runCandidateTestGate } from './moa-test-gate.js'
22
6
  export { streamMoATurn, streamPromoteTurn } from './moa-stream.js'
23
7
  export { formatFileScoresDirective, parseFileScores } from './moa-workbench.js'
24
- /**
25
- * Executes the full Mixture of Agents pipeline.
26
- */
27
- export async function runMoAPipeline({
28
- userPrompt,
29
- messages = [],
30
- preset,
31
- callLlm,
32
- cwd = process.cwd(), prices = {}, onProgress = null,
33
- historyFilePath = null, liveCanvas = null, onStreamDelta = null, signal = null,
34
- testGateEnabled = null, testCommand = null, execFn = null,
35
- webServerPort = null, timeMachineEngine = null, timeMachineUrl = null,
36
- force = isTestEnvironment() ? true : false, checkpoint = true, checkpointFn = null, runId: customRunId = null,
37
- }) {
38
- const runId = (customRunId && String(customRunId).trim()) || crypto.randomUUID()
39
- const promoteOpts = { webServerPort, timeMachineEngine, timeMachineUrl, force: Boolean(force), checkpoint, checkpointFn, signal }
40
- if (signal?.aborted) return { error: new Error('Turn aborted') }
41
- const startTime = Date.now()
42
- // 1. Resolve configurations
43
- const referenceModels = Array.isArray(preset?.reference_models) && preset.reference_models.length > 0
44
- ? preset.reference_models : [{ provider: '', model: '' }]
45
- const primaryJudge = preset?.aggregator?.provider && preset?.aggregator?.model
46
- ? preset.aggregator : { provider: '', model: '' }
47
- const fallbackJudges = Array.isArray(preset?.aggregator_fallbacks) ? preset.aggregator_fallbacks : []
48
- const judgesChain = [primaryJudge, ...fallbackJudges]
49
- const refTemp = typeof preset?.reference_temperature === 'number' ? preset.reference_temperature : 0.6
50
- const aggTemp = typeof preset?.aggregator_temperature === 'number' ? preset.aggregator_temperature : 0.4
51
- const maxTokens = typeof preset?.max_tokens === 'number' ? preset.max_tokens : 4096
52
- const judgeCriteria = preset?.judge_criteria || ''
53
- let isFastMode = referenceModels.length === 1 && !preset?.curator_synthesis
54
- const isCuratorSynthesis = Boolean(preset?.curator_synthesis), isStreamAggregator = preset?.stream_aggregator !== false
55
- const isQuorumEnabled = Boolean(preset?.quorum_enabled), gracePeriodSec = typeof preset?.grace_period_sec === 'number' ? preset.grace_period_sec : 10
56
- const candidateRetries = 1, refTimeoutSec = typeof preset?.reference_timeout_sec === 'number' ? preset.reference_timeout_sec : 60
57
- const aggTimeoutSec = typeof preset?.aggregator_timeout_sec === 'number' ? preset.aggregator_timeout_sec : 180
58
- const isBlindEvaluation = Boolean(preset?.blind_evaluation), isPeerCritiqueEnabled = Boolean(preset?.peer_critique_enabled)
59
- const allowCandidateOverride = Boolean(preset?.allow_candidate_override)
60
- // 1. Collect project context for refinement tasks
61
- const collectedCtx = await collectProjectContext(cwd, 16000)
62
- const isRefinement = Boolean(collectedCtx?.files?.length > 0 && isRefinementTask(userPrompt, collectedCtx.files))
63
- let projectContext = ''
64
- if (collectedCtx?.skippedFiles > 0 && typeof onProgress === 'function') {
65
- const preview = collectedCtx.skippedList.slice(0, 3).join(', ')
66
- const more = collectedCtx.skippedList.length > 3 ? ` (+${collectedCtx.skippedList.length - 3})` : ''
67
- onProgress(`⚠️ *Workspace scan note: ${collectedCtx.skippedFiles} file(s) skipped (${preview}${more})*\n`)
68
- }
69
- if (isRefinement) {
70
- if (typeof onProgress === 'function') {
71
- onProgress('🔍 *Reading project files for refinement context...*\n')
72
- }
73
- projectContext = formatProjectContext(collectedCtx.files, {
74
- skippedFiles: collectedCtx.skippedFiles,
75
- skippedList: collectedCtx.skippedList,
76
- })
77
- }
78
- // 2. Budget Guardrail check
79
- const budgetCheck = applyBudgetGuardrails({
80
- references: referenceModels,
81
- enabled: preset?.budget_guard_enabled,
82
- maxBudgetUsd: preset?.max_budget_usd,
83
- action: preset?.budget_action || 'trim',
84
- prices,
85
- promptLength: (userPrompt?.length || 1000) + (judgeCriteria?.length || 0) + (projectContext ? projectContext.length : 0),
86
- judgeCriteria,
87
- projectContext,
88
- messages,
89
- maxTokens,
90
- peerCritiqueEnabled: isPeerCritiqueEnabled,
91
- aggregator: primaryJudge,
92
- judgeModels: preset?.multi_judge_enabled ? preset?.judge_models : [],
93
- })
94
- if (!budgetCheck.allowed) {
95
- return createBudgetAbortPayload({ reason: budgetCheck.reason, primaryJudge, referenceOutputs: [], currentSpentUsd: 0, preset, isRefinement, isFastMode, collectedCtx, startTime })
96
- }
97
- const effectiveReferences = budgetCheck.references
98
- if (effectiveReferences.length === 1 && !preset?.curator_synthesis) {
99
- isFastMode = true
100
- }
101
- if (budgetCheck.action === 'trim' && typeof onProgress === 'function') {
102
- onProgress(`✂️ *${budgetCheck.reason}*\n`)
103
- }
104
- // 3. Build prompts & Feature 8 multi-turn context
105
- const priorTurnBaseline = preset?.multi_turn_enabled !== false ? extractPriorTurnBaseline(messages) : null
106
- const prunedHistory = preset?.multi_turn_enabled !== false ? pruneMultiTurnMessages(messages) : messages
107
- const candidateSystemPrompt = projectContext
108
- ? `${REFERENCE_SYSTEM_PROMPT}\n\n### Current Project Files & Context:\n${projectContext}`
109
- : REFERENCE_SYSTEM_PROMPT
110
- const askClarifyingQuestions = preset?.ask_clarifying_questions !== false
111
- const needsQuestions = askClarifyingQuestions && isBroadPromptRequiringQuestions(userPrompt, messages)
112
- const enrichedMessages = [...prunedHistory, { role: 'user', content: userPrompt }]
113
- // 4. Parallel fan-out to candidate models with temperature gradient & local fallback
114
- const referenceOutputs = await runReferencesParallel(
115
- effectiveReferences,
116
- enrichedMessages,
117
- {
118
- systemPrompt: candidateSystemPrompt,
119
- temperature: refTemp,
120
- temperature_gradient_enabled: Boolean(preset?.temperature_gradient_enabled),
121
- local_fallback_enabled: Boolean(preset?.local_fallback_enabled),
122
- local_fallback_models: preset?.local_fallback_models || [],
123
- maxTokens,
124
- prices,
125
- timeoutSec: refTimeoutSec,
126
- quorumEnabled: isQuorumEnabled,
127
- gracePeriodSec,
128
- maxRetries: candidateRetries,
129
- signal,
130
- },
131
- callLlm,
132
- onProgress
133
- )
134
- if (signal?.aborted) {
135
- await cleanMoaWorkspaces(cwd, { runId })
136
- return { error: new Error('Turn aborted') }
137
- }
138
- // Fail fast if all candidates failed
139
- const successfulRefs = referenceOutputs.filter((r) => r.ok)
140
- if (successfulRefs.length === 0) {
141
- const reasons = referenceOutputs.map((r) => `${r.label}: ${r.error || 'unknown error'}`).join('; ')
142
- const totalTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0)
143
- const totalCostUsd = Number((referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0)).toFixed(5))
144
- return {
145
- kind: 'failure', content: `⚠️ All advisor models (${referenceOutputs.length}) failed: ${reasons}`,
146
- aggregator: slotLabel(primaryJudge), references: referenceOutputs, presetName: preset?.name || 'default',
147
- isRefinement, isFastMode, winningIndex: 0, winnerModel: 'none', promotedFiles: [],
148
- usage: { totalTokens, totalCostUsd, candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })), aggregator: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 } },
149
- skippedFiles: collectedCtx?.skippedFiles || 0, skippedList: collectedCtx?.skippedList || [], durationMs: Date.now() - startTime,
150
- }
151
- }
152
- // 5. Extract file blocks & write candidate workspaces
153
- for (let i = 0; i < referenceOutputs.length; i++) {
154
- const ref = referenceOutputs[i]
155
- if (!ref.ok) continue
156
- const files = extractFileBlocks(ref.text)
157
- ref.files = files
158
- if (files.length > 0) {
159
- ref.syntaxWarning = (verifyFileSyntax(files) || []).map((w) => `${w.file}: ${w.error}`).join('; ')
160
- await writeCandidateWorkspace(cwd, i + 1, files, { runId })
161
- }
162
- }
163
- if (signal?.aborted) {
164
- await cleanMoaWorkspaces(cwd, { runId })
165
- return { error: new Error('Turn aborted') }
166
- }
167
- // 5b. Test Execution Gate
168
- await executeTestGateForCandidates({
169
- cwd,
170
- referenceOutputs,
171
- preset,
172
- options: { testGateEnabled, testCommand, execFn, runId },
173
- onProgress,
174
- })
175
- // 6. Questionnaire synthesis branch
176
- if (needsQuestions) {
177
- if (typeof onProgress === 'function') {
178
- onProgress('📋 *Judge synthesizes the clarification questionnaire...*\n')
179
- }
180
- const questionPrompt = buildQuestionSynthesisPrompt(userPrompt, referenceOutputs)
181
- let questionsContent = ''
182
- let qUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
183
- try {
184
- const qRes = await callWithTransientRetry(callLlm, {
185
- provider: primaryJudge.provider,
186
- model: primaryJudge.model,
187
- messages: [{ role: 'user', content: questionPrompt }],
188
- temperature: 0.3,
189
- maxTokens: 2048,
190
- timeoutMs: aggTimeoutSec * 1000,
191
- }, 1)
192
- questionsContent = typeof qRes === 'string' ? qRes : (qRes?.content || qRes?.text || '')
193
- const qFallbackUsage = (typeof qRes === 'object' && qRes?.usage) ? qRes.usage : {
194
- inputTokens: Math.max(1, Math.round(questionPrompt.length / 4)),
195
- outputTokens: Math.max(1, Math.round(questionsContent.length / 4)),
196
- }
197
- qUsage = estimateTokenCost(primaryJudge, qFallbackUsage, prices)
198
- } catch (err) {
199
- logger.warn('[dsh-moa] Questionnaire synthesis failed, proceeding with fallback questions:', err)
200
- questionsContent = `### Clarification of Requirements: "${userPrompt}"\n\n1. **Architecture & Scope**: Single-file deliverable or multi-module project structure?\n2. **Design & Style**: Minimalist, dark mode, or clean neutral theme?\n3. **Functional Priorities**: Core MVP or comprehensive extended implementation?\n\n*Reply with your preferences (e.g. "1, 2") or proceed with defaults.*`
201
- }
202
- await cleanMoaWorkspaces(cwd, { runId })
203
- const totalTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0) + qUsage.totalTokens
204
- const totalCostUsd = Number((referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + qUsage.costUsd).toFixed(5))
205
- const durationMs = Date.now() - startTime
206
- try {
207
- await recordMoaRunAsync({
208
- cwd,
209
- prompt: userPrompt,
210
- preset: preset?.name || 'default',
211
- isRefinement,
212
- candidates: candidatesForHistory(referenceOutputs),
213
- aggregator: { provider: primaryJudge.provider, model: primaryJudge.model, usage: qUsage, costUsd: qUsage.costUsd },
214
- winnerIndex: -1,
215
- winnerModel: 'none',
216
- promotedFiles: [],
217
- totalTokens,
218
- totalCostUsd,
219
- durationMs,
220
- }, historyFilePath || undefined)
221
- } catch (histErr) {
222
- logger.warn('[dsh-moa] Failed to record questionnaire run in history:', histErr?.message || histErr)
223
- }
224
- return {
225
- kind: 'questions', content: questionsContent, aggregator: slotLabel(primaryJudge),
226
- references: referenceOutputs, presetName: preset?.name || 'default', isRefinement, isFastMode,
227
- winningIndex: 0, winnerModel: 'none', promotedFiles: [],
228
- usage: { totalTokens, totalCostUsd, candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })), aggregator: qUsage },
229
- skippedFiles: collectedCtx?.skippedFiles || 0, skippedList: collectedCtx?.skippedList || [], durationMs,
230
- }
231
- }
232
-
233
- // 7. Fast Mode (1 candidate, bypass judge)
234
- if (isFastMode) {
235
- if (signal?.aborted) {
236
- await cleanMoaWorkspaces(cwd, { runId })
237
- return { error: new Error('Turn aborted') }
238
- }
239
- const single = successfulRefs[0]
240
- let promotedFiles = [], promoteErr = null
241
- if (single.files?.length > 0) {
242
- try {
243
- promotedFiles = await promoteCandidateWorkspace(cwd, single.index, { ...promoteOpts, runId, keepMoa: allowCandidateOverride })
244
- } catch (e) { promoteErr = e }
245
- } else if (!allowCandidateOverride) {
246
- await cleanMoaWorkspaces(cwd, { runId })
247
- }
248
- if (promoteErr) {
249
- if (!allowCandidateOverride) await cleanMoaWorkspaces(cwd, { runId })
250
- if (signal?.aborted || promoteErr?.message?.includes('aborted') || promoteErr?.name === 'AbortError' || promoteErr?.message === 'Turn aborted') {
251
- return { error: new Error('Turn aborted') }
252
- }
253
- const totalTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0)
254
- const totalCostUsd = Number((referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0)).toFixed(5))
255
- const durationMs = Date.now() - startTime
256
- try {
257
- await recordMoaRunAsync({
258
- id: runId, cwd, prompt: userPrompt, preset: preset?.name || 'default',
259
- isRefinement, isFastMode: true, candidates: candidatesForHistory(referenceOutputs),
260
- aggregator: null, winnerIndex: 0, winnerModel: 'none',
261
- promotedFiles: [], totalTokens, totalCostUsd, durationMs,
262
- error: promoteErr?.message || String(promoteErr),
263
- }, historyFilePath || undefined)
264
- } catch (histErr) {
265
- logger.warn('[dsh-moa] Failed to record fast synthesis failure in history:', histErr?.message || histErr)
266
- }
267
- return {
268
- kind: 'failure', error: promoteErr,
269
- content: '🛑 Checkpoint or Promotion Failure: ' + (promoteErr?.message || String(promoteErr)),
270
- aggregator: 'Fast Mode (Direct)', references: referenceOutputs,
271
- presetName: preset?.name || 'default', isRefinement, isFastMode: true,
272
- winningIndex: 0, winnerModel: 'none', promotedFiles: [], runId,
273
- usage: {
274
- totalTokens, totalCostUsd,
275
- candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
276
- aggregator: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
277
- },
278
- skippedFiles: collectedCtx?.skippedFiles || 0,
279
- skippedList: collectedCtx?.skippedList || [],
280
- durationMs,
281
- }
282
- }
283
-
284
- const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs, { winningIndex: single.index })
285
- const totalTokens = single.usage?.totalTokens || 0
286
- const totalCostUsd = Number((single.costUsd || 0).toFixed(5))
287
- const durationMs = Date.now() - startTime
288
-
289
- try {
290
- await recordMoaRunAsync({
291
- id: runId, cwd, prompt: userPrompt, preset: preset?.name || 'default',
292
- isRefinement, isFastMode: true, candidates: candidatesForHistory(referenceOutputs),
293
- aggregator: null, winnerIndex: single.index, winnerModel: single.label,
294
- promotedFiles, totalTokens, totalCostUsd, durationMs,
295
- }, historyFilePath || undefined)
296
- } catch (histErr) {
297
- logger.warn('[dsh-moa] Failed to record fast synthesis run in history:', histErr?.message || histErr)
298
- }
299
-
300
- return {
301
- kind: 'synthesis', content: single.text, aggregator: 'Fast Mode (Direct)', references: referenceOutputs,
302
- presetName: preset?.name || 'default', isRefinement, isFastMode: true, winningIndex: single.index,
303
- winnerModel: single.label, promotedFiles, runId, ...(livePreview ? { liveCanvas: livePreview } : {}),
304
- usage: { totalTokens, totalCostUsd, candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })), aggregator: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 } },
305
- skippedFiles: collectedCtx?.skippedFiles || 0, skippedList: collectedCtx?.skippedList || [], durationMs,
306
- }
307
- }
308
-
309
- if (signal?.aborted) {
310
- await cleanMoaWorkspaces(cwd, { runId })
311
- return { error: new Error('Turn aborted') }
312
- }
313
-
314
- // 7b. Consilium Round 2 (Peer Critique) if enabled
315
- let runRound2 = isPeerCritiqueEnabled && successfulRefs.length > 1
316
- if (runRound2 && preset?.budget_guard_enabled && preset?.max_budget_usd > 0) {
317
- const currentSpent = referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0)
318
- const r2Next = estimateRound2Cost(successfulRefs, prices, maxTokens)
319
- const downNext = estimateDownstreamCost(primaryJudge, preset?.judge_models, successfulRefs.length, prices, maxTokens)
320
- const phaseCheck = checkPhaseBudget({ phaseName: 'Consilium Round 2', currentSpentUsd: currentSpent, nextPhaseCostUsd: r2Next + downNext, maxBudgetUsd: preset.max_budget_usd, action: preset.budget_action || 'trim', enabled: true })
321
- if (!phaseCheck.allowed) {
322
- if (phaseCheck.action === 'abort') {
323
- await cleanMoaWorkspaces(cwd, { runId })
324
- return createBudgetAbortPayload({ reason: phaseCheck.reason, primaryJudge, referenceOutputs, currentSpentUsd: currentSpent, preset, isRefinement, isFastMode, collectedCtx, startTime })
325
- }
326
- runRound2 = false
327
- if (typeof onProgress === 'function') {
328
- onProgress(`✂️ *Skipping Round 2 to stay within budget ($${preset.max_budget_usd.toFixed(4)})*\n`)
329
- }
330
- }
331
- }
332
-
333
- if (runRound2) {
334
- if (typeof onProgress === 'function') {
335
- onProgress('🤝 *Consilium Round 2: Candidates reviewing peer proposals in parallel...*\n')
336
- }
337
- const r2Promises = successfulRefs.map(async (cand) => {
338
- const opponents = successfulRefs
339
- .filter((c) => c.index !== cand.index)
340
- .map((c) => ({
341
- label: isBlindEvaluation ? `Candidate ${c.index}` : c.label,
342
- role_persona: c.role_persona || c.slot?.role_persona,
343
- text: c.text,
344
- }))
345
- const r2Prompt = buildPeerCritiquePrompt(userPrompt, cand.text, opponents, isBlindEvaluation)
346
- try {
347
- const slot = cand.slot || { provider: cand.provider, model: cand.model }
348
- const res = await callWithTransientRetry(callLlm, {
349
- provider: slot.provider,
350
- model: slot.model,
351
- messages: [{ role: 'user', content: r2Prompt }],
352
- temperature: refTemp,
353
- maxTokens,
354
- timeoutMs: refTimeoutSec * 1000,
355
- signal,
356
- }, candidateRetries)
357
- const refinedText = typeof res === 'string' ? res : (res?.content || res?.text || cand.text)
358
- cand.text = refinedText
359
- const r2Files = extractFileBlocks(refinedText)
360
- if (r2Files.length > 0) {
361
- cand.files = r2Files
362
- cand.syntaxWarning = (verifyFileSyntax(r2Files) || []).map((w) => `${w.file}: ${w.error}`).join('; ')
363
- await writeCandidateWorkspace(cwd, cand.index, r2Files, { runId })
364
- }
365
- if (typeof res === 'object' && res?.usage) {
366
- cand.usage.inputTokens = (cand.usage.inputTokens || 0) + (res.usage.inputTokens || 0)
367
- cand.usage.outputTokens = (cand.usage.outputTokens || 0) + (res.usage.outputTokens || 0)
368
- cand.usage.totalTokens = (cand.usage.totalTokens || 0) + (res.usage.totalTokens || 0)
369
- const extraCost = estimateTokenCost(slot, res.usage, prices)
370
- cand.costUsd = Number(((cand.costUsd || 0) + extraCost.costUsd).toFixed(5))
371
- }
372
- } catch (r2Err) {
373
- logger.warn(`[dsh-moa] Round 2 peer critique failed for candidate ${cand.index}:`, r2Err?.message || r2Err)
374
- }
375
- })
376
- await Promise.allSettled(r2Promises)
377
- await executeTestGateForCandidates({
378
- cwd,
379
- referenceOutputs: successfulRefs,
380
- preset,
381
- options: { testGateEnabled, testCommand, execFn, runId },
382
- onProgress,
383
- })
384
- }
385
-
386
- // 7c. Feature 2: Multi-Judge Panel & Consensus Voting
387
- let multiJudgeResult = null
388
- if (signal?.aborted) {
389
- await cleanMoaWorkspaces(cwd, { runId })
390
- return { error: new Error('Turn aborted') }
391
- }
392
-
393
- const currentRefSpent = referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0)
394
- const panelPhase = await runMultiJudgePhase({
395
- preset, successfulRefs, primaryJudge, userPrompt, judgeCriteria, prices, maxTokens, callLlm, signal, onProgress, currentSpent: currentRefSpent,
396
- })
397
- if (panelPhase.abort) {
398
- await cleanMoaWorkspaces(cwd, { runId })
399
- return createBudgetAbortPayload({ reason: panelPhase.reason, primaryJudge, referenceOutputs, currentSpentUsd: currentRefSpent, preset, isRefinement, isFastMode, collectedCtx, startTime })
400
- }
401
- multiJudgeResult = panelPhase.result
402
-
403
- // 7d. Feature 3: Composite Block Merge Directive
404
- const compositeMergeDirective = (preset?.composite_merge_enabled && successfulRefs.length > 1)
405
- ? buildCompositeBlockDirectives(successfulRefs, { blind: Boolean(preset.blind_evaluation) })
406
- : null
407
-
408
- // 7e. Interactive Workbench File Scoring Directive
409
- const fileScoringDirective = (preset?.interactive_workbench_enabled)
410
- ? formatFileScoresDirective()
411
- : null
412
-
413
- // 8. Synthesis phase via primary judge or fallback chain
414
- const synthesisPrompt = buildSynthesisPrompt(userPrompt, referenceOutputs, judgeCriteria, {
415
- curatorSynthesis: isCuratorSynthesis,
416
- blindEvaluation: isBlindEvaluation,
417
- priorTurnBaseline,
418
- compositeMergeDirective,
419
- fileScoringDirective,
420
- consensusReport: multiJudgeResult?.consensusReport,
421
- })
422
- const synthPromptTokens = Math.max(50, Math.ceil(synthesisPrompt.length / 4))
423
-
424
- if (preset?.budget_guard_enabled && preset?.max_budget_usd > 0 && !isFastMode) {
425
- const currentSpent = referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + (multiJudgeResult?.totalCostUsd || 0)
426
- const downNext = estimateDownstreamCost(primaryJudge, [], successfulRefs.length, prices, maxTokens, synthPromptTokens)
427
- const phaseCheck = checkPhaseBudget({ phaseName: 'Judge Synthesis', currentSpentUsd: currentSpent, nextPhaseCostUsd: downNext, maxBudgetUsd: preset.max_budget_usd, action: preset.budget_action || 'trim', enabled: true })
428
- if (!phaseCheck.allowed && fallbackJudges.length === 0) {
429
- await cleanMoaWorkspaces(cwd, { runId })
430
- return createBudgetAbortPayload({ reason: phaseCheck.reason, primaryJudge, referenceOutputs, currentSpentUsd: currentSpent, preset, isRefinement, isFastMode, collectedCtx, startTime, multiJudgeResult })
431
- }
432
- }
433
- let synthesizedText = ''
434
- let aggUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
435
- let chosenJudge = primaryJudge
436
- let judgeSuccess = false
437
- let lastJudgeError = null
438
-
439
- for (let jIdx = 0; jIdx < judgesChain.length; jIdx++) {
440
- const currentJudge = judgesChain[jIdx]
441
- const currentLabel = slotLabel(currentJudge)
442
-
443
- if (preset?.budget_guard_enabled && preset?.max_budget_usd > 0 && !isFastMode) {
444
- const currentSpent = referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + (multiJudgeResult?.totalCostUsd || 0)
445
- const judgeCost = estimateDownstreamCost(currentJudge, [], successfulRefs.length, prices, maxTokens, synthPromptTokens)
446
- const phaseCheck = checkPhaseBudget({ phaseName: `Judge Synthesis (${currentLabel})`, currentSpentUsd: currentSpent, nextPhaseCostUsd: judgeCost, maxBudgetUsd: preset.max_budget_usd, action: preset.budget_action || 'trim', enabled: true })
447
- if (!phaseCheck.allowed) {
448
- lastJudgeError = new Error(phaseCheck.reason)
449
- logger.warn(`[dsh-moa] Judge ${currentLabel} budget check failed: ${phaseCheck.reason}`)
450
- if (typeof onProgress === 'function') onProgress(`⚠️ *Judge ${currentLabel} skipped: exceeds budget limit.*\n`)
451
- continue
452
- }
453
- }
454
-
455
- if (typeof onProgress === 'function') {
456
- const modeTitle = isCuratorSynthesis ? 'Curator' : 'Judge'
457
- const fallbackBadge = jIdx > 0 ? ` (Fallback #${jIdx})` : ''
458
- onProgress(`⚖️ *${modeTitle} (${currentLabel}${fallbackBadge}) synthesizing solution...*\n`)
459
- }
460
-
461
- try {
462
- const aggRes = await callWithTransientRetry(callLlm, {
463
- provider: currentJudge.provider,
464
- model: currentJudge.model,
465
- messages: [{ role: 'user', content: synthesisPrompt }],
466
- temperature: aggTemp,
467
- maxTokens,
468
- timeoutMs: aggTimeoutSec * 1000,
469
- signal,
470
- onStreamDelta: (delta) => {
471
- if (isStreamAggregator && typeof onStreamDelta === 'function') {
472
- onStreamDelta(delta)
473
- }
474
- },
475
- }, 0)
476
-
477
- synthesizedText = typeof aggRes === 'string' ? aggRes : (aggRes?.content || aggRes?.text || '')
478
-
479
- const aggFallbackUsage = (typeof aggRes === 'object' && aggRes?.usage) ? aggRes.usage : {
480
- inputTokens: Math.max(1, Math.round(synthesisPrompt.length / 4)),
481
- outputTokens: Math.max(1, Math.round(synthesizedText.length / 4)),
482
- }
483
- aggUsage = estimateTokenCost(currentJudge, aggFallbackUsage, prices)
484
- chosenJudge = currentJudge
485
- judgeSuccess = true
486
- break
487
- } catch (err) {
488
- lastJudgeError = err
489
- logger.warn(`[dsh-moa] Judge ${currentLabel} failed:`, err)
490
- if (typeof onProgress === 'function') {
491
- const nextJudge = judgesChain[jIdx + 1]
492
- const nextHint = nextJudge ? ` Trying fallback ${slotLabel(nextJudge)}...` : ''
493
- onProgress(`⚠️ *Judge ${currentLabel} failed: ${err?.message || err}.${nextHint}*\n`)
494
- }
495
- }
496
- }
497
-
498
- if (!judgeSuccess) {
499
- if (preset?.budget_guard_enabled && preset?.max_budget_usd > 0 && !isFastMode && (lastJudgeError?.message?.includes('exceeds max budget') || lastJudgeError?.message?.includes('Budget Guardrail') || preset?.budget_action === 'abort')) {
500
- await cleanMoaWorkspaces(cwd, { runId })
501
- const currentSpent = referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + (multiJudgeResult?.totalCostUsd || 0)
502
- return createBudgetAbortPayload({ reason: lastJudgeError?.message || 'Budget exceeded before judge synthesis', primaryJudge, referenceOutputs, currentSpentUsd: currentSpent, preset, isRefinement, isFastMode, collectedCtx, startTime, multiJudgeResult })
503
- }
504
- const firstErr = lastJudgeError ? (lastJudgeError.message || String(lastJudgeError)) : 'unknown error'
505
- synthesizedText = `⚠️ *[Aggregator error: ${firstErr}. Fallback candidate outputs:]*\n\n` +
506
- successfulRefs.map((r, i) => `### Candidate ${i + 1} (${r.label})\n${r.text}`).join('\n\n')
507
- }
508
-
509
- if (signal?.aborted) {
510
- await cleanMoaWorkspaces(cwd, { runId })
511
- return { error: new Error('Turn aborted') }
512
- }
513
-
514
- // 9. Evaluate winner & promote files
515
- const winningIndex = (multiJudgeResult?.consensus && multiJudgeResult?.winningCandidateIndex)
516
- ? multiJudgeResult.winningCandidateIndex
517
- : parseWinnerIndex(synthesizedText, 1, referenceOutputs.length)
518
-
519
- const recommendedAssembler = isCuratorSynthesis
520
- ? parseRecommendedAssembler(synthesizedText, winningIndex, referenceOutputs.length)
521
- : null
522
-
523
- // Check if aggregator synthesized unified file blocks directly
524
- const synthesizedFiles = extractFileBlocks(synthesizedText)
525
- let promotedFiles = []
526
-
527
- const keepWorkspaces = allowCandidateOverride
528
- if (signal?.aborted) {
529
- if (!keepWorkspaces) await cleanMoaWorkspaces(cwd, { runId })
530
- return { error: new Error('Turn aborted') }
531
- }
532
- try {
533
- if (synthesizedFiles.length > 0) {
534
- await writeCandidateWorkspace(cwd, 'curator-synthesis', synthesizedFiles, { runId })
535
- promotedFiles = await promoteCandidateWorkspace(cwd, 'curator-synthesis', { ...promoteOpts, runId, keepMoa: keepWorkspaces })
536
- } else if (referenceOutputs[winningIndex - 1]?.files?.length > 0) {
537
- promotedFiles = await promoteCandidateWorkspace(cwd, winningIndex, { ...promoteOpts, runId, keepMoa: keepWorkspaces })
538
- } else if (successfulRefs[0]?.files?.length > 0) {
539
- const fallbackIdx = successfulRefs[0].index
540
- promotedFiles = await promoteCandidateWorkspace(cwd, fallbackIdx, { ...promoteOpts, runId, keepMoa: keepWorkspaces })
541
- } else if (!keepWorkspaces) {
542
- await cleanMoaWorkspaces(cwd, { runId })
543
- }
544
- } catch (promoteErr) {
545
- if (!keepWorkspaces) await cleanMoaWorkspaces(cwd, { runId })
546
- if (signal?.aborted || promoteErr?.message === 'Turn aborted') return { error: new Error('Turn aborted') }
547
- throw promoteErr
548
- }
549
-
550
- const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs, { winningIndex, synthesizedFiles })
551
- const durationMs = Date.now() - startTime
552
- const usageSummary = summarizeMoAUsage(referenceOutputs, aggUsage, multiJudgeResult?.totalUsage)
553
- const winningRef = referenceOutputs[winningIndex - 1]
554
- const winnerModel = winningRef?.label || slotLabel(effectiveReferences[0] || referenceModels[0])
555
- const finalAggLabel = slotLabel(chosenJudge)
556
-
557
- // Feature 9: Benchmark & Post-Mortem PR report
558
- let benchmarkReport = null
559
- if (preset?.report_generation_enabled) {
560
- benchmarkReport = generateMoABenchmarkReport({
561
- runId, timestamp: new Date().toISOString(), preset: preset?.name || 'default',
562
- prompt: userPrompt, durationMs, usage: usageSummary, costUsd: usageSummary.totalCostUsd,
563
- candidates: referenceOutputs, winningIndex, winningLabel: winnerModel,
564
- consensus: multiJudgeResult, testGate: referenceOutputs.filter((r) => r.testResult).map((r) => r.testResult),
565
- isComposite: Boolean(preset?.composite_merge_enabled),
566
- })
567
- }
568
-
569
- // Feature 10: Interactive Workbench candidate file quality scores
570
- const fileScores = (preset?.interactive_workbench_enabled)
571
- ? parseFileScores(synthesizedText, referenceOutputs)
572
- : null
573
-
574
- try {
575
- await recordMoaRunAsync({
576
- id: runId, cwd, prompt: userPrompt, preset: preset?.name || 'default', isRefinement,
577
- candidates: candidatesForHistory(referenceOutputs),
578
- aggregator: { provider: chosenJudge.provider, model: chosenJudge.model, usage: aggUsage, costUsd: aggUsage.costUsd },
579
- winnerIndex: winningIndex, winnerModel, promotedFiles, benchmarkReport, consensus: multiJudgeResult,
580
- fileScores, totalTokens: usageSummary.totalTokens, totalCostUsd: usageSummary.totalCostUsd, durationMs,
581
- }, historyFilePath || undefined)
582
- } catch (histErr) {
583
- logger.warn('[dsh-moa] Failed to record run in history:', histErr)
584
- }
585
-
586
- return {
587
- kind: 'synthesis', content: synthesizedText, aggregator: isFastMode ? 'Fast Mode (Direct)' : finalAggLabel,
588
- references: referenceOutputs, presetName: preset?.name || 'default', isRefinement, isFastMode, isCuratorSynthesis,
589
- recommendedAssembler, winningIndex, winnerModel, promotedFiles, runId, allowCandidateOverride,
590
- benchmarkReport, consensus: multiJudgeResult, fileScores, ...(livePreview ? { liveCanvas: livePreview } : {}),
591
- usage: usageSummary, skippedFiles: collectedCtx?.skippedFiles || 0, skippedList: collectedCtx?.skippedList || [], durationMs,
592
- }
593
- }
8
+ export { runMoAPipeline } from './pipeline/runner.js'