@goodandready/dsh-moa 0.2.26 → 0.2.29
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 +30 -0
- package/README.ru.md +30 -0
- package/lib/client.js +194 -0
- package/lib/file-workspace.js +5 -4
- package/lib/history.js +12 -6
- package/lib/index.js +39 -1
- package/lib/logger.js +40 -0
- package/lib/moa-budget.js +97 -0
- package/lib/moa-candidates.js +91 -22
- package/lib/moa-context.js +70 -0
- package/lib/moa-multi-judge.js +301 -0
- package/lib/moa-prompts.js +3 -0
- package/lib/moa-report.js +124 -0
- package/lib/moa-router.js +102 -0
- package/lib/moa-runner.js +108 -127
- package/lib/moa-stream.js +117 -0
- package/lib/moa-test-gate.js +4 -3
- package/lib/routes.js +92 -7
- package/package.json +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Preset Router for Mixture of Agents (MoA).
|
|
3
|
+
* Supports heuristic keyword classification and optional JEV/LLM zero-shot routing.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const INTENT_RULES = [
|
|
7
|
+
{ preset: 'security-audit', regex: /\b(security|vulnerability|exploit|cve|xss|injection|sanitize|threat|auth|permission|secret)\b/i },
|
|
8
|
+
{ preset: 'bug-hunter', regex: /\b(bug|fix|error|crash|exception|regression|reproduce|broken|fail|issue)\b/i },
|
|
9
|
+
{ preset: 'refactor-cleanup', regex: /\b(refactor|clean|simplify|ponytail|yagni|prune|dedup|modular|dead\s*code)\b/i },
|
|
10
|
+
{ preset: 'code-review', regex: /\b(review|critique|audit|pr|pull\s*request|diff|inspect|check\s*code)\b/i },
|
|
11
|
+
{ preset: 'frontend-ui', regex: /\b(frontend|ui|css|html|react|vue|tailwind|styling|styles?|stylesheet|component|button|layout|page|landing|modal)\b/i },
|
|
12
|
+
{ preset: 'deep-architect', regex: /\b(architect|distributed|system|microservice|database|scalab|pipeline|infra|schema)\b/i },
|
|
13
|
+
{ preset: 'math-logic', regex: /\b(math|algorithm|proof|matrix|calc|complexity|combinatorics|graph|tree|dynamic\s*programming)\b/i },
|
|
14
|
+
{ preset: 'creative-brainstorm', regex: /\b(brainstorm|idea|concept|creative|alternative|options|feature\s*idea)\b/i },
|
|
15
|
+
{ preset: 'fast-audit', regex: /\b(quick|fast|brief|summary|one-line|short|tldr)\b/i },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fast keyword-based intent classification.
|
|
20
|
+
*/
|
|
21
|
+
export function classifyPromptIntent(prompt = '') {
|
|
22
|
+
if (!prompt || typeof prompt !== 'string') return null
|
|
23
|
+
const text = prompt.trim()
|
|
24
|
+
for (const rule of INTENT_RULES) {
|
|
25
|
+
if (rule.regex.test(text)) {
|
|
26
|
+
return rule.preset
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolves the optimal preset for a given user prompt.
|
|
34
|
+
*/
|
|
35
|
+
export async function resolvePresetForPrompt({
|
|
36
|
+
prompt = '',
|
|
37
|
+
presets = [],
|
|
38
|
+
defaultPreset = 'default',
|
|
39
|
+
routingModel = null,
|
|
40
|
+
callLlm = null,
|
|
41
|
+
enabled = false,
|
|
42
|
+
timeoutMs = 2000,
|
|
43
|
+
}) {
|
|
44
|
+
const availableNames = (presets || []).map((p) => p.name).filter(Boolean)
|
|
45
|
+
const safeDefault = availableNames.includes(defaultPreset) ? defaultPreset : (availableNames[0] || 'default')
|
|
46
|
+
|
|
47
|
+
if (!enabled || !prompt || typeof prompt !== 'string') {
|
|
48
|
+
return { presetName: safeDefault, reason: 'default', isAutoRouted: false }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 1. If LLM routing model (e.g. JEV) is configured and callable, run fast zero-shot classifier
|
|
52
|
+
if (routingModel?.provider && routingModel?.model && typeof callLlm === 'function') {
|
|
53
|
+
try {
|
|
54
|
+
const systemPrompt = `You are the MoA Preset Router. Given user prompt, classify which preset is best suited.
|
|
55
|
+
Available presets: ${availableNames.join(', ')}
|
|
56
|
+
Respond with ONLY the chosen preset name and nothing else.`
|
|
57
|
+
|
|
58
|
+
const abortCtrl = new AbortController()
|
|
59
|
+
const timer = setTimeout(() => abortCtrl.abort(), timeoutMs)
|
|
60
|
+
timer.unref?.()
|
|
61
|
+
|
|
62
|
+
const res = await callLlm({
|
|
63
|
+
provider: routingModel.provider,
|
|
64
|
+
model: routingModel.model,
|
|
65
|
+
messages: [
|
|
66
|
+
{ role: 'system', content: systemPrompt },
|
|
67
|
+
{ role: 'user', content: prompt.slice(0, 500) },
|
|
68
|
+
],
|
|
69
|
+
temperature: 0.1,
|
|
70
|
+
maxTokens: 30,
|
|
71
|
+
signal: abortCtrl.signal,
|
|
72
|
+
})
|
|
73
|
+
clearTimeout(timer)
|
|
74
|
+
|
|
75
|
+
const rawChoice = typeof res === 'string' ? res : (res?.content || res?.text || '')
|
|
76
|
+
const match = availableNames.find((name) => new RegExp(`\\b${name}\\b`, 'i').test(rawChoice))
|
|
77
|
+
if (match) {
|
|
78
|
+
return {
|
|
79
|
+
presetName: match,
|
|
80
|
+
reason: 'llm_classifier',
|
|
81
|
+
routerModel: `${routingModel.provider}:${routingModel.model}`,
|
|
82
|
+
isAutoRouted: true,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// Fallback silently to heuristic rules on timeout or network error
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 2. Keyword heuristic classifier
|
|
91
|
+
const heuristic = classifyPromptIntent(prompt)
|
|
92
|
+
if (heuristic && availableNames.includes(heuristic)) {
|
|
93
|
+
return {
|
|
94
|
+
presetName: heuristic,
|
|
95
|
+
reason: 'keyword_heuristic',
|
|
96
|
+
isAutoRouted: true,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 3. Fallback to default
|
|
101
|
+
return { presetName: safeDefault, reason: 'default_fallback', isAutoRouted: false }
|
|
102
|
+
}
|
package/lib/moa-runner.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
import { logger } from './logger.js'
|
|
1
2
|
import path from 'node:path'
|
|
2
3
|
import crypto from 'node:crypto'
|
|
3
4
|
import { bestEffort } from './best-effort.js'
|
|
4
5
|
import { extractFileBlocks, collectProjectContext, formatProjectContext, isRefinementTask, writeCandidateWorkspace, promoteCandidateWorkspace, cleanMoaWorkspaces, verifyFileSyntax } from './file-workspace.js'
|
|
5
6
|
import { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
|
|
6
|
-
import {
|
|
7
|
+
import { recordMoaRunAsync, candidatesForHistory } from './history.js'
|
|
7
8
|
import { createPromotedPreview } from './live-canvas.js'
|
|
8
9
|
import { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildCuratorSynthesisPrompt, buildSynthesisPrompt, buildPeerCritiquePrompt, ROLE_PERSONA_PROMPTS, SYSTEM_ROLE_PROPOSER, ANTIPATTERNS_RUBRIC } from './moa-prompts.js'
|
|
9
10
|
import { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
|
|
10
11
|
import { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel } from './moa-candidates.js'
|
|
11
12
|
import { executeTestGateForCandidates, runCandidateTestGate } from './moa-test-gate.js'
|
|
13
|
+
import { executeMultiJudgePanel, buildCompositeBlockDirectives } from './moa-multi-judge.js'
|
|
14
|
+
import { applyBudgetGuardrails } from './moa-budget.js'
|
|
15
|
+
import { extractPriorTurnBaseline, pruneMultiTurnMessages } from './moa-context.js'
|
|
16
|
+
import { generateMoABenchmarkReport } from './moa-report.js'
|
|
12
17
|
|
|
13
18
|
// Re-exports for consumers & backward compatibility
|
|
14
19
|
export { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildCuratorSynthesisPrompt, buildSynthesisPrompt, ANTIPATTERNS_RUBRIC } from './moa-prompts.js'
|
|
@@ -16,6 +21,7 @@ export { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSu
|
|
|
16
21
|
export { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
|
|
17
22
|
export { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel } from './moa-candidates.js'
|
|
18
23
|
export { executeTestGateForCandidates, runCandidateTestGate } from './moa-test-gate.js'
|
|
24
|
+
export { streamMoATurn } from './moa-stream.js'
|
|
19
25
|
|
|
20
26
|
/**
|
|
21
27
|
* Executes the full Mixture of Agents pipeline.
|
|
@@ -60,6 +66,38 @@ export async function runMoAPipeline({
|
|
|
60
66
|
const isBlindEvaluation = Boolean(preset?.blind_evaluation), isPeerCritiqueEnabled = Boolean(preset?.peer_critique_enabled)
|
|
61
67
|
const allowCandidateOverride = Boolean(preset?.allow_candidate_override), runId = crypto.randomUUID()
|
|
62
68
|
|
|
69
|
+
// Feature 6: Budget Guardrail check
|
|
70
|
+
const budgetCheck = applyBudgetGuardrails({
|
|
71
|
+
references: referenceModels,
|
|
72
|
+
enabled: preset?.budget_guard_enabled,
|
|
73
|
+
maxBudgetUsd: preset?.max_budget_usd,
|
|
74
|
+
action: preset?.budget_action || 'trim',
|
|
75
|
+
prices,
|
|
76
|
+
promptLength: userPrompt?.length || 1000,
|
|
77
|
+
})
|
|
78
|
+
if (!budgetCheck.allowed) {
|
|
79
|
+
return {
|
|
80
|
+
kind: 'failure',
|
|
81
|
+
content: `🛑 Budget Guardrail Abort: ${budgetCheck.reason}`,
|
|
82
|
+
aggregator: slotLabel(primaryJudge),
|
|
83
|
+
references: [],
|
|
84
|
+
presetName: preset?.name || 'default',
|
|
85
|
+
isRefinement: false,
|
|
86
|
+
isFastMode,
|
|
87
|
+
winningIndex: 0,
|
|
88
|
+
winnerModel: 'none',
|
|
89
|
+
promotedFiles: [],
|
|
90
|
+
usage: { totalTokens: 0, totalCostUsd: 0, candidates: [], aggregator: { totalTokens: 0, costUsd: 0 } },
|
|
91
|
+
skippedFiles: 0,
|
|
92
|
+
skippedList: [],
|
|
93
|
+
durationMs: Date.now() - startTime,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const effectiveReferences = budgetCheck.references
|
|
97
|
+
if (budgetCheck.action === 'trim' && typeof onProgress === 'function') {
|
|
98
|
+
onProgress(`✂️ *${budgetCheck.reason}*\n`)
|
|
99
|
+
}
|
|
100
|
+
|
|
63
101
|
// 2. Collect project context for refinement tasks
|
|
64
102
|
const collectedCtx = await collectProjectContext(cwd, 16000)
|
|
65
103
|
const isRefinement = Boolean(collectedCtx?.files?.length > 0 && isRefinementTask(userPrompt, collectedCtx.files))
|
|
@@ -79,23 +117,28 @@ export async function runMoAPipeline({
|
|
|
79
117
|
})
|
|
80
118
|
}
|
|
81
119
|
|
|
82
|
-
// 3. Build prompts &
|
|
120
|
+
// 3. Build prompts & Feature 8 multi-turn context
|
|
121
|
+
const priorTurnBaseline = preset?.multi_turn_enabled !== false ? extractPriorTurnBaseline(messages) : null
|
|
122
|
+
const prunedHistory = preset?.multi_turn_enabled !== false ? pruneMultiTurnMessages(messages) : messages
|
|
123
|
+
|
|
83
124
|
const candidateSystemPrompt = projectContext
|
|
84
125
|
? `${REFERENCE_SYSTEM_PROMPT}\n\n### Current Project Files & Context:\n${projectContext}`
|
|
85
126
|
: REFERENCE_SYSTEM_PROMPT
|
|
86
127
|
|
|
87
128
|
const askClarifyingQuestions = preset?.ask_clarifying_questions !== false
|
|
88
129
|
const needsQuestions = askClarifyingQuestions && isBroadPromptRequiringQuestions(userPrompt, messages)
|
|
130
|
+
const enrichedMessages = [...prunedHistory, { role: 'user', content: userPrompt }]
|
|
89
131
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// 4. Parallel fan-out to candidate models with quorum & transient retry
|
|
132
|
+
// 4. Parallel fan-out to candidate models with temperature gradient & local fallback
|
|
93
133
|
const referenceOutputs = await runReferencesParallel(
|
|
94
|
-
|
|
134
|
+
effectiveReferences,
|
|
95
135
|
enrichedMessages,
|
|
96
136
|
{
|
|
97
137
|
systemPrompt: candidateSystemPrompt,
|
|
98
138
|
temperature: refTemp,
|
|
139
|
+
temperature_gradient_enabled: Boolean(preset?.temperature_gradient_enabled),
|
|
140
|
+
local_fallback_enabled: Boolean(preset?.local_fallback_enabled),
|
|
141
|
+
local_fallback_models: preset?.local_fallback_models || [],
|
|
99
142
|
maxTokens,
|
|
100
143
|
prices,
|
|
101
144
|
timeoutSec: refTimeoutSec,
|
|
@@ -185,7 +228,7 @@ export async function runMoAPipeline({
|
|
|
185
228
|
}
|
|
186
229
|
qUsage = estimateTokenCost(primaryJudge, qFallbackUsage, prices)
|
|
187
230
|
} catch (err) {
|
|
188
|
-
|
|
231
|
+
logger.warn('[dsh-moa] Questionnaire synthesis failed, proceeding with fallback questions:', err)
|
|
189
232
|
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.*`
|
|
190
233
|
}
|
|
191
234
|
|
|
@@ -195,7 +238,7 @@ export async function runMoAPipeline({
|
|
|
195
238
|
const durationMs = Date.now() - startTime
|
|
196
239
|
|
|
197
240
|
try {
|
|
198
|
-
|
|
241
|
+
await recordMoaRunAsync({
|
|
199
242
|
prompt: userPrompt,
|
|
200
243
|
preset: preset?.name || 'default',
|
|
201
244
|
isRefinement,
|
|
@@ -209,7 +252,7 @@ export async function runMoAPipeline({
|
|
|
209
252
|
durationMs,
|
|
210
253
|
}, historyFilePath || undefined)
|
|
211
254
|
} catch (histErr) {
|
|
212
|
-
|
|
255
|
+
logger.warn('[dsh-moa] Failed to record questionnaire run in history:', histErr?.message || histErr)
|
|
213
256
|
}
|
|
214
257
|
|
|
215
258
|
return {
|
|
@@ -251,7 +294,7 @@ export async function runMoAPipeline({
|
|
|
251
294
|
const durationMs = Date.now() - startTime
|
|
252
295
|
|
|
253
296
|
try {
|
|
254
|
-
|
|
297
|
+
await recordMoaRunAsync({
|
|
255
298
|
prompt: userPrompt,
|
|
256
299
|
preset: preset?.name || 'default',
|
|
257
300
|
isRefinement,
|
|
@@ -266,7 +309,7 @@ export async function runMoAPipeline({
|
|
|
266
309
|
durationMs,
|
|
267
310
|
}, historyFilePath || undefined)
|
|
268
311
|
} catch (histErr) {
|
|
269
|
-
|
|
312
|
+
logger.warn('[dsh-moa] Failed to record fast synthesis run in history:', histErr?.message || histErr)
|
|
270
313
|
}
|
|
271
314
|
|
|
272
315
|
return {
|
|
@@ -334,7 +377,7 @@ export async function runMoAPipeline({
|
|
|
334
377
|
cand.costUsd = Number(((cand.costUsd || 0) + extraCost.costUsd).toFixed(5))
|
|
335
378
|
}
|
|
336
379
|
} catch (r2CostErr) {
|
|
337
|
-
|
|
380
|
+
logger.warn('[dsh-moa] Failed to estimate Round 2 cost:', r2CostErr?.message || r2CostErr)
|
|
338
381
|
}
|
|
339
382
|
})
|
|
340
383
|
await Promise.allSettled(r2Promises)
|
|
@@ -347,10 +390,32 @@ export async function runMoAPipeline({
|
|
|
347
390
|
})
|
|
348
391
|
}
|
|
349
392
|
|
|
393
|
+
// 7c. Feature 2: Multi-Judge Panel & Consensus Voting
|
|
394
|
+
let multiJudgeResult = null
|
|
395
|
+
if (preset?.multi_judge_enabled && successfulRefs.length > 1) {
|
|
396
|
+
multiJudgeResult = await executeMultiJudgePanel({
|
|
397
|
+
judges: (preset.judge_models && preset.judge_models.length > 0) ? preset.judge_models : [primaryJudge],
|
|
398
|
+
candidates: successfulRefs,
|
|
399
|
+
userPrompt,
|
|
400
|
+
callLlm,
|
|
401
|
+
strategy: preset.judge_voting_strategy || 'majority',
|
|
402
|
+
signal,
|
|
403
|
+
onProgress,
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// 7d. Feature 3: Composite Block Merge Directive
|
|
408
|
+
const compositeMergeDirective = (preset?.composite_merge_enabled && successfulRefs.length > 1)
|
|
409
|
+
? buildCompositeBlockDirectives(successfulRefs)
|
|
410
|
+
: null
|
|
411
|
+
|
|
350
412
|
// 8. Synthesis phase via primary judge or fallback chain
|
|
351
413
|
const synthesisPrompt = buildSynthesisPrompt(userPrompt, referenceOutputs, judgeCriteria, {
|
|
352
414
|
curatorSynthesis: isCuratorSynthesis,
|
|
353
415
|
blindEvaluation: isBlindEvaluation,
|
|
416
|
+
priorTurnBaseline,
|
|
417
|
+
compositeMergeDirective,
|
|
418
|
+
consensusReport: multiJudgeResult?.consensusReport,
|
|
354
419
|
})
|
|
355
420
|
let synthesizedText = ''
|
|
356
421
|
let aggUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
|
|
@@ -396,7 +461,7 @@ export async function runMoAPipeline({
|
|
|
396
461
|
break
|
|
397
462
|
} catch (err) {
|
|
398
463
|
lastJudgeError = err
|
|
399
|
-
|
|
464
|
+
logger.warn(`[dsh-moa] Judge ${currentLabel} failed:`, err)
|
|
400
465
|
if (typeof onProgress === 'function') {
|
|
401
466
|
const nextJudge = judgesChain[jIdx + 1]
|
|
402
467
|
const nextHint = nextJudge ? ` Trying fallback ${slotLabel(nextJudge)}...` : ''
|
|
@@ -412,7 +477,10 @@ export async function runMoAPipeline({
|
|
|
412
477
|
}
|
|
413
478
|
|
|
414
479
|
// 9. Evaluate winner & promote files
|
|
415
|
-
const winningIndex =
|
|
480
|
+
const winningIndex = (multiJudgeResult?.consensus && multiJudgeResult?.winningCandidateIndex)
|
|
481
|
+
? multiJudgeResult.winningCandidateIndex
|
|
482
|
+
: parseWinnerIndex(synthesizedText, 1, referenceOutputs.length)
|
|
483
|
+
|
|
416
484
|
const recommendedAssembler = isCuratorSynthesis
|
|
417
485
|
? parseRecommendedAssembler(synthesizedText, winningIndex, referenceOutputs.length)
|
|
418
486
|
: null
|
|
@@ -435,15 +503,34 @@ export async function runMoAPipeline({
|
|
|
435
503
|
}
|
|
436
504
|
|
|
437
505
|
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs)
|
|
438
|
-
|
|
439
506
|
const durationMs = Date.now() - startTime
|
|
440
507
|
const usageSummary = summarizeMoAUsage(referenceOutputs, aggUsage)
|
|
441
508
|
const winningRef = referenceOutputs[winningIndex - 1]
|
|
442
509
|
const winnerModel = winningRef?.label || slotLabel(referenceModels[0])
|
|
443
510
|
const finalAggLabel = slotLabel(chosenJudge)
|
|
444
511
|
|
|
512
|
+
// Feature 9: Benchmark & Post-Mortem PR report
|
|
513
|
+
let benchmarkReport = null
|
|
514
|
+
if (preset?.report_generation_enabled) {
|
|
515
|
+
benchmarkReport = generateMoABenchmarkReport({
|
|
516
|
+
runId,
|
|
517
|
+
timestamp: new Date().toISOString(),
|
|
518
|
+
preset: preset?.name || 'default',
|
|
519
|
+
prompt: userPrompt,
|
|
520
|
+
durationMs,
|
|
521
|
+
usage: usageSummary,
|
|
522
|
+
costUsd: usageSummary.totalCostUsd,
|
|
523
|
+
candidates: referenceOutputs,
|
|
524
|
+
winningIndex,
|
|
525
|
+
winningLabel: winnerModel,
|
|
526
|
+
consensus: multiJudgeResult,
|
|
527
|
+
testGate: referenceOutputs.find((r) => r.testResult)?.testResult || null,
|
|
528
|
+
isComposite: Boolean(preset?.composite_merge_enabled),
|
|
529
|
+
})
|
|
530
|
+
}
|
|
531
|
+
|
|
445
532
|
try {
|
|
446
|
-
|
|
533
|
+
await recordMoaRunAsync({
|
|
447
534
|
id: runId,
|
|
448
535
|
prompt: userPrompt,
|
|
449
536
|
preset: preset?.name || 'default',
|
|
@@ -453,12 +540,14 @@ export async function runMoAPipeline({
|
|
|
453
540
|
winnerIndex: winningIndex,
|
|
454
541
|
winnerModel,
|
|
455
542
|
promotedFiles,
|
|
543
|
+
benchmarkReport,
|
|
544
|
+
consensus: multiJudgeResult,
|
|
456
545
|
totalTokens: usageSummary.totalTokens,
|
|
457
546
|
totalCostUsd: usageSummary.totalCostUsd,
|
|
458
547
|
durationMs,
|
|
459
548
|
}, historyFilePath || undefined)
|
|
460
549
|
} catch (histErr) {
|
|
461
|
-
|
|
550
|
+
logger.warn('[dsh-moa] Failed to record run in history:', histErr)
|
|
462
551
|
}
|
|
463
552
|
|
|
464
553
|
return {
|
|
@@ -476,6 +565,8 @@ export async function runMoAPipeline({
|
|
|
476
565
|
promotedFiles,
|
|
477
566
|
runId,
|
|
478
567
|
allowCandidateOverride,
|
|
568
|
+
benchmarkReport,
|
|
569
|
+
consensus: multiJudgeResult,
|
|
479
570
|
...(livePreview ? { liveCanvas: livePreview } : {}),
|
|
480
571
|
usage: usageSummary,
|
|
481
572
|
skippedFiles: collectedCtx?.skippedFiles || 0,
|
|
@@ -483,113 +574,3 @@ export async function runMoAPipeline({
|
|
|
483
574
|
durationMs,
|
|
484
575
|
}
|
|
485
576
|
}
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* Streams a full MoA turn into chat with live aggregator tokens and progress feedback.
|
|
489
|
-
*/
|
|
490
|
-
export async function* streamMoATurn({ targetPreset, userPrompt, messages, callLlm, cwd, prices, historyFilePath, liveCanvas }, options = {}) {
|
|
491
|
-
const signal = options?.signal
|
|
492
|
-
if (signal?.aborted) return
|
|
493
|
-
|
|
494
|
-
yield { type: 'block-start', index: 0, blockType: 'text' }
|
|
495
|
-
yield { type: 'text-delta', index: 0, text: '🧠 *Mixture of Agents started...*\n\n' }
|
|
496
|
-
|
|
497
|
-
// Async push-queue for zero-latency live delta streaming
|
|
498
|
-
const queue = []
|
|
499
|
-
let notify = null
|
|
500
|
-
|
|
501
|
-
const pushUpdate = (text) => {
|
|
502
|
-
queue.push({ type: 'text-delta', index: 0, text })
|
|
503
|
-
if (notify) {
|
|
504
|
-
notify()
|
|
505
|
-
notify = null
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
let done = false
|
|
510
|
-
const pipelinePromise = runMoAPipeline({
|
|
511
|
-
userPrompt,
|
|
512
|
-
messages,
|
|
513
|
-
preset: targetPreset,
|
|
514
|
-
callLlm,
|
|
515
|
-
cwd,
|
|
516
|
-
onProgress: pushUpdate,
|
|
517
|
-
onStreamDelta: (delta) => pushUpdate(delta),
|
|
518
|
-
prices,
|
|
519
|
-
historyFilePath,
|
|
520
|
-
liveCanvas,
|
|
521
|
-
signal,
|
|
522
|
-
})
|
|
523
|
-
.catch((err) => ({ error: err }))
|
|
524
|
-
.finally(() => {
|
|
525
|
-
done = true
|
|
526
|
-
if (notify) {
|
|
527
|
-
notify()
|
|
528
|
-
notify = null
|
|
529
|
-
}
|
|
530
|
-
})
|
|
531
|
-
|
|
532
|
-
const startTime = Date.now()
|
|
533
|
-
let lastYieldTime = Date.now()
|
|
534
|
-
|
|
535
|
-
try {
|
|
536
|
-
while (!done || queue.length > 0) {
|
|
537
|
-
if (signal?.aborted) {
|
|
538
|
-
await cleanMoaWorkspaces(cwd)
|
|
539
|
-
return
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
while (queue.length > 0) {
|
|
543
|
-
const item = queue.shift()
|
|
544
|
-
yield item
|
|
545
|
-
lastYieldTime = Date.now()
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
if (done) break
|
|
549
|
-
|
|
550
|
-
// Wait for next push item or max 2.5s heartbeat
|
|
551
|
-
await Promise.race([
|
|
552
|
-
new Promise((resolve) => { notify = resolve }),
|
|
553
|
-
new Promise((resolve) => { const t = setTimeout(resolve, 2500); t.unref?.(); }),
|
|
554
|
-
])
|
|
555
|
-
|
|
556
|
-
const elapsedSec = Math.floor((Date.now() - startTime) / 1000)
|
|
557
|
-
if (!done && Date.now() - lastYieldTime >= 3000) {
|
|
558
|
-
yield { type: 'text-delta', index: 0, text: `⏳ *[${elapsedSec}s] Still processing...*\n` }
|
|
559
|
-
lastYieldTime = Date.now()
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
const result = await pipelinePromise
|
|
564
|
-
if (signal?.aborted) {
|
|
565
|
-
await cleanMoaWorkspaces(cwd)
|
|
566
|
-
return
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
if (result?.error) {
|
|
570
|
-
const errText = '\n\n⚠️ **Mixture of Agents error**: ' + (result.error?.message || String(result.error))
|
|
571
|
-
yield { type: 'text-delta', index: 0, text: errText }
|
|
572
|
-
yield { type: 'block-end', index: 0, block: { type: 'text', text: errText } }
|
|
573
|
-
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
574
|
-
return
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
const formatted = formatMoAResponse({
|
|
578
|
-
moaResult: result,
|
|
579
|
-
presetName: targetPreset?.name || 'default',
|
|
580
|
-
})
|
|
581
|
-
|
|
582
|
-
yield { type: 'text-delta', index: 0, text: '\n---\n\n' + formatted }
|
|
583
|
-
yield { type: 'block-end', index: 0, block: { type: 'text', text: formatted } }
|
|
584
|
-
yield {
|
|
585
|
-
type: 'usage',
|
|
586
|
-
usage: {
|
|
587
|
-
inputTokens: result?.usage?.totalTokens || 0,
|
|
588
|
-
outputTokens: Math.round((formatted.length || 0) / 4),
|
|
589
|
-
},
|
|
590
|
-
}
|
|
591
|
-
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
592
|
-
} finally {
|
|
593
|
-
// cleanup
|
|
594
|
-
}
|
|
595
|
-
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-latency Live Streaming Generator for MoA Turns
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { cleanMoaWorkspaces } from './file-workspace.js'
|
|
6
|
+
import { formatMoAResponse } from './moa-parser.js'
|
|
7
|
+
import { runMoAPipeline } from './moa-runner.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Streams a full MoA turn into chat with live aggregator tokens and progress feedback.
|
|
11
|
+
*/
|
|
12
|
+
export async function* streamMoATurn({ targetPreset, userPrompt, messages, callLlm, cwd, prices, historyFilePath, liveCanvas }, options = {}) {
|
|
13
|
+
const signal = options?.signal
|
|
14
|
+
if (signal?.aborted) return
|
|
15
|
+
|
|
16
|
+
yield { type: 'block-start', index: 0, blockType: 'text' }
|
|
17
|
+
yield { type: 'text-delta', index: 0, text: '🧠 *Mixture of Agents started...*\n\n' }
|
|
18
|
+
|
|
19
|
+
// Async push-queue for zero-latency live delta streaming
|
|
20
|
+
const queue = []
|
|
21
|
+
let notify = null
|
|
22
|
+
|
|
23
|
+
const pushUpdate = (text) => {
|
|
24
|
+
queue.push({ type: 'text-delta', index: 0, text })
|
|
25
|
+
if (notify) {
|
|
26
|
+
notify()
|
|
27
|
+
notify = null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let done = false
|
|
32
|
+
const pipelinePromise = runMoAPipeline({
|
|
33
|
+
userPrompt,
|
|
34
|
+
messages,
|
|
35
|
+
preset: targetPreset,
|
|
36
|
+
callLlm,
|
|
37
|
+
cwd,
|
|
38
|
+
onProgress: pushUpdate,
|
|
39
|
+
onStreamDelta: (delta) => pushUpdate(delta),
|
|
40
|
+
prices,
|
|
41
|
+
historyFilePath,
|
|
42
|
+
liveCanvas,
|
|
43
|
+
signal,
|
|
44
|
+
})
|
|
45
|
+
.catch((err) => ({ error: err }))
|
|
46
|
+
.finally(() => {
|
|
47
|
+
done = true
|
|
48
|
+
if (notify) {
|
|
49
|
+
notify()
|
|
50
|
+
notify = null
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const startTime = Date.now()
|
|
55
|
+
let lastYieldTime = Date.now()
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
while (!done || queue.length > 0) {
|
|
59
|
+
if (signal?.aborted) {
|
|
60
|
+
await cleanMoaWorkspaces(cwd)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
while (queue.length > 0) {
|
|
65
|
+
const item = queue.shift()
|
|
66
|
+
yield item
|
|
67
|
+
lastYieldTime = Date.now()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (done) break
|
|
71
|
+
|
|
72
|
+
// Wait for next push item or max 2.5s heartbeat
|
|
73
|
+
await Promise.race([
|
|
74
|
+
new Promise((resolve) => { notify = resolve }),
|
|
75
|
+
new Promise((resolve) => { const t = setTimeout(resolve, 2500); t.unref?.(); }),
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
const elapsedSec = Math.floor((Date.now() - startTime) / 1000)
|
|
79
|
+
if (!done && Date.now() - lastYieldTime >= 3000) {
|
|
80
|
+
yield { type: 'text-delta', index: 0, text: `⏳ *[${elapsedSec}s] Still processing...*\n` }
|
|
81
|
+
lastYieldTime = Date.now()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const result = await pipelinePromise
|
|
86
|
+
if (signal?.aborted) {
|
|
87
|
+
await cleanMoaWorkspaces(cwd)
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (result?.error) {
|
|
92
|
+
const errText = '\n\n⚠️ **Mixture of Agents error**: ' + (result.error?.message || String(result.error))
|
|
93
|
+
yield { type: 'text-delta', index: 0, text: errText }
|
|
94
|
+
yield { type: 'block-end', index: 0, block: { type: 'text', text: errText } }
|
|
95
|
+
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const formatted = formatMoAResponse({
|
|
100
|
+
moaResult: result,
|
|
101
|
+
presetName: targetPreset?.name || 'default',
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
yield { type: 'text-delta', index: 0, text: '\n---\n\n' + formatted }
|
|
105
|
+
yield { type: 'block-end', index: 0, block: { type: 'text', text: formatted } }
|
|
106
|
+
yield {
|
|
107
|
+
type: 'usage',
|
|
108
|
+
usage: {
|
|
109
|
+
inputTokens: result?.usage?.totalTokens || 0,
|
|
110
|
+
outputTokens: Math.round((formatted.length || 0) / 4),
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
114
|
+
} finally {
|
|
115
|
+
// cleanup
|
|
116
|
+
}
|
|
117
|
+
}
|
package/lib/moa-test-gate.js
CHANGED
|
@@ -4,6 +4,7 @@ import fsSync from 'node:fs'
|
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { spawn } from 'node:child_process'
|
|
6
6
|
import { assertPathContained } from './file-workspace.js'
|
|
7
|
+
import { bestEffort } from './best-effort.js'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Resolves the directory for candidate sandbox.
|
|
@@ -179,9 +180,9 @@ export async function runCandidateTestGate({
|
|
|
179
180
|
}
|
|
180
181
|
} finally {
|
|
181
182
|
if (stageDir) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
await bestEffort('moa-test-gate cleanup stageDir', () =>
|
|
184
|
+
fs.rm(stageDir, { recursive: true, force: true })
|
|
185
|
+
)
|
|
185
186
|
}
|
|
186
187
|
}
|
|
187
188
|
}
|