@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/index.js +37 -25
- package/lib/moa-runner.js +1 -586
- package/lib/pipeline/candidates.js +99 -0
- package/lib/pipeline/context.js +107 -0
- package/lib/pipeline/fast-mode.js +100 -0
- package/lib/pipeline/multi-judge.js +37 -0
- package/lib/pipeline/peer-critique.js +113 -0
- package/lib/pipeline/promotion.js +114 -0
- package/lib/pipeline/questions.js +79 -0
- package/lib/pipeline/runner.js +163 -0
- package/lib/pipeline/synthesis.js +150 -0
- package/lib/services.js +35 -0
- package/package.json +1 -1
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'
|