@goodandready/dsh-moa 0.2.36 → 0.2.38
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/README.md +1 -1
- package/README.ru.md +1 -1
- package/README.zh.md +3 -0
- package/lib/file-workspace.js +195 -143
- package/lib/moa-candidates.js +54 -6
- package/lib/moa-llm.js +74 -6
- package/lib/moa-runner.js +38 -33
- package/lib/moa-stream.js +5 -3
- package/lib/moa-test-gate.js +94 -41
- package/lib/routes.js +5 -2
- package/package.json +1 -1
package/lib/moa-candidates.js
CHANGED
|
@@ -99,6 +99,7 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
const runOne = async () => {
|
|
102
|
+
let candidateTimer = null
|
|
102
103
|
try {
|
|
103
104
|
const callPromise = callWithTransientRetry(
|
|
104
105
|
callLlm,
|
|
@@ -115,14 +116,12 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
115
116
|
)
|
|
116
117
|
|
|
117
118
|
const timeoutPromise = new Promise((_, reject) => {
|
|
118
|
-
|
|
119
|
+
candidateTimer = setTimeout(() => {
|
|
119
120
|
bestEffort('candidate timeout abort', () => {
|
|
120
121
|
abortControllers[i].abort(new Error(`Timeout after ${Math.round(timeoutMs / 1000)}s`))
|
|
121
122
|
})
|
|
122
123
|
reject(new Error(`Timeout after ${Math.round(timeoutMs / 1000)}s`))
|
|
123
124
|
}, timeoutMs)
|
|
124
|
-
timer.unref?.()
|
|
125
|
-
abortControllers[i].signal.addEventListener('abort', () => clearTimeout(timer), { once: true })
|
|
126
125
|
})
|
|
127
126
|
|
|
128
127
|
const res = await Promise.race([callPromise, timeoutPromise])
|
|
@@ -153,16 +152,51 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
153
152
|
onProgress(`✅ *Candidate ${i + 1}/${total} (${label}, T=${effectiveTemperature}) finished${costStr}* | Live total: ~\$${currentTotalCost.toFixed(4)}\n`)
|
|
154
153
|
}
|
|
155
154
|
} catch (err) {
|
|
155
|
+
if (candidateTimer) clearTimeout(candidateTimer)
|
|
156
|
+
|
|
157
|
+
// If turn was aborted by caller, do NOT run fallbacks
|
|
158
|
+
if (options.signal?.aborted) {
|
|
159
|
+
finishedCount++
|
|
160
|
+
results[i] = {
|
|
161
|
+
index: i + 1,
|
|
162
|
+
slot,
|
|
163
|
+
label,
|
|
164
|
+
role_persona: role,
|
|
165
|
+
temperature: effectiveTemperature,
|
|
166
|
+
text: '[aborted]',
|
|
167
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
168
|
+
costUsd: 0,
|
|
169
|
+
ok: false,
|
|
170
|
+
error: 'Turn aborted',
|
|
171
|
+
}
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
156
175
|
// Feature 10: Local fallback models if online candidate fails
|
|
157
176
|
let fallbackRecovered = false
|
|
158
177
|
if (options.local_fallback_enabled && Array.isArray(options.local_fallback_models) && options.local_fallback_models.length > 0) {
|
|
159
178
|
for (const fbSlot of options.local_fallback_models) {
|
|
179
|
+
if (options.signal?.aborted) break
|
|
160
180
|
const fbLabel = slotLabel(fbSlot)
|
|
181
|
+
const fbAbortCtrl = new AbortController()
|
|
182
|
+
let fbTimer = null
|
|
183
|
+
let removeParentAbort = null
|
|
184
|
+
|
|
185
|
+
if (options.signal) {
|
|
186
|
+
const onParentAbort = () => fbAbortCtrl.abort(options.signal.reason || new Error('Turn aborted'))
|
|
187
|
+
options.signal.addEventListener('abort', onParentAbort, { once: true })
|
|
188
|
+
removeParentAbort = () => options.signal.removeEventListener('abort', onParentAbort)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const fbTimeoutMs = typeof options.fallbackTimeoutMs === 'number' && options.fallbackTimeoutMs > 0
|
|
192
|
+
? options.fallbackTimeoutMs
|
|
193
|
+
: Math.max(15000, timeoutMs)
|
|
194
|
+
|
|
161
195
|
try {
|
|
162
196
|
if (typeof onProgress === 'function') {
|
|
163
197
|
onProgress(`🔄 *Candidate ${i + 1} (${label}) failed, attempting local fallback to ${fbLabel}...*\n`)
|
|
164
198
|
}
|
|
165
|
-
const
|
|
199
|
+
const fbCallPromise = callWithTransientRetry(
|
|
166
200
|
callLlm,
|
|
167
201
|
{
|
|
168
202
|
provider: fbSlot.provider,
|
|
@@ -170,11 +204,22 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
170
204
|
messages: slotMessages,
|
|
171
205
|
temperature: effectiveTemperature,
|
|
172
206
|
maxTokens: options.maxTokens ?? 4096,
|
|
173
|
-
timeoutMs,
|
|
174
|
-
signal:
|
|
207
|
+
timeoutMs: fbTimeoutMs,
|
|
208
|
+
signal: fbAbortCtrl.signal,
|
|
175
209
|
},
|
|
176
210
|
0
|
|
177
211
|
)
|
|
212
|
+
|
|
213
|
+
const fbTimeoutPromise = new Promise((_, reject) => {
|
|
214
|
+
fbTimer = setTimeout(() => {
|
|
215
|
+
bestEffort('fallback timeout abort', () => {
|
|
216
|
+
fbAbortCtrl.abort(new Error(`Timeout after ${Math.round(fbTimeoutMs / 1000)}s`))
|
|
217
|
+
})
|
|
218
|
+
reject(new Error(`Timeout after ${Math.round(fbTimeoutMs / 1000)}s`))
|
|
219
|
+
}, fbTimeoutMs)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const fbRes = await Promise.race([fbCallPromise, fbTimeoutPromise])
|
|
178
223
|
const fbText = typeof fbRes === 'string' ? fbRes : (fbRes?.content || fbRes?.text || '')
|
|
179
224
|
const fbUsage = (typeof fbRes === 'object' && fbRes?.usage) ? fbRes.usage : {
|
|
180
225
|
inputTokens: Math.max(1, Math.round(slotMessages.map((m) => m.content).join('').length / 4)),
|
|
@@ -202,6 +247,9 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
202
247
|
break
|
|
203
248
|
} catch (fbErr) {
|
|
204
249
|
logger.warn(`[dsh-moa] Fallback ${fbLabel} also failed:`, fbErr?.message || fbErr)
|
|
250
|
+
} finally {
|
|
251
|
+
if (fbTimer) clearTimeout(fbTimer)
|
|
252
|
+
if (removeParentAbort) removeParentAbort()
|
|
205
253
|
}
|
|
206
254
|
}
|
|
207
255
|
}
|
package/lib/moa-llm.js
CHANGED
|
@@ -1,17 +1,49 @@
|
|
|
1
|
+
function combineSignals(signal, timeoutSignal) {
|
|
2
|
+
if (!signal) return timeoutSignal
|
|
3
|
+
if (typeof AbortSignal.any === 'function') {
|
|
4
|
+
try {
|
|
5
|
+
return AbortSignal.any([signal, timeoutSignal])
|
|
6
|
+
} catch {
|
|
7
|
+
// fallback if signal is not an instance of AbortSignal
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const combinedCtrl = new AbortController()
|
|
11
|
+
const onAbort = () => {
|
|
12
|
+
const reason = signal.aborted ? (signal.reason || new Error('Turn aborted')) : timeoutSignal.reason
|
|
13
|
+
combinedCtrl.abort(reason)
|
|
14
|
+
}
|
|
15
|
+
if (signal.aborted || timeoutSignal.aborted) {
|
|
16
|
+
onAbort()
|
|
17
|
+
return combinedCtrl.signal
|
|
18
|
+
}
|
|
19
|
+
signal.addEventListener?.('abort', onAbort, { once: true })
|
|
20
|
+
timeoutSignal.addEventListener?.('abort', onAbort, { once: true })
|
|
21
|
+
return combinedCtrl.signal
|
|
22
|
+
}
|
|
23
|
+
|
|
1
24
|
/**
|
|
2
25
|
* Unified LLM Dispatcher using ctx.llm.prepareCall / stream
|
|
3
26
|
*/
|
|
4
27
|
|
|
5
28
|
export function createLlmCaller(ctx) {
|
|
6
|
-
return async ({ provider, model, messages, temperature = 0.6, maxTokens = 4096, timeoutMs, onStreamDelta }) => {
|
|
29
|
+
return async ({ provider, model, messages, temperature = 0.6, maxTokens = 4096, timeoutMs, onStreamDelta, signal, stop, reasoningEffort }) => {
|
|
7
30
|
if (!ctx.llm) {
|
|
8
31
|
throw new Error('ctx.llm is not available in cordis context')
|
|
9
32
|
}
|
|
10
33
|
|
|
34
|
+
const callConfig = {
|
|
35
|
+
provider,
|
|
36
|
+
model,
|
|
37
|
+
...(temperature !== undefined ? { temperature } : {}),
|
|
38
|
+
...(maxTokens !== undefined ? { maxTokens } : {}),
|
|
39
|
+
...(stop !== undefined ? { stop } : {}),
|
|
40
|
+
...(reasoningEffort !== undefined ? { reasoningEffort } : {}),
|
|
41
|
+
}
|
|
42
|
+
|
|
11
43
|
let prep
|
|
12
44
|
try {
|
|
13
|
-
// DSH 0.
|
|
14
|
-
prep = await ctx.llm.prepareCall(
|
|
45
|
+
// DSH 0.2.0-rc.2 API expects LlmCallConfig object, supports signal
|
|
46
|
+
prep = await ctx.llm.prepareCall(callConfig, signal)
|
|
15
47
|
} catch (err) {
|
|
16
48
|
if (typeof ctx.llm.prepareCall === 'function') {
|
|
17
49
|
try {
|
|
@@ -27,23 +59,33 @@ export function createLlmCaller(ctx) {
|
|
|
27
59
|
throw new Error(`LLM provider/model ${provider}:${model} could not be prepared`)
|
|
28
60
|
}
|
|
29
61
|
|
|
62
|
+
if (signal?.aborted) {
|
|
63
|
+
throw (signal.reason || new Error('Turn aborted'))
|
|
64
|
+
}
|
|
65
|
+
|
|
30
66
|
const effectiveTimeoutMs = typeof timeoutMs === 'number' && timeoutMs > 0 ? timeoutMs : 120000
|
|
31
67
|
const abortCtrl = new AbortController()
|
|
32
68
|
const timeoutId = setTimeout(() => abortCtrl.abort(new Error(`LLM call timeout after ${Math.round(effectiveTimeoutMs / 1000)}s`)), effectiveTimeoutMs)
|
|
33
69
|
timeoutId.unref?.()
|
|
70
|
+
const combinedSignal = combineSignals(signal, abortCtrl.signal)
|
|
34
71
|
|
|
35
72
|
try {
|
|
73
|
+
const streamConfig = prep.config || callConfig
|
|
36
74
|
const stream = prep.stream({
|
|
75
|
+
...streamConfig,
|
|
37
76
|
messages,
|
|
38
|
-
temperature,
|
|
39
|
-
maxTokens,
|
|
40
|
-
signal:
|
|
77
|
+
temperature: streamConfig.temperature ?? temperature,
|
|
78
|
+
maxTokens: streamConfig.maxTokens ?? maxTokens,
|
|
79
|
+
signal: combinedSignal,
|
|
41
80
|
})
|
|
42
81
|
|
|
43
82
|
let fullText = ''
|
|
44
83
|
let usageInfo = null
|
|
45
84
|
|
|
46
85
|
for await (const chunk of stream) {
|
|
86
|
+
if (combinedSignal?.aborted) {
|
|
87
|
+
throw (combinedSignal.reason || new Error('Turn aborted'))
|
|
88
|
+
}
|
|
47
89
|
if (chunk.type === 'text-delta' && typeof chunk.text === 'string') {
|
|
48
90
|
fullText += chunk.text
|
|
49
91
|
if (typeof onStreamDelta === 'function') {
|
|
@@ -53,9 +95,35 @@ export function createLlmCaller(ctx) {
|
|
|
53
95
|
if (!fullText) fullText = chunk.block.text
|
|
54
96
|
} else if (chunk.type === 'usage' && chunk.usage) {
|
|
55
97
|
usageInfo = chunk.usage
|
|
98
|
+
} else if (chunk.type === 'finish') {
|
|
99
|
+
const reason = chunk.reason
|
|
100
|
+
if (reason && (reason.kind === 'error' || reason.kind === 'aborted')) {
|
|
101
|
+
const failure = reason.failure || {}
|
|
102
|
+
const errMsg = failure.message || failure.error?.message || reason.message || reason.error?.message || `LLM stream finished with ${reason.kind}`
|
|
103
|
+
const err = new Error(errMsg)
|
|
104
|
+
err.code = failure.code || reason.code || (reason.kind === 'error' ? 'LLM_ERROR' : 'ABORT_ERR')
|
|
105
|
+
if (failure.cause) err.cause = failure.cause
|
|
106
|
+
throw err
|
|
107
|
+
}
|
|
108
|
+
} else if (chunk.type === 'error') {
|
|
109
|
+
const errMsg = chunk.error?.message || chunk.message || 'LLM stream error'
|
|
110
|
+
const err = new Error(errMsg)
|
|
111
|
+
err.code = chunk.code || chunk.error?.code || 'LLM_ERROR'
|
|
112
|
+
if (chunk.error?.cause) err.cause = chunk.error.cause
|
|
113
|
+
throw err
|
|
56
114
|
}
|
|
57
115
|
}
|
|
58
116
|
|
|
117
|
+
if (combinedSignal?.aborted) {
|
|
118
|
+
throw (combinedSignal.reason || new Error('Turn aborted'))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!fullText.trim()) {
|
|
122
|
+
const err = new Error('LLM call returned empty response')
|
|
123
|
+
err.code = 'EMPTY_RESPONSE'
|
|
124
|
+
throw err
|
|
125
|
+
}
|
|
126
|
+
|
|
59
127
|
clearTimeout(timeoutId)
|
|
60
128
|
return {
|
|
61
129
|
content: fullText,
|
package/lib/moa-runner.js
CHANGED
|
@@ -12,7 +12,6 @@ import { executeMultiJudgePanel, buildCompositeBlockDirectives } from './moa-mul
|
|
|
12
12
|
import { applyBudgetGuardrails } from './moa-budget.js'
|
|
13
13
|
import { extractPriorTurnBaseline, pruneMultiTurnMessages } from './moa-context.js'
|
|
14
14
|
import { generateMoABenchmarkReport } from './moa-report.js'
|
|
15
|
-
|
|
16
15
|
// Re-exports for consumers & backward compatibility
|
|
17
16
|
export { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildCuratorSynthesisPrompt, buildSynthesisPrompt, ANTIPATTERNS_RUBRIC } from './moa-prompts.js'
|
|
18
17
|
export { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
|
|
@@ -20,7 +19,6 @@ export { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
|
|
|
20
19
|
export { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel } from './moa-candidates.js'
|
|
21
20
|
export { executeTestGateForCandidates, runCandidateTestGate } from './moa-test-gate.js'
|
|
22
21
|
export { streamMoATurn } from './moa-stream.js'
|
|
23
|
-
|
|
24
22
|
/**
|
|
25
23
|
* Executes the full Mixture of Agents pipeline.
|
|
26
24
|
*/
|
|
@@ -45,6 +43,7 @@ export async function runMoAPipeline({
|
|
|
45
43
|
force = isTestEnvironment() ? true : false,
|
|
46
44
|
checkpoint = true,
|
|
47
45
|
checkpointFn = null,
|
|
46
|
+
runId: customRunId = null,
|
|
48
47
|
}) {
|
|
49
48
|
const promoteOpts = {
|
|
50
49
|
webServerPort,
|
|
@@ -56,16 +55,13 @@ export async function runMoAPipeline({
|
|
|
56
55
|
}
|
|
57
56
|
if (signal?.aborted) return { error: new Error('Turn aborted') }
|
|
58
57
|
const startTime = Date.now()
|
|
59
|
-
|
|
60
58
|
// 1. Resolve configurations
|
|
61
59
|
const referenceModels = Array.isArray(preset?.reference_models) && preset.reference_models.length > 0
|
|
62
60
|
? preset.reference_models : [{ provider: '', model: '' }]
|
|
63
61
|
const primaryJudge = preset?.aggregator?.provider && preset?.aggregator?.model
|
|
64
62
|
? preset.aggregator : { provider: '', model: '' }
|
|
65
|
-
|
|
66
63
|
const fallbackJudges = Array.isArray(preset?.aggregator_fallbacks) ? preset.aggregator_fallbacks : []
|
|
67
64
|
const judgesChain = [primaryJudge, ...fallbackJudges]
|
|
68
|
-
|
|
69
65
|
const refTemp = typeof preset?.reference_temperature === 'number' ? preset.reference_temperature : 0.6
|
|
70
66
|
const aggTemp = typeof preset?.aggregator_temperature === 'number' ? preset.aggregator_temperature : 0.4
|
|
71
67
|
const maxTokens = typeof preset?.max_tokens === 'number' ? preset.max_tokens : 4096
|
|
@@ -76,8 +72,7 @@ export async function runMoAPipeline({
|
|
|
76
72
|
const candidateRetries = 1, refTimeoutSec = typeof preset?.reference_timeout_sec === 'number' ? preset.reference_timeout_sec : 60
|
|
77
73
|
const aggTimeoutSec = typeof preset?.aggregator_timeout_sec === 'number' ? preset.aggregator_timeout_sec : 180
|
|
78
74
|
const isBlindEvaluation = Boolean(preset?.blind_evaluation), isPeerCritiqueEnabled = Boolean(preset?.peer_critique_enabled)
|
|
79
|
-
const allowCandidateOverride = Boolean(preset?.allow_candidate_override), runId =
|
|
80
|
-
|
|
75
|
+
const allowCandidateOverride = Boolean(preset?.allow_candidate_override), runId = customRunId || null
|
|
81
76
|
// Feature 6: Budget Guardrail check
|
|
82
77
|
const budgetCheck = applyBudgetGuardrails({
|
|
83
78
|
references: referenceModels,
|
|
@@ -109,7 +104,6 @@ export async function runMoAPipeline({
|
|
|
109
104
|
if (budgetCheck.action === 'trim' && typeof onProgress === 'function') {
|
|
110
105
|
onProgress(`✂️ *${budgetCheck.reason}*\n`)
|
|
111
106
|
}
|
|
112
|
-
|
|
113
107
|
// 2. Collect project context for refinement tasks
|
|
114
108
|
const collectedCtx = await collectProjectContext(cwd, 16000)
|
|
115
109
|
const isRefinement = Boolean(collectedCtx?.files?.length > 0 && isRefinementTask(userPrompt, collectedCtx.files))
|
|
@@ -128,19 +122,15 @@ export async function runMoAPipeline({
|
|
|
128
122
|
skippedList: collectedCtx.skippedList,
|
|
129
123
|
})
|
|
130
124
|
}
|
|
131
|
-
|
|
132
125
|
// 3. Build prompts & Feature 8 multi-turn context
|
|
133
126
|
const priorTurnBaseline = preset?.multi_turn_enabled !== false ? extractPriorTurnBaseline(messages) : null
|
|
134
127
|
const prunedHistory = preset?.multi_turn_enabled !== false ? pruneMultiTurnMessages(messages) : messages
|
|
135
|
-
|
|
136
128
|
const candidateSystemPrompt = projectContext
|
|
137
129
|
? `${REFERENCE_SYSTEM_PROMPT}\n\n### Current Project Files & Context:\n${projectContext}`
|
|
138
130
|
: REFERENCE_SYSTEM_PROMPT
|
|
139
|
-
|
|
140
131
|
const askClarifyingQuestions = preset?.ask_clarifying_questions !== false
|
|
141
132
|
const needsQuestions = askClarifyingQuestions && isBroadPromptRequiringQuestions(userPrompt, messages)
|
|
142
133
|
const enrichedMessages = [...prunedHistory, { role: 'user', content: userPrompt }]
|
|
143
|
-
|
|
144
134
|
// 4. Parallel fan-out to candidate models with temperature gradient & local fallback
|
|
145
135
|
const referenceOutputs = await runReferencesParallel(
|
|
146
136
|
effectiveReferences,
|
|
@@ -162,12 +152,10 @@ export async function runMoAPipeline({
|
|
|
162
152
|
callLlm,
|
|
163
153
|
onProgress
|
|
164
154
|
)
|
|
165
|
-
|
|
166
155
|
if (signal?.aborted) {
|
|
167
|
-
await cleanMoaWorkspaces(cwd)
|
|
156
|
+
await cleanMoaWorkspaces(cwd, { runId })
|
|
168
157
|
return { error: new Error('Turn aborted') }
|
|
169
158
|
}
|
|
170
|
-
|
|
171
159
|
// Fail fast if all candidates failed
|
|
172
160
|
const successfulRefs = referenceOutputs.filter((r) => r.ok)
|
|
173
161
|
if (successfulRefs.length === 0) {
|
|
@@ -194,7 +182,6 @@ export async function runMoAPipeline({
|
|
|
194
182
|
durationMs: Date.now() - startTime,
|
|
195
183
|
}
|
|
196
184
|
}
|
|
197
|
-
|
|
198
185
|
// 5. Extract file blocks & write candidate workspaces
|
|
199
186
|
for (let i = 0; i < referenceOutputs.length; i++) {
|
|
200
187
|
const ref = referenceOutputs[i]
|
|
@@ -203,19 +190,21 @@ export async function runMoAPipeline({
|
|
|
203
190
|
ref.files = files
|
|
204
191
|
if (files.length > 0) {
|
|
205
192
|
ref.syntaxWarning = (verifyFileSyntax(files) || []).map((w) => `${w.file}: ${w.error}`).join('; ')
|
|
206
|
-
await writeCandidateWorkspace(cwd, i + 1, files)
|
|
193
|
+
await writeCandidateWorkspace(cwd, i + 1, files, { runId })
|
|
207
194
|
}
|
|
208
195
|
}
|
|
209
|
-
|
|
196
|
+
if (signal?.aborted) {
|
|
197
|
+
await cleanMoaWorkspaces(cwd, { runId })
|
|
198
|
+
return { error: new Error('Turn aborted') }
|
|
199
|
+
}
|
|
210
200
|
// 5b. Test Execution Gate
|
|
211
201
|
await executeTestGateForCandidates({
|
|
212
202
|
cwd,
|
|
213
203
|
referenceOutputs,
|
|
214
204
|
preset,
|
|
215
|
-
options: { testGateEnabled, testCommand, execFn },
|
|
205
|
+
options: { testGateEnabled, testCommand, execFn, runId },
|
|
216
206
|
onProgress,
|
|
217
207
|
})
|
|
218
|
-
|
|
219
208
|
// 6. Questionnaire synthesis branch
|
|
220
209
|
if (needsQuestions) {
|
|
221
210
|
if (typeof onProgress === 'function') {
|
|
@@ -243,12 +232,10 @@ export async function runMoAPipeline({
|
|
|
243
232
|
logger.warn('[dsh-moa] Questionnaire synthesis failed, proceeding with fallback questions:', err)
|
|
244
233
|
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.*`
|
|
245
234
|
}
|
|
246
|
-
|
|
247
|
-
await cleanMoaWorkspaces(cwd)
|
|
235
|
+
await cleanMoaWorkspaces(cwd, { runId })
|
|
248
236
|
const totalTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0) + qUsage.totalTokens
|
|
249
237
|
const totalCostUsd = Number((referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + qUsage.costUsd).toFixed(5))
|
|
250
238
|
const durationMs = Date.now() - startTime
|
|
251
|
-
|
|
252
239
|
try {
|
|
253
240
|
await recordMoaRunAsync({
|
|
254
241
|
prompt: userPrompt,
|
|
@@ -266,7 +253,6 @@ export async function runMoAPipeline({
|
|
|
266
253
|
} catch (histErr) {
|
|
267
254
|
logger.warn('[dsh-moa] Failed to record questionnaire run in history:', histErr?.message || histErr)
|
|
268
255
|
}
|
|
269
|
-
|
|
270
256
|
return {
|
|
271
257
|
kind: 'questions',
|
|
272
258
|
content: questionsContent,
|
|
@@ -292,12 +278,16 @@ export async function runMoAPipeline({
|
|
|
292
278
|
|
|
293
279
|
// 7. Fast Mode (1 candidate, bypass judge)
|
|
294
280
|
if (isFastMode) {
|
|
281
|
+
if (signal?.aborted) {
|
|
282
|
+
await cleanMoaWorkspaces(cwd, { runId })
|
|
283
|
+
return { error: new Error('Turn aborted') }
|
|
284
|
+
}
|
|
295
285
|
const single = successfulRefs[0]
|
|
296
286
|
let promotedFiles = []
|
|
297
287
|
if (single.files?.length > 0) {
|
|
298
|
-
promotedFiles = await promoteCandidateWorkspace(cwd, single.index, promoteOpts)
|
|
288
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, single.index, { ...promoteOpts, runId })
|
|
299
289
|
} else {
|
|
300
|
-
await cleanMoaWorkspaces(cwd)
|
|
290
|
+
await cleanMoaWorkspaces(cwd, { runId })
|
|
301
291
|
}
|
|
302
292
|
|
|
303
293
|
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs)
|
|
@@ -348,6 +338,11 @@ export async function runMoAPipeline({
|
|
|
348
338
|
}
|
|
349
339
|
}
|
|
350
340
|
|
|
341
|
+
if (signal?.aborted) {
|
|
342
|
+
await cleanMoaWorkspaces(cwd, { runId })
|
|
343
|
+
return { error: new Error('Turn aborted') }
|
|
344
|
+
}
|
|
345
|
+
|
|
351
346
|
// 7b. Consilium Round 2 (Peer Critique) if enabled
|
|
352
347
|
if (isPeerCritiqueEnabled && successfulRefs.length > 1) {
|
|
353
348
|
if (typeof onProgress === 'function') {
|
|
@@ -379,7 +374,7 @@ export async function runMoAPipeline({
|
|
|
379
374
|
if (r2Files.length > 0) {
|
|
380
375
|
cand.files = r2Files
|
|
381
376
|
cand.syntaxWarning = (verifyFileSyntax(r2Files) || []).map((w) => `${w.file}: ${w.error}`).join('; ')
|
|
382
|
-
await writeCandidateWorkspace(cwd, cand.index, r2Files)
|
|
377
|
+
await writeCandidateWorkspace(cwd, cand.index, r2Files, { runId })
|
|
383
378
|
}
|
|
384
379
|
if (typeof res === 'object' && res?.usage) {
|
|
385
380
|
cand.usage.inputTokens = (cand.usage.inputTokens || 0) + (res.usage.inputTokens || 0)
|
|
@@ -397,13 +392,18 @@ export async function runMoAPipeline({
|
|
|
397
392
|
cwd,
|
|
398
393
|
referenceOutputs: successfulRefs,
|
|
399
394
|
preset,
|
|
400
|
-
options: { testGateEnabled, testCommand, execFn },
|
|
395
|
+
options: { testGateEnabled, testCommand, execFn, runId },
|
|
401
396
|
onProgress,
|
|
402
397
|
})
|
|
403
398
|
}
|
|
404
399
|
|
|
405
400
|
// 7c. Feature 2: Multi-Judge Panel & Consensus Voting
|
|
406
401
|
let multiJudgeResult = null
|
|
402
|
+
if (signal?.aborted) {
|
|
403
|
+
await cleanMoaWorkspaces(cwd, { runId })
|
|
404
|
+
return { error: new Error('Turn aborted') }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
407
|
if (preset?.multi_judge_enabled && successfulRefs.length > 1) {
|
|
408
408
|
multiJudgeResult = await executeMultiJudgePanel({
|
|
409
409
|
judges: (preset.judge_models && preset.judge_models.length > 0) ? preset.judge_models : [primaryJudge],
|
|
@@ -488,6 +488,11 @@ export async function runMoAPipeline({
|
|
|
488
488
|
successfulRefs.map((r, i) => `### Candidate ${i + 1} (${r.label})\n${r.text}`).join('\n\n')
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
+
if (signal?.aborted) {
|
|
492
|
+
await cleanMoaWorkspaces(cwd, { runId })
|
|
493
|
+
return { error: new Error('Turn aborted') }
|
|
494
|
+
}
|
|
495
|
+
|
|
491
496
|
// 9. Evaluate winner & promote files
|
|
492
497
|
const winningIndex = (multiJudgeResult?.consensus && multiJudgeResult?.winningCandidateIndex)
|
|
493
498
|
? multiJudgeResult.winningCandidateIndex
|
|
@@ -503,15 +508,15 @@ export async function runMoAPipeline({
|
|
|
503
508
|
|
|
504
509
|
const keepWorkspaces = allowCandidateOverride
|
|
505
510
|
if (synthesizedFiles.length > 0) {
|
|
506
|
-
await writeCandidateWorkspace(cwd, 'curator-synthesis', synthesizedFiles)
|
|
507
|
-
promotedFiles = await promoteCandidateWorkspace(cwd, 'curator-synthesis', { ...promoteOpts, keepMoa: keepWorkspaces })
|
|
511
|
+
await writeCandidateWorkspace(cwd, 'curator-synthesis', synthesizedFiles, { runId })
|
|
512
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, 'curator-synthesis', { ...promoteOpts, runId, keepMoa: keepWorkspaces })
|
|
508
513
|
} else if (referenceOutputs[winningIndex - 1]?.files?.length > 0) {
|
|
509
|
-
promotedFiles = await promoteCandidateWorkspace(cwd, winningIndex, { ...promoteOpts, keepMoa: keepWorkspaces })
|
|
514
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, winningIndex, { ...promoteOpts, runId, keepMoa: keepWorkspaces })
|
|
510
515
|
} else if (successfulRefs[0]?.files?.length > 0) {
|
|
511
516
|
const fallbackIdx = successfulRefs[0].index
|
|
512
|
-
promotedFiles = await promoteCandidateWorkspace(cwd, fallbackIdx, { ...promoteOpts, keepMoa: keepWorkspaces })
|
|
517
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, fallbackIdx, { ...promoteOpts, runId, keepMoa: keepWorkspaces })
|
|
513
518
|
} else if (!keepWorkspaces) {
|
|
514
|
-
await cleanMoaWorkspaces(cwd)
|
|
519
|
+
await cleanMoaWorkspaces(cwd, { runId })
|
|
515
520
|
}
|
|
516
521
|
|
|
517
522
|
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs)
|
package/lib/moa-stream.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
1
2
|
/**
|
|
2
3
|
* Zero-latency Live Streaming Generator for MoA Turns
|
|
3
4
|
*/
|
|
@@ -11,6 +12,7 @@ import { runMoAPipeline } from './moa-runner.js'
|
|
|
11
12
|
*/
|
|
12
13
|
export async function* streamMoATurn({ targetPreset, userPrompt, messages, callLlm, cwd, prices, historyFilePath, liveCanvas, webServerPort, timeMachineEngine, force }, options = {}) {
|
|
13
14
|
const signal = options?.signal
|
|
15
|
+
const runId = options?.runId || null
|
|
14
16
|
if (signal?.aborted) return
|
|
15
17
|
|
|
16
18
|
const internalAbortCtrl = new AbortController()
|
|
@@ -69,7 +71,7 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
|
|
|
69
71
|
|
|
70
72
|
while (!done || queue.length > 0) {
|
|
71
73
|
if (combinedSignal.aborted) {
|
|
72
|
-
await cleanMoaWorkspaces(cwd)
|
|
74
|
+
await cleanMoaWorkspaces(cwd, runId ? { runId } : {})
|
|
73
75
|
return
|
|
74
76
|
}
|
|
75
77
|
|
|
@@ -96,7 +98,7 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
|
|
|
96
98
|
|
|
97
99
|
const result = await pipelinePromise
|
|
98
100
|
if (combinedSignal.aborted) {
|
|
99
|
-
await cleanMoaWorkspaces(cwd)
|
|
101
|
+
await cleanMoaWorkspaces(cwd, runId ? { runId } : {})
|
|
100
102
|
return
|
|
101
103
|
}
|
|
102
104
|
|
|
@@ -126,7 +128,7 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
|
|
|
126
128
|
} finally {
|
|
127
129
|
internalAbortCtrl.abort()
|
|
128
130
|
if (!done) {
|
|
129
|
-
await cleanMoaWorkspaces(cwd).catch(() => {})
|
|
131
|
+
await cleanMoaWorkspaces(cwd, runId ? { runId } : {}).catch(() => {})
|
|
130
132
|
}
|
|
131
133
|
}
|
|
132
134
|
}
|