@goodandready/dsh-moa 0.2.26 → 0.2.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/moa-runner.js CHANGED
@@ -3,12 +3,16 @@ import crypto from 'node:crypto'
3
3
  import { bestEffort } from './best-effort.js'
4
4
  import { extractFileBlocks, collectProjectContext, formatProjectContext, isRefinementTask, writeCandidateWorkspace, promoteCandidateWorkspace, cleanMoaWorkspaces, verifyFileSyntax } from './file-workspace.js'
5
5
  import { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
6
- import { recordMoaRun, recordMoaRunAsync, candidatesForHistory } from './history.js'
6
+ import { recordMoaRun, candidatesForHistory } from './history.js'
7
7
  import { createPromotedPreview } from './live-canvas.js'
8
8
  import { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildCuratorSynthesisPrompt, buildSynthesisPrompt, buildPeerCritiquePrompt, ROLE_PERSONA_PROMPTS, SYSTEM_ROLE_PROPOSER, ANTIPATTERNS_RUBRIC } from './moa-prompts.js'
9
9
  import { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
10
10
  import { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel } from './moa-candidates.js'
11
11
  import { executeTestGateForCandidates, runCandidateTestGate } from './moa-test-gate.js'
12
+ import { executeMultiJudgePanel, buildCompositeBlockDirectives } from './moa-multi-judge.js'
13
+ import { applyBudgetGuardrails } from './moa-budget.js'
14
+ import { extractPriorTurnBaseline, pruneMultiTurnMessages } from './moa-context.js'
15
+ import { generateMoABenchmarkReport } from './moa-report.js'
12
16
 
13
17
  // Re-exports for consumers & backward compatibility
14
18
  export { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildCuratorSynthesisPrompt, buildSynthesisPrompt, ANTIPATTERNS_RUBRIC } from './moa-prompts.js'
@@ -16,6 +20,7 @@ export { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSu
16
20
  export { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
17
21
  export { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel } from './moa-candidates.js'
18
22
  export { executeTestGateForCandidates, runCandidateTestGate } from './moa-test-gate.js'
23
+ export { streamMoATurn } from './moa-stream.js'
19
24
 
20
25
  /**
21
26
  * Executes the full Mixture of Agents pipeline.
@@ -60,6 +65,38 @@ export async function runMoAPipeline({
60
65
  const isBlindEvaluation = Boolean(preset?.blind_evaluation), isPeerCritiqueEnabled = Boolean(preset?.peer_critique_enabled)
61
66
  const allowCandidateOverride = Boolean(preset?.allow_candidate_override), runId = crypto.randomUUID()
62
67
 
68
+ // Feature 6: Budget Guardrail check
69
+ const budgetCheck = applyBudgetGuardrails({
70
+ references: referenceModels,
71
+ enabled: preset?.budget_guard_enabled,
72
+ maxBudgetUsd: preset?.max_budget_usd,
73
+ action: preset?.budget_action || 'trim',
74
+ prices,
75
+ promptLength: userPrompt?.length || 1000,
76
+ })
77
+ if (!budgetCheck.allowed) {
78
+ return {
79
+ kind: 'failure',
80
+ content: `šŸ›‘ Budget Guardrail Abort: ${budgetCheck.reason}`,
81
+ aggregator: slotLabel(primaryJudge),
82
+ references: [],
83
+ presetName: preset?.name || 'default',
84
+ isRefinement: false,
85
+ isFastMode,
86
+ winningIndex: 0,
87
+ winnerModel: 'none',
88
+ promotedFiles: [],
89
+ usage: { totalTokens: 0, totalCostUsd: 0, candidates: [], aggregator: { totalTokens: 0, costUsd: 0 } },
90
+ skippedFiles: 0,
91
+ skippedList: [],
92
+ durationMs: Date.now() - startTime,
93
+ }
94
+ }
95
+ const effectiveReferences = budgetCheck.references
96
+ if (budgetCheck.action === 'trim' && typeof onProgress === 'function') {
97
+ onProgress(`āœ‚ļø *${budgetCheck.reason}*\n`)
98
+ }
99
+
63
100
  // 2. Collect project context for refinement tasks
64
101
  const collectedCtx = await collectProjectContext(cwd, 16000)
65
102
  const isRefinement = Boolean(collectedCtx?.files?.length > 0 && isRefinementTask(userPrompt, collectedCtx.files))
@@ -79,23 +116,28 @@ export async function runMoAPipeline({
79
116
  })
80
117
  }
81
118
 
82
- // 3. Build prompts & evaluate broad questionnaire needs
119
+ // 3. Build prompts & Feature 8 multi-turn context
120
+ const priorTurnBaseline = preset?.multi_turn_enabled !== false ? extractPriorTurnBaseline(messages) : null
121
+ const prunedHistory = preset?.multi_turn_enabled !== false ? pruneMultiTurnMessages(messages) : messages
122
+
83
123
  const candidateSystemPrompt = projectContext
84
124
  ? `${REFERENCE_SYSTEM_PROMPT}\n\n### Current Project Files & Context:\n${projectContext}`
85
125
  : REFERENCE_SYSTEM_PROMPT
86
126
 
87
127
  const askClarifyingQuestions = preset?.ask_clarifying_questions !== false
88
128
  const needsQuestions = askClarifyingQuestions && isBroadPromptRequiringQuestions(userPrompt, messages)
129
+ const enrichedMessages = [...prunedHistory, { role: 'user', content: userPrompt }]
89
130
 
90
- const enrichedMessages = [...messages, { role: 'user', content: userPrompt }]
91
-
92
- // 4. Parallel fan-out to candidate models with quorum & transient retry
131
+ // 4. Parallel fan-out to candidate models with temperature gradient & local fallback
93
132
  const referenceOutputs = await runReferencesParallel(
94
- referenceModels,
133
+ effectiveReferences,
95
134
  enrichedMessages,
96
135
  {
97
136
  systemPrompt: candidateSystemPrompt,
98
137
  temperature: refTemp,
138
+ temperature_gradient_enabled: Boolean(preset?.temperature_gradient_enabled),
139
+ local_fallback_enabled: Boolean(preset?.local_fallback_enabled),
140
+ local_fallback_models: preset?.local_fallback_models || [],
99
141
  maxTokens,
100
142
  prices,
101
143
  timeoutSec: refTimeoutSec,
@@ -347,10 +389,32 @@ export async function runMoAPipeline({
347
389
  })
348
390
  }
349
391
 
392
+ // 7c. Feature 2: Multi-Judge Panel & Consensus Voting
393
+ let multiJudgeResult = null
394
+ if (preset?.multi_judge_enabled && successfulRefs.length > 1) {
395
+ multiJudgeResult = await executeMultiJudgePanel({
396
+ judges: (preset.judge_models && preset.judge_models.length > 0) ? preset.judge_models : [primaryJudge],
397
+ candidates: successfulRefs,
398
+ userPrompt,
399
+ callLlm,
400
+ strategy: preset.judge_voting_strategy || 'majority',
401
+ signal,
402
+ onProgress,
403
+ })
404
+ }
405
+
406
+ // 7d. Feature 3: Composite Block Merge Directive
407
+ const compositeMergeDirective = (preset?.composite_merge_enabled && successfulRefs.length > 1)
408
+ ? buildCompositeBlockDirectives(successfulRefs)
409
+ : null
410
+
350
411
  // 8. Synthesis phase via primary judge or fallback chain
351
412
  const synthesisPrompt = buildSynthesisPrompt(userPrompt, referenceOutputs, judgeCriteria, {
352
413
  curatorSynthesis: isCuratorSynthesis,
353
414
  blindEvaluation: isBlindEvaluation,
415
+ priorTurnBaseline,
416
+ compositeMergeDirective,
417
+ consensusReport: multiJudgeResult?.consensusReport,
354
418
  })
355
419
  let synthesizedText = ''
356
420
  let aggUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
@@ -412,7 +476,10 @@ export async function runMoAPipeline({
412
476
  }
413
477
 
414
478
  // 9. Evaluate winner & promote files
415
- const winningIndex = parseWinnerIndex(synthesizedText, 1, referenceOutputs.length)
479
+ const winningIndex = (multiJudgeResult?.consensus && multiJudgeResult?.winningCandidateIndex)
480
+ ? multiJudgeResult.winningCandidateIndex
481
+ : parseWinnerIndex(synthesizedText, 1, referenceOutputs.length)
482
+
416
483
  const recommendedAssembler = isCuratorSynthesis
417
484
  ? parseRecommendedAssembler(synthesizedText, winningIndex, referenceOutputs.length)
418
485
  : null
@@ -435,13 +502,32 @@ export async function runMoAPipeline({
435
502
  }
436
503
 
437
504
  const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs)
438
-
439
505
  const durationMs = Date.now() - startTime
440
506
  const usageSummary = summarizeMoAUsage(referenceOutputs, aggUsage)
441
507
  const winningRef = referenceOutputs[winningIndex - 1]
442
508
  const winnerModel = winningRef?.label || slotLabel(referenceModels[0])
443
509
  const finalAggLabel = slotLabel(chosenJudge)
444
510
 
511
+ // Feature 9: Benchmark & Post-Mortem PR report
512
+ let benchmarkReport = null
513
+ if (preset?.report_generation_enabled) {
514
+ benchmarkReport = generateMoABenchmarkReport({
515
+ runId,
516
+ timestamp: new Date().toISOString(),
517
+ preset: preset?.name || 'default',
518
+ prompt: userPrompt,
519
+ durationMs,
520
+ usage: usageSummary,
521
+ costUsd: usageSummary.totalCostUsd,
522
+ candidates: referenceOutputs,
523
+ winningIndex,
524
+ winningLabel: winnerModel,
525
+ consensus: multiJudgeResult,
526
+ testGate: referenceOutputs.find((r) => r.testResult)?.testResult || null,
527
+ isComposite: Boolean(preset?.composite_merge_enabled),
528
+ })
529
+ }
530
+
445
531
  try {
446
532
  recordMoaRun({
447
533
  id: runId,
@@ -453,6 +539,8 @@ export async function runMoAPipeline({
453
539
  winnerIndex: winningIndex,
454
540
  winnerModel,
455
541
  promotedFiles,
542
+ benchmarkReport,
543
+ consensus: multiJudgeResult,
456
544
  totalTokens: usageSummary.totalTokens,
457
545
  totalCostUsd: usageSummary.totalCostUsd,
458
546
  durationMs,
@@ -476,6 +564,8 @@ export async function runMoAPipeline({
476
564
  promotedFiles,
477
565
  runId,
478
566
  allowCandidateOverride,
567
+ benchmarkReport,
568
+ consensus: multiJudgeResult,
479
569
  ...(livePreview ? { liveCanvas: livePreview } : {}),
480
570
  usage: usageSummary,
481
571
  skippedFiles: collectedCtx?.skippedFiles || 0,
@@ -483,113 +573,3 @@ export async function runMoAPipeline({
483
573
  durationMs,
484
574
  }
485
575
  }
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/routes.js CHANGED
@@ -11,6 +11,8 @@ import {
11
11
  import { promoteCandidateWorkspace, readCandidateFiles, computeLineDiff, isSafeRelativePath } from './file-workspace.js'
12
12
  import { isSafeWriteRequest } from './updater.js'
13
13
  import { runMoAPipeline } from './moa-runner.js'
14
+ import { generateMoABenchmarkReport } from './moa-report.js'
15
+ import { resolvePresetForPrompt } from './moa-router.js'
14
16
 
15
17
  let _modelsCache = { timestamp: 0, models: [] }
16
18
  const MODELS_CACHE_TTL = 60 * 1000
@@ -423,16 +425,72 @@ ctx.effect(() => {
423
425
  const urlPath = req.url.split('?')[0]
424
426
  const parts = urlPath.split('/').filter(Boolean)
425
427
  const runId = parts[2] || ''
428
+ const isReport = parts[3] === 'report'
426
429
  const run = runId ? getMoaRunById(runId) : null
427
430
  if (!run) {
428
431
  writeJson(res, 404, { ok: false, error: 'Run not found' })
429
432
  return
430
433
  }
434
+ if (isReport) {
435
+ const u = new URL(req.url, 'http://127.0.0.1')
436
+ const format = u.searchParams.get('format') || 'md'
437
+ const report = run.benchmarkReport || generateMoABenchmarkReport({
438
+ runId: run.id,
439
+ timestamp: run.timestamp,
440
+ preset: run.preset,
441
+ prompt: run.prompt,
442
+ durationMs: run.durationMs,
443
+ usage: { totalTokens: run.totalTokens },
444
+ costUsd: run.totalCostUsd,
445
+ candidates: run.candidates,
446
+ winningIndex: run.winnerIndex,
447
+ winningLabel: run.winnerModel,
448
+ })
449
+ if (format === 'json') {
450
+ writeJson(res, 200, { ok: true, report: typeof report === 'object' ? (report.json || report) : report })
451
+ return
452
+ }
453
+ const mdText = typeof report === 'string' ? report : (report.markdown || JSON.stringify(report, null, 2))
454
+ res.writeHead(200, { 'Content-Type': 'text/markdown; charset=utf-8' })
455
+ res.end(mdText)
456
+ return
457
+ }
431
458
  writeJson(res, 200, { ok: true, run })
432
459
  },
433
460
  })
434
461
  }, 'dsh-moa: run by id route')
435
462
 
463
+ // Route: /dsh-moa/route-preset
464
+ ctx.effect(() => {
465
+ return ctx.webServer.register({
466
+ kind: 'exact',
467
+ path: '/dsh-moa/route-preset',
468
+ handler: async (req, res) => {
469
+ if (req.method !== 'POST') {
470
+ writeJson(res, 405, { ok: false, error: 'POST required' })
471
+ return
472
+ }
473
+ try {
474
+ const raw = await readBody(req)
475
+ const body = JSON.parse(raw || '{}')
476
+ const prompt = body?.prompt || ''
477
+ const cfg = live()
478
+ const result = await resolvePresetForPrompt({
479
+ prompt,
480
+ presets: cfg.presets || [],
481
+ defaultPreset: cfg.default_preset || 'default',
482
+ routingModel: cfg.smart_routing_model,
483
+ callLlm,
484
+ enabled: true,
485
+ })
486
+ writeJson(res, 200, { ok: true, ...result })
487
+ } catch (err) {
488
+ writeJson(res, 500, { ok: false, error: err?.message || String(err) })
489
+ }
490
+ },
491
+ })
492
+ }, 'dsh-moa: route preset endpoint')
493
+
436
494
  // Route: /dsh-moa/run
437
495
  ctx.effect(() => {
438
496
  return ctx.webServer.register({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.26",
3
+ "version": "0.2.27",
4
4
  "description": "Mixture of Agents (MoA) plugin for DeepSeek Harness with /moa slash command",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",