@goodandready/dsh-moa 0.2.61 → 0.2.62
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 +39 -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 +40 -0
- package/package.json +1 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { runReferencesParallel } from '../moa-candidates.js'
|
|
2
|
+
import { extractFileBlocks, writeCandidateWorkspace, cleanMoaWorkspaces } from '../workspace/fs.js'
|
|
3
|
+
import { verifyFileSyntax } from '../workspace/diff.js'
|
|
4
|
+
import { executeTestGateForCandidates } from '../moa-test-gate.js'
|
|
5
|
+
import { slotLabel } from '../moa-prompts.js'
|
|
6
|
+
|
|
7
|
+
export async function stageCandidateRound1({
|
|
8
|
+
effectiveReferences,
|
|
9
|
+
enrichedMessages,
|
|
10
|
+
candidateSystemPrompt,
|
|
11
|
+
refTemp,
|
|
12
|
+
preset,
|
|
13
|
+
maxTokens,
|
|
14
|
+
prices,
|
|
15
|
+
refTimeoutSec,
|
|
16
|
+
isQuorumEnabled,
|
|
17
|
+
gracePeriodSec,
|
|
18
|
+
candidateRetries,
|
|
19
|
+
signal,
|
|
20
|
+
callLlm,
|
|
21
|
+
onProgress,
|
|
22
|
+
cwd,
|
|
23
|
+
runId,
|
|
24
|
+
startTime,
|
|
25
|
+
primaryJudge,
|
|
26
|
+
isRefinement,
|
|
27
|
+
isFastMode,
|
|
28
|
+
collectedCtx,
|
|
29
|
+
testGateEnabled,
|
|
30
|
+
testCommand,
|
|
31
|
+
execFn,
|
|
32
|
+
}) {
|
|
33
|
+
const referenceOutputs = await runReferencesParallel(
|
|
34
|
+
effectiveReferences,
|
|
35
|
+
enrichedMessages,
|
|
36
|
+
{
|
|
37
|
+
systemPrompt: candidateSystemPrompt,
|
|
38
|
+
temperature: refTemp,
|
|
39
|
+
temperature_gradient_enabled: Boolean(preset?.temperature_gradient_enabled),
|
|
40
|
+
local_fallback_enabled: Boolean(preset?.local_fallback_enabled),
|
|
41
|
+
local_fallback_models: preset?.local_fallback_models || [],
|
|
42
|
+
maxTokens,
|
|
43
|
+
prices,
|
|
44
|
+
timeoutSec: refTimeoutSec,
|
|
45
|
+
quorumEnabled: isQuorumEnabled,
|
|
46
|
+
gracePeriodSec,
|
|
47
|
+
maxRetries: candidateRetries,
|
|
48
|
+
signal,
|
|
49
|
+
},
|
|
50
|
+
callLlm,
|
|
51
|
+
onProgress
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if (signal?.aborted) {
|
|
55
|
+
await cleanMoaWorkspaces(cwd, { runId })
|
|
56
|
+
return { ok: false, abort: true, error: new Error('Turn aborted') }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const successfulRefs = referenceOutputs.filter((r) => r.ok)
|
|
60
|
+
if (successfulRefs.length === 0) {
|
|
61
|
+
const reasons = referenceOutputs.map((r) => `${r.label}: ${r.error || 'unknown error'}`).join('; ')
|
|
62
|
+
const totalTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0)
|
|
63
|
+
const totalCostUsd = Number((referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0)).toFixed(5))
|
|
64
|
+
const failurePayload = {
|
|
65
|
+
kind: 'failure', content: `⚠️ All advisor models (${referenceOutputs.length}) failed: ${reasons}`,
|
|
66
|
+
aggregator: slotLabel(primaryJudge), references: referenceOutputs, presetName: preset?.name || 'default',
|
|
67
|
+
isRefinement, isFastMode, winningIndex: 0, winnerModel: 'none', promotedFiles: [],
|
|
68
|
+
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 } },
|
|
69
|
+
skippedFiles: collectedCtx?.skippedFiles || 0, skippedList: collectedCtx?.skippedList || [], durationMs: Date.now() - startTime,
|
|
70
|
+
}
|
|
71
|
+
return { ok: false, failurePayload }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < referenceOutputs.length; i++) {
|
|
75
|
+
const ref = referenceOutputs[i]
|
|
76
|
+
if (!ref.ok) continue
|
|
77
|
+
const files = extractFileBlocks(ref.text)
|
|
78
|
+
ref.files = files
|
|
79
|
+
if (files.length > 0) {
|
|
80
|
+
ref.syntaxWarning = (verifyFileSyntax(files) || []).map((w) => `${w.file}: ${w.error}`).join('; ')
|
|
81
|
+
await writeCandidateWorkspace(cwd, i + 1, files, { runId })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (signal?.aborted) {
|
|
86
|
+
await cleanMoaWorkspaces(cwd, { runId })
|
|
87
|
+
return { ok: false, abort: true, error: new Error('Turn aborted') }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await executeTestGateForCandidates({
|
|
91
|
+
cwd,
|
|
92
|
+
referenceOutputs,
|
|
93
|
+
preset,
|
|
94
|
+
options: { testGateEnabled, testCommand, execFn, runId },
|
|
95
|
+
onProgress,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
return { ok: true, referenceOutputs, successfulRefs }
|
|
99
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { collectProjectContext, formatProjectContext, isRefinementTask } from '../workspace/scanner.js'
|
|
2
|
+
import { applyBudgetGuardrails, createBudgetAbortPayload } from '../moa-budget.js'
|
|
3
|
+
import { extractPriorTurnBaseline, pruneMultiTurnMessages } from '../moa-context.js'
|
|
4
|
+
import { REFERENCE_SYSTEM_PROMPT } from '../moa-candidates.js'
|
|
5
|
+
import { isBroadPromptRequiringQuestions } from '../moa-prompts.js'
|
|
6
|
+
|
|
7
|
+
export async function stageProjectContext({
|
|
8
|
+
userPrompt,
|
|
9
|
+
messages = [],
|
|
10
|
+
preset,
|
|
11
|
+
cwd,
|
|
12
|
+
prices,
|
|
13
|
+
maxTokens,
|
|
14
|
+
startTime,
|
|
15
|
+
onProgress,
|
|
16
|
+
}) {
|
|
17
|
+
const referenceModels = Array.isArray(preset?.reference_models) && preset.reference_models.length > 0
|
|
18
|
+
? preset.reference_models : [{ provider: '', model: '' }]
|
|
19
|
+
const primaryJudge = preset?.aggregator?.provider && preset?.aggregator?.model
|
|
20
|
+
? preset.aggregator : { provider: '', model: '' }
|
|
21
|
+
const fallbackJudges = Array.isArray(preset?.aggregator_fallbacks) ? preset.aggregator_fallbacks : []
|
|
22
|
+
const judgesChain = [primaryJudge, ...fallbackJudges]
|
|
23
|
+
const refTemp = typeof preset?.reference_temperature === 'number' ? preset.reference_temperature : 0.6
|
|
24
|
+
const aggTemp = typeof preset?.aggregator_temperature === 'number' ? preset.aggregator_temperature : 0.4
|
|
25
|
+
const judgeCriteria = preset?.judge_criteria || ''
|
|
26
|
+
let isFastMode = referenceModels.length === 1 && !preset?.curator_synthesis
|
|
27
|
+
const isCuratorSynthesis = Boolean(preset?.curator_synthesis)
|
|
28
|
+
const isStreamAggregator = preset?.stream_aggregator !== false
|
|
29
|
+
const isQuorumEnabled = Boolean(preset?.quorum_enabled)
|
|
30
|
+
const gracePeriodSec = typeof preset?.grace_period_sec === 'number' ? preset.grace_period_sec : 10
|
|
31
|
+
const candidateRetries = 1
|
|
32
|
+
const refTimeoutSec = typeof preset?.reference_timeout_sec === 'number' ? preset.reference_timeout_sec : 60
|
|
33
|
+
const aggTimeoutSec = typeof preset?.aggregator_timeout_sec === 'number' ? preset.aggregator_timeout_sec : 180
|
|
34
|
+
const isBlindEvaluation = Boolean(preset?.blind_evaluation)
|
|
35
|
+
const isPeerCritiqueEnabled = Boolean(preset?.peer_critique_enabled)
|
|
36
|
+
const allowCandidateOverride = Boolean(preset?.allow_candidate_override)
|
|
37
|
+
|
|
38
|
+
const collectedCtx = await collectProjectContext(cwd, 16000)
|
|
39
|
+
const isRefinement = Boolean(collectedCtx?.files?.length > 0 && isRefinementTask(userPrompt, collectedCtx.files))
|
|
40
|
+
let projectContext = ''
|
|
41
|
+
if (collectedCtx?.skippedFiles > 0 && typeof onProgress === 'function') {
|
|
42
|
+
const preview = collectedCtx.skippedList.slice(0, 3).join(', ')
|
|
43
|
+
const more = collectedCtx.skippedList.length > 3 ? ` (+${collectedCtx.skippedList.length - 3})` : ''
|
|
44
|
+
onProgress(`⚠️ *Workspace scan note: ${collectedCtx.skippedFiles} file(s) skipped (${preview}${more})*\n`)
|
|
45
|
+
}
|
|
46
|
+
if (isRefinement) {
|
|
47
|
+
if (typeof onProgress === 'function') {
|
|
48
|
+
onProgress('🔍 *Reading project files for refinement context...*\n')
|
|
49
|
+
}
|
|
50
|
+
projectContext = formatProjectContext(collectedCtx.files, {
|
|
51
|
+
skippedFiles: collectedCtx.skippedFiles,
|
|
52
|
+
skippedList: collectedCtx.skippedList,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const budgetCheck = applyBudgetGuardrails({
|
|
57
|
+
references: referenceModels,
|
|
58
|
+
enabled: preset?.budget_guard_enabled,
|
|
59
|
+
maxBudgetUsd: preset?.max_budget_usd,
|
|
60
|
+
action: preset?.budget_action || 'trim',
|
|
61
|
+
prices,
|
|
62
|
+
promptLength: (userPrompt?.length || 1000) + (judgeCriteria?.length || 0) + (projectContext ? projectContext.length : 0),
|
|
63
|
+
judgeCriteria,
|
|
64
|
+
projectContext,
|
|
65
|
+
messages,
|
|
66
|
+
maxTokens,
|
|
67
|
+
peerCritiqueEnabled: isPeerCritiqueEnabled,
|
|
68
|
+
aggregator: primaryJudge,
|
|
69
|
+
judgeModels: preset?.multi_judge_enabled ? preset?.judge_models : [],
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
if (!budgetCheck.allowed) {
|
|
73
|
+
const abortPayload = createBudgetAbortPayload({
|
|
74
|
+
reason: budgetCheck.reason, primaryJudge, referenceOutputs: [], currentSpentUsd: 0,
|
|
75
|
+
preset, isRefinement, isFastMode, collectedCtx, startTime,
|
|
76
|
+
})
|
|
77
|
+
return { ok: false, abortPayload }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const effectiveReferences = budgetCheck.references
|
|
81
|
+
if (effectiveReferences.length === 1 && !preset?.curator_synthesis) {
|
|
82
|
+
isFastMode = true
|
|
83
|
+
}
|
|
84
|
+
if (budgetCheck.action === 'trim' && typeof onProgress === 'function') {
|
|
85
|
+
onProgress(`✂️ *${budgetCheck.reason}*\n`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const priorTurnBaseline = preset?.multi_turn_enabled !== false ? extractPriorTurnBaseline(messages) : null
|
|
89
|
+
const prunedHistory = preset?.multi_turn_enabled !== false ? pruneMultiTurnMessages(messages) : messages
|
|
90
|
+
const candidateSystemPrompt = projectContext
|
|
91
|
+
? `${REFERENCE_SYSTEM_PROMPT}\n\n### Current Project Files & Context:\n${projectContext}`
|
|
92
|
+
: REFERENCE_SYSTEM_PROMPT
|
|
93
|
+
const askClarifyingQuestions = preset?.ask_clarifying_questions !== false
|
|
94
|
+
const needsQuestions = askClarifyingQuestions && isBroadPromptRequiringQuestions(userPrompt, messages)
|
|
95
|
+
const enrichedMessages = [...prunedHistory, { role: 'user', content: userPrompt }]
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
ok: true,
|
|
99
|
+
referenceModels, primaryJudge, fallbackJudges, judgesChain,
|
|
100
|
+
refTemp, aggTemp, maxTokens, judgeCriteria, isFastMode, isCuratorSynthesis,
|
|
101
|
+
isStreamAggregator, isQuorumEnabled, gracePeriodSec, candidateRetries,
|
|
102
|
+
refTimeoutSec, aggTimeoutSec, isBlindEvaluation, isPeerCritiqueEnabled,
|
|
103
|
+
allowCandidateOverride, collectedCtx, isRefinement, projectContext,
|
|
104
|
+
effectiveReferences, priorTurnBaseline, candidateSystemPrompt,
|
|
105
|
+
needsQuestions, enrichedMessages,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { logger } from '../logger.js'
|
|
2
|
+
import { promoteCandidateWorkspace, cleanMoaWorkspaces } from '../workspace/fs.js'
|
|
3
|
+
import { createPromotedPreview } from '../live-canvas.js'
|
|
4
|
+
import { recordMoaRunAsync, candidatesForHistory } from '../history.js'
|
|
5
|
+
|
|
6
|
+
export async function stageFastMode({
|
|
7
|
+
successfulRefs,
|
|
8
|
+
referenceOutputs,
|
|
9
|
+
allowCandidateOverride,
|
|
10
|
+
promoteOpts,
|
|
11
|
+
cwd,
|
|
12
|
+
runId,
|
|
13
|
+
signal,
|
|
14
|
+
userPrompt,
|
|
15
|
+
preset,
|
|
16
|
+
isRefinement,
|
|
17
|
+
historyFilePath,
|
|
18
|
+
startTime,
|
|
19
|
+
collectedCtx,
|
|
20
|
+
liveCanvas,
|
|
21
|
+
}) {
|
|
22
|
+
if (signal?.aborted) {
|
|
23
|
+
await cleanMoaWorkspaces(cwd, { runId })
|
|
24
|
+
return { ok: false, abort: true, error: new Error('Turn aborted') }
|
|
25
|
+
}
|
|
26
|
+
const single = successfulRefs[0]
|
|
27
|
+
let promotedFiles = [], promoteErr = null
|
|
28
|
+
if (single.files?.length > 0) {
|
|
29
|
+
try {
|
|
30
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, single.index, { ...promoteOpts, runId, keepMoa: allowCandidateOverride })
|
|
31
|
+
} catch (e) { promoteErr = e }
|
|
32
|
+
} else if (!allowCandidateOverride) {
|
|
33
|
+
await cleanMoaWorkspaces(cwd, { runId })
|
|
34
|
+
}
|
|
35
|
+
if (promoteErr) {
|
|
36
|
+
if (!allowCandidateOverride) await cleanMoaWorkspaces(cwd, { runId })
|
|
37
|
+
if (signal?.aborted || promoteErr?.message?.includes('aborted') || promoteErr?.name === 'AbortError' || promoteErr?.message === 'Turn aborted') {
|
|
38
|
+
return { ok: false, abort: true, error: new Error('Turn aborted') }
|
|
39
|
+
}
|
|
40
|
+
const totalTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0)
|
|
41
|
+
const totalCostUsd = Number((referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0)).toFixed(5))
|
|
42
|
+
const durationMs = Date.now() - startTime
|
|
43
|
+
try {
|
|
44
|
+
await recordMoaRunAsync({
|
|
45
|
+
id: runId, cwd, prompt: userPrompt, preset: preset?.name || 'default',
|
|
46
|
+
isRefinement, isFastMode: true, candidates: candidatesForHistory(referenceOutputs),
|
|
47
|
+
aggregator: null, winnerIndex: 0, winnerModel: 'none',
|
|
48
|
+
promotedFiles: [], totalTokens, totalCostUsd, durationMs,
|
|
49
|
+
error: promoteErr?.message || String(promoteErr),
|
|
50
|
+
}, historyFilePath || undefined)
|
|
51
|
+
} catch (histErr) {
|
|
52
|
+
logger.warn('[dsh-moa] Failed to record fast synthesis failure in history:', histErr?.message || histErr)
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
payload: {
|
|
57
|
+
kind: 'failure', error: promoteErr,
|
|
58
|
+
content: '🛑 Checkpoint or Promotion Failure: ' + (promoteErr?.message || String(promoteErr)),
|
|
59
|
+
aggregator: 'Fast Mode (Direct)', references: referenceOutputs,
|
|
60
|
+
presetName: preset?.name || 'default', isRefinement, isFastMode: true,
|
|
61
|
+
winningIndex: 0, winnerModel: 'none', promotedFiles: [], runId,
|
|
62
|
+
usage: {
|
|
63
|
+
totalTokens, totalCostUsd,
|
|
64
|
+
candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
|
|
65
|
+
aggregator: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
66
|
+
},
|
|
67
|
+
skippedFiles: collectedCtx?.skippedFiles || 0,
|
|
68
|
+
skippedList: collectedCtx?.skippedList || [],
|
|
69
|
+
durationMs,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs, { winningIndex: single.index })
|
|
75
|
+
const totalTokens = single.usage?.totalTokens || 0
|
|
76
|
+
const totalCostUsd = Number((single.costUsd || 0).toFixed(5))
|
|
77
|
+
const durationMs = Date.now() - startTime
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
await recordMoaRunAsync({
|
|
81
|
+
id: runId, cwd, prompt: userPrompt, preset: preset?.name || 'default',
|
|
82
|
+
isRefinement, isFastMode: true, candidates: candidatesForHistory(referenceOutputs),
|
|
83
|
+
aggregator: null, winnerIndex: single.index, winnerModel: single.label,
|
|
84
|
+
promotedFiles, totalTokens, totalCostUsd, durationMs,
|
|
85
|
+
}, historyFilePath || undefined)
|
|
86
|
+
} catch (histErr) {
|
|
87
|
+
logger.warn('[dsh-moa] Failed to record fast synthesis run in history:', histErr?.message || histErr)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
ok: true,
|
|
92
|
+
payload: {
|
|
93
|
+
kind: 'synthesis', content: single.text, aggregator: 'Fast Mode (Direct)', references: referenceOutputs,
|
|
94
|
+
presetName: preset?.name || 'default', isRefinement, isFastMode: true, winningIndex: single.index,
|
|
95
|
+
winnerModel: single.label, promotedFiles, runId, ...(livePreview ? { liveCanvas: livePreview } : {}),
|
|
96
|
+
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 } },
|
|
97
|
+
skippedFiles: collectedCtx?.skippedFiles || 0, skippedList: collectedCtx?.skippedList || [], durationMs,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { runMultiJudgePhase } from '../moa-multi-judge.js'
|
|
2
|
+
import { createBudgetAbortPayload } from '../moa-budget.js'
|
|
3
|
+
import { cleanMoaWorkspaces } from '../workspace/fs.js'
|
|
4
|
+
|
|
5
|
+
export async function stageMultiJudge({
|
|
6
|
+
preset,
|
|
7
|
+
successfulRefs,
|
|
8
|
+
referenceOutputs,
|
|
9
|
+
primaryJudge,
|
|
10
|
+
userPrompt,
|
|
11
|
+
judgeCriteria,
|
|
12
|
+
prices,
|
|
13
|
+
maxTokens,
|
|
14
|
+
callLlm,
|
|
15
|
+
signal,
|
|
16
|
+
onProgress,
|
|
17
|
+
cwd,
|
|
18
|
+
runId,
|
|
19
|
+
isRefinement,
|
|
20
|
+
isFastMode,
|
|
21
|
+
collectedCtx,
|
|
22
|
+
startTime,
|
|
23
|
+
}) {
|
|
24
|
+
const currentRefSpent = referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0)
|
|
25
|
+
const panelPhase = await runMultiJudgePhase({
|
|
26
|
+
preset, successfulRefs, primaryJudge, userPrompt, judgeCriteria, prices, maxTokens, callLlm, signal, onProgress, currentSpent: currentRefSpent,
|
|
27
|
+
})
|
|
28
|
+
if (panelPhase.abort) {
|
|
29
|
+
await cleanMoaWorkspaces(cwd, { runId })
|
|
30
|
+
const abortPayload = createBudgetAbortPayload({
|
|
31
|
+
reason: panelPhase.reason, primaryJudge, referenceOutputs, currentSpentUsd: currentRefSpent,
|
|
32
|
+
preset, isRefinement, isFastMode, collectedCtx, startTime,
|
|
33
|
+
})
|
|
34
|
+
return { ok: false, abortPayload }
|
|
35
|
+
}
|
|
36
|
+
return { ok: true, multiJudgeResult: panelPhase.result }
|
|
37
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { logger } from '../logger.js'
|
|
2
|
+
import { estimateRound2Cost, estimateDownstreamCost, checkPhaseBudget, createBudgetAbortPayload } from '../moa-budget.js'
|
|
3
|
+
import { buildPeerCritiquePrompt } from '../moa-prompts.js'
|
|
4
|
+
import { callWithTransientRetry } from '../moa-candidates.js'
|
|
5
|
+
import { extractFileBlocks, writeCandidateWorkspace, cleanMoaWorkspaces } from '../workspace/fs.js'
|
|
6
|
+
import { verifyFileSyntax } from '../workspace/diff.js'
|
|
7
|
+
import { estimateTokenCost } from '../pricing.js'
|
|
8
|
+
import { executeTestGateForCandidates } from '../moa-test-gate.js'
|
|
9
|
+
|
|
10
|
+
export async function stagePeerCritique({
|
|
11
|
+
isPeerCritiqueEnabled,
|
|
12
|
+
successfulRefs,
|
|
13
|
+
referenceOutputs,
|
|
14
|
+
preset,
|
|
15
|
+
prices,
|
|
16
|
+
maxTokens,
|
|
17
|
+
primaryJudge,
|
|
18
|
+
userPrompt,
|
|
19
|
+
isBlindEvaluation,
|
|
20
|
+
refTemp,
|
|
21
|
+
refTimeoutSec,
|
|
22
|
+
candidateRetries,
|
|
23
|
+
signal,
|
|
24
|
+
callLlm,
|
|
25
|
+
onProgress,
|
|
26
|
+
cwd,
|
|
27
|
+
runId,
|
|
28
|
+
isRefinement,
|
|
29
|
+
isFastMode,
|
|
30
|
+
collectedCtx,
|
|
31
|
+
startTime,
|
|
32
|
+
options: { testGateEnabled, testCommand, execFn } = {},
|
|
33
|
+
}) {
|
|
34
|
+
let runRound2 = isPeerCritiqueEnabled && successfulRefs.length > 1
|
|
35
|
+
if (runRound2 && preset?.budget_guard_enabled && preset?.max_budget_usd > 0) {
|
|
36
|
+
const currentSpent = referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0)
|
|
37
|
+
const r2Next = estimateRound2Cost(successfulRefs, prices, maxTokens)
|
|
38
|
+
const downNext = estimateDownstreamCost(primaryJudge, preset?.judge_models, successfulRefs.length, prices, maxTokens)
|
|
39
|
+
const phaseCheck = checkPhaseBudget({
|
|
40
|
+
phaseName: 'Consilium Round 2', currentSpentUsd: currentSpent, nextPhaseCostUsd: r2Next + downNext,
|
|
41
|
+
maxBudgetUsd: preset.max_budget_usd, action: preset.budget_action || 'trim', enabled: true,
|
|
42
|
+
})
|
|
43
|
+
if (!phaseCheck.allowed) {
|
|
44
|
+
if (phaseCheck.action === 'abort') {
|
|
45
|
+
await cleanMoaWorkspaces(cwd, { runId })
|
|
46
|
+
const abortPayload = createBudgetAbortPayload({
|
|
47
|
+
reason: phaseCheck.reason, primaryJudge, referenceOutputs, currentSpentUsd: currentSpent,
|
|
48
|
+
preset, isRefinement, isFastMode, collectedCtx, startTime,
|
|
49
|
+
})
|
|
50
|
+
return { ok: false, abortPayload }
|
|
51
|
+
}
|
|
52
|
+
runRound2 = false
|
|
53
|
+
if (typeof onProgress === 'function') {
|
|
54
|
+
onProgress(`✂️ *Skipping Round 2 to stay within budget ($${preset.max_budget_usd.toFixed(4)})*\n`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (runRound2) {
|
|
60
|
+
if (typeof onProgress === 'function') {
|
|
61
|
+
onProgress('🤝 *Consilium Round 2: Candidates reviewing peer proposals in parallel...*\n')
|
|
62
|
+
}
|
|
63
|
+
const r2Promises = successfulRefs.map(async (cand) => {
|
|
64
|
+
const opponents = successfulRefs
|
|
65
|
+
.filter((c) => c.index !== cand.index)
|
|
66
|
+
.map((c) => ({
|
|
67
|
+
label: isBlindEvaluation ? `Candidate ${c.index}` : c.label,
|
|
68
|
+
role_persona: c.role_persona || c.slot?.role_persona,
|
|
69
|
+
text: c.text,
|
|
70
|
+
}))
|
|
71
|
+
const r2Prompt = buildPeerCritiquePrompt(userPrompt, cand.text, opponents, isBlindEvaluation)
|
|
72
|
+
try {
|
|
73
|
+
const slot = cand.slot || { provider: cand.provider, model: cand.model }
|
|
74
|
+
const res = await callWithTransientRetry(callLlm, {
|
|
75
|
+
provider: slot.provider,
|
|
76
|
+
model: slot.model,
|
|
77
|
+
messages: [{ role: 'user', content: r2Prompt }],
|
|
78
|
+
temperature: refTemp,
|
|
79
|
+
maxTokens,
|
|
80
|
+
timeoutMs: refTimeoutSec * 1000,
|
|
81
|
+
signal,
|
|
82
|
+
}, candidateRetries)
|
|
83
|
+
const refinedText = typeof res === 'string' ? res : (res?.content || res?.text || cand.text)
|
|
84
|
+
cand.text = refinedText
|
|
85
|
+
const r2Files = extractFileBlocks(refinedText)
|
|
86
|
+
if (r2Files.length > 0) {
|
|
87
|
+
cand.files = r2Files
|
|
88
|
+
cand.syntaxWarning = (verifyFileSyntax(r2Files) || []).map((w) => `${w.file}: ${w.error}`).join('; ')
|
|
89
|
+
await writeCandidateWorkspace(cwd, cand.index, r2Files, { runId })
|
|
90
|
+
}
|
|
91
|
+
if (typeof res === 'object' && res?.usage) {
|
|
92
|
+
cand.usage.inputTokens = (cand.usage.inputTokens || 0) + (res.usage.inputTokens || 0)
|
|
93
|
+
cand.usage.outputTokens = (cand.usage.outputTokens || 0) + (res.usage.outputTokens || 0)
|
|
94
|
+
cand.usage.totalTokens = (cand.usage.totalTokens || 0) + (res.usage.totalTokens || 0)
|
|
95
|
+
const extraCost = estimateTokenCost(slot, res.usage, prices)
|
|
96
|
+
cand.costUsd = Number(((cand.costUsd || 0) + extraCost.costUsd).toFixed(5))
|
|
97
|
+
}
|
|
98
|
+
} catch (r2Err) {
|
|
99
|
+
logger.warn(`[dsh-moa] Round 2 peer critique failed for candidate ${cand.index}:`, r2Err?.message || r2Err)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
await Promise.allSettled(r2Promises)
|
|
103
|
+
await executeTestGateForCandidates({
|
|
104
|
+
cwd,
|
|
105
|
+
referenceOutputs: successfulRefs,
|
|
106
|
+
preset,
|
|
107
|
+
options: { testGateEnabled, testCommand, execFn, runId },
|
|
108
|
+
onProgress,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { ok: true }
|
|
113
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { logger } from '../logger.js'
|
|
2
|
+
import { parseWinnerIndex, parseRecommendedAssembler } from '../moa-parser.js'
|
|
3
|
+
import { extractFileBlocks, writeCandidateWorkspace, promoteCandidateWorkspace, cleanMoaWorkspaces } from '../workspace/fs.js'
|
|
4
|
+
import { createPromotedPreview } from '../live-canvas.js'
|
|
5
|
+
import { summarizeMoAUsage } from '../pricing.js'
|
|
6
|
+
import { generateMoABenchmarkReport } from '../moa-report.js'
|
|
7
|
+
import { parseFileScores } from '../moa-workbench.js'
|
|
8
|
+
import { recordMoaRunAsync, candidatesForHistory } from '../history.js'
|
|
9
|
+
import { slotLabel } from '../moa-prompts.js'
|
|
10
|
+
|
|
11
|
+
export async function stagePromotion({
|
|
12
|
+
synthesizedText,
|
|
13
|
+
referenceOutputs,
|
|
14
|
+
successfulRefs,
|
|
15
|
+
multiJudgeResult,
|
|
16
|
+
isCuratorSynthesis,
|
|
17
|
+
allowCandidateOverride,
|
|
18
|
+
promoteOpts,
|
|
19
|
+
cwd,
|
|
20
|
+
runId,
|
|
21
|
+
signal,
|
|
22
|
+
liveCanvas,
|
|
23
|
+
startTime,
|
|
24
|
+
chosenJudge,
|
|
25
|
+
aggUsage,
|
|
26
|
+
userPrompt,
|
|
27
|
+
preset,
|
|
28
|
+
isRefinement,
|
|
29
|
+
isFastMode,
|
|
30
|
+
effectiveReferences,
|
|
31
|
+
referenceModels,
|
|
32
|
+
historyFilePath,
|
|
33
|
+
collectedCtx,
|
|
34
|
+
}) {
|
|
35
|
+
const winningIndex = (multiJudgeResult?.consensus && multiJudgeResult?.winningCandidateIndex)
|
|
36
|
+
? multiJudgeResult.winningCandidateIndex
|
|
37
|
+
: parseWinnerIndex(synthesizedText, 1, referenceOutputs.length)
|
|
38
|
+
|
|
39
|
+
const recommendedAssembler = isCuratorSynthesis
|
|
40
|
+
? parseRecommendedAssembler(synthesizedText, winningIndex, referenceOutputs.length)
|
|
41
|
+
: null
|
|
42
|
+
|
|
43
|
+
const synthesizedFiles = extractFileBlocks(synthesizedText)
|
|
44
|
+
let promotedFiles = []
|
|
45
|
+
const keepWorkspaces = allowCandidateOverride
|
|
46
|
+
|
|
47
|
+
if (signal?.aborted) {
|
|
48
|
+
if (!keepWorkspaces) await cleanMoaWorkspaces(cwd, { runId })
|
|
49
|
+
return { ok: false, abort: true, error: new Error('Turn aborted') }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
if (synthesizedFiles.length > 0) {
|
|
54
|
+
await writeCandidateWorkspace(cwd, 'curator-synthesis', synthesizedFiles, { runId })
|
|
55
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, 'curator-synthesis', { ...promoteOpts, runId, keepMoa: keepWorkspaces })
|
|
56
|
+
} else if (referenceOutputs[winningIndex - 1]?.files?.length > 0) {
|
|
57
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, winningIndex, { ...promoteOpts, runId, keepMoa: keepWorkspaces })
|
|
58
|
+
} else if (successfulRefs[0]?.files?.length > 0) {
|
|
59
|
+
const fallbackIdx = successfulRefs[0].index
|
|
60
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, fallbackIdx, { ...promoteOpts, runId, keepMoa: keepWorkspaces })
|
|
61
|
+
} else if (!keepWorkspaces) {
|
|
62
|
+
await cleanMoaWorkspaces(cwd, { runId })
|
|
63
|
+
}
|
|
64
|
+
} catch (promoteErr) {
|
|
65
|
+
if (!keepWorkspaces) await cleanMoaWorkspaces(cwd, { runId })
|
|
66
|
+
if (signal?.aborted || promoteErr?.message === 'Turn aborted') return { ok: false, abort: true, error: new Error('Turn aborted') }
|
|
67
|
+
throw promoteErr
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs, { winningIndex, synthesizedFiles })
|
|
71
|
+
const durationMs = Date.now() - startTime
|
|
72
|
+
const usageSummary = summarizeMoAUsage(referenceOutputs, aggUsage, multiJudgeResult?.totalUsage)
|
|
73
|
+
const winningRef = referenceOutputs[winningIndex - 1]
|
|
74
|
+
const winnerModel = winningRef?.label || slotLabel(effectiveReferences[0] || referenceModels[0])
|
|
75
|
+
const finalAggLabel = slotLabel(chosenJudge)
|
|
76
|
+
|
|
77
|
+
let benchmarkReport = null
|
|
78
|
+
if (preset?.report_generation_enabled) {
|
|
79
|
+
benchmarkReport = generateMoABenchmarkReport({
|
|
80
|
+
runId, timestamp: new Date().toISOString(), preset: preset?.name || 'default',
|
|
81
|
+
prompt: userPrompt, durationMs, usage: usageSummary, costUsd: usageSummary.totalCostUsd,
|
|
82
|
+
candidates: referenceOutputs, winningIndex, winningLabel: winnerModel,
|
|
83
|
+
consensus: multiJudgeResult, testGate: referenceOutputs.filter((r) => r.testResult).map((r) => r.testResult),
|
|
84
|
+
isComposite: Boolean(preset?.composite_merge_enabled),
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const fileScores = (preset?.interactive_workbench_enabled)
|
|
89
|
+
? parseFileScores(synthesizedText, referenceOutputs)
|
|
90
|
+
: null
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await recordMoaRunAsync({
|
|
94
|
+
id: runId, cwd, prompt: userPrompt, preset: preset?.name || 'default', isRefinement,
|
|
95
|
+
candidates: candidatesForHistory(referenceOutputs),
|
|
96
|
+
aggregator: { provider: chosenJudge.provider, model: chosenJudge.model, usage: aggUsage, costUsd: aggUsage.costUsd },
|
|
97
|
+
winnerIndex: winningIndex, winnerModel, promotedFiles, benchmarkReport, consensus: multiJudgeResult,
|
|
98
|
+
fileScores, totalTokens: usageSummary.totalTokens, totalCostUsd: usageSummary.totalCostUsd, durationMs,
|
|
99
|
+
}, historyFilePath || undefined)
|
|
100
|
+
} catch (histErr) {
|
|
101
|
+
logger.warn('[dsh-moa] Failed to record run in history:', histErr)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
ok: true,
|
|
106
|
+
payload: {
|
|
107
|
+
kind: 'synthesis', content: synthesizedText, aggregator: isFastMode ? 'Fast Mode (Direct)' : finalAggLabel,
|
|
108
|
+
references: referenceOutputs, presetName: preset?.name || 'default', isRefinement, isFastMode, isCuratorSynthesis,
|
|
109
|
+
recommendedAssembler, winningIndex, winnerModel, promotedFiles, runId, allowCandidateOverride,
|
|
110
|
+
benchmarkReport, consensus: multiJudgeResult, fileScores, ...(livePreview ? { liveCanvas: livePreview } : {}),
|
|
111
|
+
usage: usageSummary, skippedFiles: collectedCtx?.skippedFiles || 0, skippedList: collectedCtx?.skippedList || [], durationMs,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { logger } from '../logger.js'
|
|
2
|
+
import { buildQuestionSynthesisPrompt, slotLabel } from '../moa-prompts.js'
|
|
3
|
+
import { callWithTransientRetry } from '../moa-candidates.js'
|
|
4
|
+
import { estimateTokenCost } from '../pricing.js'
|
|
5
|
+
import { cleanMoaWorkspaces } from '../workspace/fs.js'
|
|
6
|
+
import { recordMoaRunAsync, candidatesForHistory } from '../history.js'
|
|
7
|
+
|
|
8
|
+
export async function stageClarifyingQuestions({
|
|
9
|
+
userPrompt,
|
|
10
|
+
referenceOutputs,
|
|
11
|
+
primaryJudge,
|
|
12
|
+
aggTimeoutSec,
|
|
13
|
+
prices,
|
|
14
|
+
callLlm,
|
|
15
|
+
cwd,
|
|
16
|
+
runId,
|
|
17
|
+
preset,
|
|
18
|
+
isRefinement,
|
|
19
|
+
isFastMode,
|
|
20
|
+
collectedCtx,
|
|
21
|
+
startTime,
|
|
22
|
+
historyFilePath,
|
|
23
|
+
onProgress,
|
|
24
|
+
}) {
|
|
25
|
+
if (typeof onProgress === 'function') {
|
|
26
|
+
onProgress('📋 *Judge synthesizes the clarification questionnaire...*\n')
|
|
27
|
+
}
|
|
28
|
+
const questionPrompt = buildQuestionSynthesisPrompt(userPrompt, referenceOutputs)
|
|
29
|
+
let questionsContent = ''
|
|
30
|
+
let qUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
|
|
31
|
+
try {
|
|
32
|
+
const qRes = await callWithTransientRetry(callLlm, {
|
|
33
|
+
provider: primaryJudge.provider,
|
|
34
|
+
model: primaryJudge.model,
|
|
35
|
+
messages: [{ role: 'user', content: questionPrompt }],
|
|
36
|
+
temperature: 0.3,
|
|
37
|
+
maxTokens: 2048,
|
|
38
|
+
timeoutMs: aggTimeoutSec * 1000,
|
|
39
|
+
}, 1)
|
|
40
|
+
questionsContent = typeof qRes === 'string' ? qRes : (qRes?.content || qRes?.text || '')
|
|
41
|
+
const qFallbackUsage = (typeof qRes === 'object' && qRes?.usage) ? qRes.usage : {
|
|
42
|
+
inputTokens: Math.max(1, Math.round(questionPrompt.length / 4)),
|
|
43
|
+
outputTokens: Math.max(1, Math.round(questionsContent.length / 4)),
|
|
44
|
+
}
|
|
45
|
+
qUsage = estimateTokenCost(primaryJudge, qFallbackUsage, prices)
|
|
46
|
+
} catch (err) {
|
|
47
|
+
logger.warn('[dsh-moa] Questionnaire synthesis failed, proceeding with fallback questions:', err)
|
|
48
|
+
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.*`
|
|
49
|
+
}
|
|
50
|
+
await cleanMoaWorkspaces(cwd, { runId })
|
|
51
|
+
const totalTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0) + qUsage.totalTokens
|
|
52
|
+
const totalCostUsd = Number((referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + qUsage.costUsd).toFixed(5))
|
|
53
|
+
const durationMs = Date.now() - startTime
|
|
54
|
+
try {
|
|
55
|
+
await recordMoaRunAsync({
|
|
56
|
+
cwd,
|
|
57
|
+
prompt: userPrompt,
|
|
58
|
+
preset: preset?.name || 'default',
|
|
59
|
+
isRefinement,
|
|
60
|
+
candidates: candidatesForHistory(referenceOutputs),
|
|
61
|
+
aggregator: { provider: primaryJudge.provider, model: primaryJudge.model, usage: qUsage, costUsd: qUsage.costUsd },
|
|
62
|
+
winnerIndex: -1,
|
|
63
|
+
winnerModel: 'none',
|
|
64
|
+
promotedFiles: [],
|
|
65
|
+
totalTokens,
|
|
66
|
+
totalCostUsd,
|
|
67
|
+
durationMs,
|
|
68
|
+
}, historyFilePath || undefined)
|
|
69
|
+
} catch (histErr) {
|
|
70
|
+
logger.warn('[dsh-moa] Failed to record questionnaire run in history:', histErr?.message || histErr)
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
kind: 'questions', content: questionsContent, aggregator: slotLabel(primaryJudge),
|
|
74
|
+
references: referenceOutputs, presetName: preset?.name || 'default', isRefinement, isFastMode,
|
|
75
|
+
winningIndex: 0, winnerModel: 'none', promotedFiles: [],
|
|
76
|
+
usage: { totalTokens, totalCostUsd, candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })), aggregator: qUsage },
|
|
77
|
+
skippedFiles: collectedCtx?.skippedFiles || 0, skippedList: collectedCtx?.skippedList || [], durationMs,
|
|
78
|
+
}
|
|
79
|
+
}
|