@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.
@@ -0,0 +1,163 @@
1
+ import crypto from 'node:crypto'
2
+ import { isTestEnvironment } from '../history.js'
3
+ import { cleanMoaWorkspaces } from '../workspace/fs.js'
4
+ import { buildCompositeBlockDirectives } from '../moa-multi-judge.js'
5
+ import { formatFileScoresDirective } from '../moa-workbench.js'
6
+ import { stageProjectContext } from './context.js'
7
+ import { stageCandidateRound1 } from './candidates.js'
8
+ import { stageClarifyingQuestions } from './questions.js'
9
+ import { stageFastMode } from './fast-mode.js'
10
+ import { stagePeerCritique } from './peer-critique.js'
11
+ import { stageMultiJudge } from './multi-judge.js'
12
+ import { stageSynthesis } from './synthesis.js'
13
+ import { stagePromotion } from './promotion.js'
14
+
15
+ export async function runMoAPipeline({
16
+ userPrompt,
17
+ messages = [],
18
+ preset,
19
+ callLlm,
20
+ cwd = process.cwd(), prices = {}, onProgress = null,
21
+ historyFilePath = null, liveCanvas = null, onStreamDelta = null, signal = null,
22
+ testGateEnabled = null, testCommand = null, execFn = null,
23
+ webServerPort = null, timeMachineEngine = null, timeMachineUrl = null,
24
+ force = isTestEnvironment() ? true : false, checkpoint = true, checkpointFn = null, runId: customRunId = null,
25
+ }) {
26
+ const runId = (customRunId && String(customRunId).trim()) || crypto.randomUUID()
27
+ const promoteOpts = { webServerPort, timeMachineEngine, timeMachineUrl, force: Boolean(force), checkpoint, checkpointFn, signal }
28
+ if (signal?.aborted) return { error: new Error('Turn aborted') }
29
+ const startTime = Date.now()
30
+
31
+ // Stage 1: Context & Configuration
32
+ const ctxStage = await stageProjectContext({
33
+ userPrompt, messages, preset, cwd, prices, maxTokens: preset?.max_tokens, startTime, onProgress,
34
+ })
35
+ if (!ctxStage.ok) return ctxStage.abortPayload
36
+
37
+ // Stage 2: Parallel Candidate Fan-out & Test Gate
38
+ const candStage = await stageCandidateRound1({
39
+ effectiveReferences: ctxStage.effectiveReferences,
40
+ enrichedMessages: ctxStage.enrichedMessages,
41
+ candidateSystemPrompt: ctxStage.candidateSystemPrompt,
42
+ refTemp: ctxStage.refTemp,
43
+ preset, maxTokens: ctxStage.maxTokens, prices,
44
+ refTimeoutSec: ctxStage.refTimeoutSec,
45
+ isQuorumEnabled: ctxStage.isQuorumEnabled,
46
+ gracePeriodSec: ctxStage.gracePeriodSec,
47
+ candidateRetries: ctxStage.candidateRetries,
48
+ signal, callLlm, onProgress, cwd, runId, startTime,
49
+ primaryJudge: ctxStage.primaryJudge,
50
+ isRefinement: ctxStage.isRefinement,
51
+ isFastMode: ctxStage.isFastMode,
52
+ collectedCtx: ctxStage.collectedCtx,
53
+ testGateEnabled, testCommand, execFn,
54
+ })
55
+ if (!candStage.ok) {
56
+ if (candStage.abort) return { error: candStage.error }
57
+ return candStage.failurePayload
58
+ }
59
+ const { referenceOutputs, successfulRefs } = candStage
60
+
61
+ // Stage 3: Clarifying Questions Branch
62
+ if (ctxStage.needsQuestions) {
63
+ return await stageClarifyingQuestions({
64
+ userPrompt, referenceOutputs, primaryJudge: ctxStage.primaryJudge,
65
+ aggTimeoutSec: ctxStage.aggTimeoutSec, prices, callLlm, cwd, runId,
66
+ preset, isRefinement: ctxStage.isRefinement, isFastMode: ctxStage.isFastMode,
67
+ collectedCtx: ctxStage.collectedCtx, startTime, historyFilePath, onProgress,
68
+ })
69
+ }
70
+
71
+ // Stage 4: Fast Mode (1 candidate bypass)
72
+ if (ctxStage.isFastMode) {
73
+ const fastRes = await stageFastMode({
74
+ successfulRefs, referenceOutputs,
75
+ allowCandidateOverride: ctxStage.allowCandidateOverride,
76
+ promoteOpts, cwd, runId, signal, userPrompt, preset,
77
+ isRefinement: ctxStage.isRefinement, historyFilePath, startTime,
78
+ collectedCtx: ctxStage.collectedCtx, liveCanvas,
79
+ })
80
+ if (!fastRes.ok) {
81
+ if (fastRes.abort) return { error: fastRes.error }
82
+ return fastRes.payload
83
+ }
84
+ return fastRes.payload
85
+ }
86
+
87
+ if (signal?.aborted) {
88
+ await cleanMoaWorkspaces(cwd, { runId })
89
+ return { error: new Error('Turn aborted') }
90
+ }
91
+
92
+ // Stage 5: Consilium Round 2 (Peer Critique)
93
+ const peerRes = await stagePeerCritique({
94
+ isPeerCritiqueEnabled: ctxStage.isPeerCritiqueEnabled,
95
+ successfulRefs, referenceOutputs, preset, prices,
96
+ maxTokens: ctxStage.maxTokens, primaryJudge: ctxStage.primaryJudge,
97
+ userPrompt, isBlindEvaluation: ctxStage.isBlindEvaluation,
98
+ refTemp: ctxStage.refTemp, refTimeoutSec: ctxStage.refTimeoutSec,
99
+ candidateRetries: ctxStage.candidateRetries, signal, callLlm, onProgress,
100
+ cwd, runId, isRefinement: ctxStage.isRefinement, isFastMode: ctxStage.isFastMode,
101
+ collectedCtx: ctxStage.collectedCtx, startTime,
102
+ options: { testGateEnabled, testCommand, execFn },
103
+ })
104
+ if (!peerRes.ok) return peerRes.abortPayload
105
+
106
+ if (signal?.aborted) {
107
+ await cleanMoaWorkspaces(cwd, { runId })
108
+ return { error: new Error('Turn aborted') }
109
+ }
110
+
111
+ // Stage 6: Multi-Judge Panel
112
+ const mjRes = await stageMultiJudge({
113
+ preset, successfulRefs, referenceOutputs, primaryJudge: ctxStage.primaryJudge,
114
+ userPrompt, judgeCriteria: ctxStage.judgeCriteria, prices, maxTokens: ctxStage.maxTokens,
115
+ callLlm, signal, onProgress, cwd, runId, isRefinement: ctxStage.isRefinement,
116
+ isFastMode: ctxStage.isFastMode, collectedCtx: ctxStage.collectedCtx, startTime,
117
+ })
118
+ if (!mjRes.ok) return mjRes.abortPayload
119
+ const multiJudgeResult = mjRes.multiJudgeResult
120
+
121
+ const compositeMergeDirective = (preset?.composite_merge_enabled && successfulRefs.length > 1)
122
+ ? buildCompositeBlockDirectives(successfulRefs, { blind: Boolean(preset.blind_evaluation) })
123
+ : null
124
+ const fileScoringDirective = (preset?.interactive_workbench_enabled)
125
+ ? formatFileScoresDirective()
126
+ : null
127
+
128
+ // Stage 7: Synthesis Phase
129
+ const synthRes = await stageSynthesis({
130
+ userPrompt, referenceOutputs, successfulRefs, judgeCriteria: ctxStage.judgeCriteria,
131
+ isCuratorSynthesis: ctxStage.isCuratorSynthesis, isBlindEvaluation: ctxStage.isBlindEvaluation,
132
+ priorTurnBaseline: ctxStage.priorTurnBaseline, compositeMergeDirective, fileScoringDirective,
133
+ multiJudgeResult, preset, prices, maxTokens: ctxStage.maxTokens, isFastMode: ctxStage.isFastMode,
134
+ primaryJudge: ctxStage.primaryJudge, fallbackJudges: ctxStage.fallbackJudges,
135
+ judgesChain: ctxStage.judgesChain, aggTemp: ctxStage.aggTemp,
136
+ aggTimeoutSec: ctxStage.aggTimeoutSec, isStreamAggregator: ctxStage.isStreamAggregator,
137
+ onStreamDelta, signal, callLlm, onProgress, cwd, runId, isRefinement: ctxStage.isRefinement,
138
+ collectedCtx: ctxStage.collectedCtx, startTime,
139
+ })
140
+ if (!synthRes.ok) return synthRes.abortPayload
141
+ const { synthesizedText, chosenJudge, aggUsage } = synthRes
142
+
143
+ if (signal?.aborted) {
144
+ await cleanMoaWorkspaces(cwd, { runId })
145
+ return { error: new Error('Turn aborted') }
146
+ }
147
+
148
+ // Stage 8: Evaluation, Winner & Promotion
149
+ const promRes = await stagePromotion({
150
+ synthesizedText, referenceOutputs, successfulRefs, multiJudgeResult,
151
+ isCuratorSynthesis: ctxStage.isCuratorSynthesis,
152
+ allowCandidateOverride: ctxStage.allowCandidateOverride,
153
+ promoteOpts, cwd, runId, signal, liveCanvas, startTime, chosenJudge, aggUsage,
154
+ userPrompt, preset, isRefinement: ctxStage.isRefinement, isFastMode: ctxStage.isFastMode,
155
+ effectiveReferences: ctxStage.effectiveReferences, referenceModels: ctxStage.referenceModels,
156
+ historyFilePath, collectedCtx: ctxStage.collectedCtx,
157
+ })
158
+ if (!promRes.ok) {
159
+ if (promRes.abort) return { error: promRes.error }
160
+ throw promRes.error
161
+ }
162
+ return promRes.payload
163
+ }
@@ -0,0 +1,150 @@
1
+ import { logger } from '../logger.js'
2
+ import { buildSynthesisPrompt, slotLabel } from '../moa-prompts.js'
3
+ import { estimateDownstreamCost, checkPhaseBudget, createBudgetAbortPayload } from '../moa-budget.js'
4
+ import { callWithTransientRetry } from '../moa-candidates.js'
5
+ import { estimateTokenCost } from '../pricing.js'
6
+ import { cleanMoaWorkspaces } from '../workspace/fs.js'
7
+
8
+ export async function stageSynthesis({
9
+ userPrompt,
10
+ referenceOutputs,
11
+ successfulRefs,
12
+ judgeCriteria,
13
+ isCuratorSynthesis,
14
+ isBlindEvaluation,
15
+ priorTurnBaseline,
16
+ compositeMergeDirective,
17
+ fileScoringDirective,
18
+ multiJudgeResult,
19
+ preset,
20
+ prices,
21
+ maxTokens,
22
+ isFastMode,
23
+ primaryJudge,
24
+ fallbackJudges,
25
+ judgesChain,
26
+ aggTemp,
27
+ aggTimeoutSec,
28
+ isStreamAggregator,
29
+ onStreamDelta,
30
+ signal,
31
+ callLlm,
32
+ onProgress,
33
+ cwd,
34
+ runId,
35
+ isRefinement,
36
+ collectedCtx,
37
+ startTime,
38
+ }) {
39
+ const synthesisPrompt = buildSynthesisPrompt(userPrompt, referenceOutputs, judgeCriteria, {
40
+ curatorSynthesis: isCuratorSynthesis,
41
+ blindEvaluation: isBlindEvaluation,
42
+ priorTurnBaseline,
43
+ compositeMergeDirective,
44
+ fileScoringDirective,
45
+ consensusReport: multiJudgeResult?.consensusReport,
46
+ })
47
+ const synthPromptTokens = Math.max(50, Math.ceil(synthesisPrompt.length / 4))
48
+
49
+ if (preset?.budget_guard_enabled && preset?.max_budget_usd > 0 && !isFastMode) {
50
+ const currentSpent = referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + (multiJudgeResult?.totalCostUsd || 0)
51
+ const downNext = estimateDownstreamCost(primaryJudge, [], successfulRefs.length, prices, maxTokens, synthPromptTokens)
52
+ const phaseCheck = checkPhaseBudget({
53
+ phaseName: 'Judge Synthesis', currentSpentUsd: currentSpent, nextPhaseCostUsd: downNext,
54
+ maxBudgetUsd: preset.max_budget_usd, action: preset.budget_action || 'trim', enabled: true,
55
+ })
56
+ if (!phaseCheck.allowed && fallbackJudges.length === 0) {
57
+ await cleanMoaWorkspaces(cwd, { runId })
58
+ const abortPayload = createBudgetAbortPayload({
59
+ reason: phaseCheck.reason, primaryJudge, referenceOutputs, currentSpentUsd: currentSpent,
60
+ preset, isRefinement, isFastMode, collectedCtx, startTime, multiJudgeResult,
61
+ })
62
+ return { ok: false, abortPayload }
63
+ }
64
+ }
65
+
66
+ let synthesizedText = ''
67
+ let aggUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
68
+ let chosenJudge = primaryJudge
69
+ let judgeSuccess = false
70
+ let lastJudgeError = null
71
+
72
+ for (let jIdx = 0; jIdx < judgesChain.length; jIdx++) {
73
+ const currentJudge = judgesChain[jIdx]
74
+ const currentLabel = slotLabel(currentJudge)
75
+
76
+ if (preset?.budget_guard_enabled && preset?.max_budget_usd > 0 && !isFastMode) {
77
+ const currentSpent = referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + (multiJudgeResult?.totalCostUsd || 0)
78
+ const judgeCost = estimateDownstreamCost(currentJudge, [], successfulRefs.length, prices, maxTokens, synthPromptTokens)
79
+ const phaseCheck = checkPhaseBudget({
80
+ phaseName: `Judge Synthesis (${currentLabel})`, currentSpentUsd: currentSpent, nextPhaseCostUsd: judgeCost,
81
+ maxBudgetUsd: preset.max_budget_usd, action: preset.budget_action || 'trim', enabled: true,
82
+ })
83
+ if (!phaseCheck.allowed) {
84
+ lastJudgeError = new Error(phaseCheck.reason)
85
+ logger.warn(`[dsh-moa] Judge ${currentLabel} budget check failed: ${phaseCheck.reason}`)
86
+ if (typeof onProgress === 'function') onProgress(`⚠️ *Judge ${currentLabel} skipped: exceeds budget limit.*\n`)
87
+ continue
88
+ }
89
+ }
90
+
91
+ if (typeof onProgress === 'function') {
92
+ const modeTitle = isCuratorSynthesis ? 'Curator' : 'Judge'
93
+ const fallbackBadge = jIdx > 0 ? ` (Fallback #${jIdx})` : ''
94
+ onProgress(`⚖️ *${modeTitle} (${currentLabel}${fallbackBadge}) synthesizing solution...*\n`)
95
+ }
96
+
97
+ try {
98
+ const aggRes = await callWithTransientRetry(callLlm, {
99
+ provider: currentJudge.provider,
100
+ model: currentJudge.model,
101
+ messages: [{ role: 'user', content: synthesisPrompt }],
102
+ temperature: aggTemp,
103
+ maxTokens,
104
+ timeoutMs: aggTimeoutSec * 1000,
105
+ signal,
106
+ onStreamDelta: (delta) => {
107
+ if (isStreamAggregator && typeof onStreamDelta === 'function') {
108
+ onStreamDelta(delta)
109
+ }
110
+ },
111
+ }, 0)
112
+
113
+ synthesizedText = typeof aggRes === 'string' ? aggRes : (aggRes?.content || aggRes?.text || '')
114
+
115
+ const aggFallbackUsage = (typeof aggRes === 'object' && aggRes?.usage) ? aggRes.usage : {
116
+ inputTokens: Math.max(1, Math.round(synthesisPrompt.length / 4)),
117
+ outputTokens: Math.max(1, Math.round(synthesizedText.length / 4)),
118
+ }
119
+ aggUsage = estimateTokenCost(currentJudge, aggFallbackUsage, prices)
120
+ chosenJudge = currentJudge
121
+ judgeSuccess = true
122
+ break
123
+ } catch (err) {
124
+ lastJudgeError = err
125
+ logger.warn(`[dsh-moa] Judge ${currentLabel} failed:`, err)
126
+ if (typeof onProgress === 'function') {
127
+ const nextJudge = judgesChain[jIdx + 1]
128
+ const nextHint = nextJudge ? ` Trying fallback ${slotLabel(nextJudge)}...` : ''
129
+ onProgress(`⚠️ *Judge ${currentLabel} failed: ${err?.message || err}.${nextHint}*\n`)
130
+ }
131
+ }
132
+ }
133
+
134
+ if (!judgeSuccess) {
135
+ if (preset?.budget_guard_enabled && preset?.max_budget_usd > 0 && !isFastMode && (lastJudgeError?.message?.includes('exceeds max budget') || lastJudgeError?.message?.includes('Budget Guardrail') || preset?.budget_action === 'abort')) {
136
+ await cleanMoaWorkspaces(cwd, { runId })
137
+ const currentSpent = referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + (multiJudgeResult?.totalCostUsd || 0)
138
+ const abortPayload = createBudgetAbortPayload({
139
+ reason: lastJudgeError?.message || 'Budget exceeded before judge synthesis', primaryJudge, referenceOutputs,
140
+ currentSpentUsd: currentSpent, preset, isRefinement, isFastMode, collectedCtx, startTime, multiJudgeResult,
141
+ })
142
+ return { ok: false, abortPayload }
143
+ }
144
+ const firstErr = lastJudgeError ? (lastJudgeError.message || String(lastJudgeError)) : 'unknown error'
145
+ synthesizedText = `⚠️ *[Aggregator error: ${firstErr}. Fallback candidate outputs:]*\n\n` +
146
+ successfulRefs.map((r, i) => `### Candidate ${i + 1} (${r.label})\n${r.text}`).join('\n\n')
147
+ }
148
+
149
+ return { ok: true, synthesizedText, chosenJudge, aggUsage }
150
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Optional-service helpers for Cordis runtime.
3
+ * Avoids tripping strict-proxy guards and survives headless profiles.
4
+ */
5
+
6
+ /**
7
+ * Safely read an optional service from ctx without throwing strict-proxy guard errors.
8
+ * @param {object | undefined} ctx
9
+ * @param {string} name
10
+ * @returns {unknown}
11
+ */
12
+ export function service(ctx, name) {
13
+ try {
14
+ const viaGet = typeof ctx?.get === 'function' ? ctx.get(name) : undefined
15
+ if (viaGet) return viaGet
16
+ } catch {
17
+ /* strict context refuses get() for a service the fiber does not inject */
18
+ }
19
+ try {
20
+ const viaReflect = ctx?.reflect?.get?.(name, false)
21
+ if (viaReflect) return viaReflect
22
+ } catch {
23
+ /* ignore reflection errors */
24
+ }
25
+ try {
26
+ return ctx ? ctx[name] : undefined
27
+ } catch {
28
+ return undefined
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Safely resolves the web server port if webServer service is mounted.
34
+ * @param {object | undefined} ctx
35
+ * @returns {number | null}
36
+ */
37
+ export function getWebServerPort(ctx) {
38
+ const ws = service(ctx, 'webServer')
39
+ return (ws && typeof ws.port === 'number') ? ws.port : null
40
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.61",
3
+ "version": "0.2.62",
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",