@goodandready/dsh-moa 0.2.60 → 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/file-workspace.js +33 -593
- 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/routes/history.js +81 -0
- package/lib/routes/pipeline.js +91 -0
- package/lib/routes/presets.js +241 -0
- package/lib/routes/workbench.js +197 -0
- package/lib/routes.js +17 -582
- package/lib/services.js +40 -0
- package/lib/workspace/checkpoint.js +51 -0
- package/lib/workspace/diff.js +94 -0
- package/lib/workspace/fs.js +284 -0
- package/lib/workspace/scanner.js +100 -0
- package/lib/workspace/security.js +80 -0
- package/package.json +1 -1
|
@@ -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,81 @@
|
|
|
1
|
+
import { getMoaHistory, getMoaLeaderboard, exportMoaHistory } from '../history.js'
|
|
2
|
+
import { writeJson, assertAuthorizedRequest } from '../access.js'
|
|
3
|
+
|
|
4
|
+
export function registerHistoryRoutes(ctx, { live }) {
|
|
5
|
+
ctx.effect(() => {
|
|
6
|
+
return ctx.webServer.register({
|
|
7
|
+
kind: 'exact',
|
|
8
|
+
path: '/dsh-moa/history',
|
|
9
|
+
handler: async (req, res) => {
|
|
10
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
11
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
12
|
+
try {
|
|
13
|
+
const u = new URL(req.url, 'http://localhost')
|
|
14
|
+
const limit = Math.max(1, Math.min(100, parseInt(u.searchParams.get('limit') || '20', 10) || 20))
|
|
15
|
+
const offset = Math.max(0, parseInt(u.searchParams.get('offset') || '0', 10) || 0)
|
|
16
|
+
const data = getMoaHistory(limit, offset)
|
|
17
|
+
writeJson(res, 200, { ok: true, ...data })
|
|
18
|
+
} catch (err) { writeJson(res, 500, { ok: false, error: err?.message || String(err) }) }
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
}, 'dsh-moa: history route')
|
|
22
|
+
|
|
23
|
+
// Route: /dsh-moa/leaderboard
|
|
24
|
+
|
|
25
|
+
ctx.effect(() => {
|
|
26
|
+
return ctx.webServer.register({
|
|
27
|
+
kind: 'exact',
|
|
28
|
+
path: '/dsh-moa/leaderboard',
|
|
29
|
+
handler: async (req, res) => {
|
|
30
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
31
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
32
|
+
try {
|
|
33
|
+
const url = new URL(req.url || '', 'http://127.0.0.1')
|
|
34
|
+
const preset = url.searchParams.get('preset') || null
|
|
35
|
+
const format = url.searchParams.get('format') || 'json'
|
|
36
|
+
|
|
37
|
+
if (format === 'csv') {
|
|
38
|
+
const csv = exportMoaHistory(undefined, { format: 'csv', preset })
|
|
39
|
+
res.writeHead(200, {
|
|
40
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
41
|
+
'Content-Disposition': 'attachment; filename="moa-leaderboard.csv"',
|
|
42
|
+
})
|
|
43
|
+
res.end(csv)
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const data = getMoaLeaderboard(undefined, { preset })
|
|
48
|
+
writeJson(res, 200, { ok: true, ...data })
|
|
49
|
+
} catch (err) { writeJson(res, 500, { ok: false, error: err?.message || String(err) }) }
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
}, 'dsh-moa: leaderboard route')
|
|
53
|
+
|
|
54
|
+
// Route: /dsh-moa/promote (Candidate Override Action)
|
|
55
|
+
|
|
56
|
+
ctx.effect(() => {
|
|
57
|
+
return ctx.webServer.register({
|
|
58
|
+
kind: 'exact',
|
|
59
|
+
path: '/dsh-moa/history/export',
|
|
60
|
+
handler: async (req, res) => {
|
|
61
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
62
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
63
|
+
try {
|
|
64
|
+
const url = new URL(req.url || '', 'http://127.0.0.1')
|
|
65
|
+
const preset = url.searchParams.get('preset') || null
|
|
66
|
+
const format = url.searchParams.get('format') || 'json'
|
|
67
|
+
const data = exportMoaHistory(undefined, { format, preset })
|
|
68
|
+
|
|
69
|
+
const isCsv = format === 'csv'
|
|
70
|
+
res.writeHead(200, {
|
|
71
|
+
'Content-Type': isCsv ? 'text/csv; charset=utf-8' : 'application/json; charset=utf-8',
|
|
72
|
+
'Content-Disposition': `attachment; filename="moa-history.${isCsv ? 'csv' : 'json'}"`,
|
|
73
|
+
})
|
|
74
|
+
res.end(data)
|
|
75
|
+
} catch (err) { writeJson(res, 500, { ok: false, error: err?.message || String(err) }) }
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
}, 'dsh-moa: history export route')
|
|
79
|
+
|
|
80
|
+
// Route: /dsh-moa/runs/<id> (run replay by id and interactive workbench)
|
|
81
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import { isTestEnvironment } from '../history.js'
|
|
4
|
+
import { isLoopbackUrl } from '../file-workspace.js'
|
|
5
|
+
import { runMoAPipeline } from '../moa-runner.js'
|
|
6
|
+
import { writeJson, readBody, isAllowedWorkspace, assertAuthorizedRequest } from '../access.js'
|
|
7
|
+
import { isSafeWriteRequest } from '../updater.js'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export function registerPipelineRoutes(ctx, { live, callLlm, liveCanvas, webServerPort, timeMachineEngine, workspaceRoot }) {
|
|
11
|
+
let _lastRunTimestamp = 0
|
|
12
|
+
const RUN_MIN_INTERVAL_MS = 1000
|
|
13
|
+
|
|
14
|
+
ctx.effect(() => {
|
|
15
|
+
return ctx.webServer.register({
|
|
16
|
+
kind: 'exact',
|
|
17
|
+
path: '/dsh-moa/run',
|
|
18
|
+
handler: async (req, res) => {
|
|
19
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
20
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST required' })
|
|
21
|
+
if (!isSafeWriteRequest(req)) {
|
|
22
|
+
writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
const now = Date.now()
|
|
26
|
+
if (now - _lastRunTimestamp < RUN_MIN_INTERVAL_MS) {
|
|
27
|
+
writeJson(res, 429, { ok: false, error: 'Rate limit exceeded: please wait before starting another run' })
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const raw = await readBody(req)
|
|
33
|
+
const payload = JSON.parse(raw)
|
|
34
|
+
const prompt = payload.prompt || ''
|
|
35
|
+
if (!prompt.trim()) {
|
|
36
|
+
writeJson(res, 400, { ok: false, error: 'Empty prompt' })
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
_lastRunTimestamp = now
|
|
40
|
+
|
|
41
|
+
const cfg = live()
|
|
42
|
+
if (cfg.enabled === false) {
|
|
43
|
+
writeJson(res, 400, { ok: false, error: 'MoA is disabled in settings' })
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
if (payload.timeMachineUrl && !isLoopbackUrl(payload.timeMachineUrl)) {
|
|
47
|
+
writeJson(res, 400, { ok: false, error: 'Invalid timeMachineUrl: loopback host required' })
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
const presetName = payload.preset || cfg.default_preset || 'default'
|
|
51
|
+
const preset = (cfg.presets || []).find((p) => p.name === presetName) || cfg.presets?.[0]
|
|
52
|
+
|
|
53
|
+
const baseWorkspaceRoot = workspaceRoot || ctx?.reflect?.get?.('baseDir', false) || ctx?.reflect?.get?.('workspaceDir', false) || process.cwd()
|
|
54
|
+
const allowedRoots = [
|
|
55
|
+
baseWorkspaceRoot,
|
|
56
|
+
process.cwd(),
|
|
57
|
+
cfg.workspace_root,
|
|
58
|
+
...(Array.isArray(cfg.allowed_workspaces) ? cfg.allowed_workspaces : (cfg.allowed_workspaces ? [cfg.allowed_workspaces] : [])),
|
|
59
|
+
...(isTestEnvironment() ? [os.tmpdir()] : []),
|
|
60
|
+
].filter(Boolean)
|
|
61
|
+
|
|
62
|
+
const targetCwd = payload.cwd ? path.resolve(payload.cwd) : baseWorkspaceRoot
|
|
63
|
+
if (payload.cwd && !isAllowedWorkspace(targetCwd, allowedRoots)) {
|
|
64
|
+
writeJson(res, 403, { ok: false, error: 'Forbidden: cwd escapes allowed workspace boundary' })
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const result = await runMoAPipeline({
|
|
69
|
+
userPrompt: prompt, messages: payload.messages || [], preset, callLlm,
|
|
70
|
+
cwd: targetCwd, prices: cfg.prices || {}, liveCanvas,
|
|
71
|
+
webServerPort: webServerPort || ctx.webServer?.port,
|
|
72
|
+
timeMachineEngine: timeMachineEngine || ctx.reflect?.get?.('timeMachineEngine', false),
|
|
73
|
+
timeMachineUrl: payload.timeMachineUrl, force: false,
|
|
74
|
+
checkpoint: true,
|
|
75
|
+
runId: payload.runId,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
if (result?.skippedFiles > 0) {
|
|
79
|
+
ctx.logger?.warn?.(`[dsh-moa] Workspace scan skipped ${result.skippedFiles} file(s): ${result.skippedList.join(', ')}`)
|
|
80
|
+
}
|
|
81
|
+
if (result?.error) {
|
|
82
|
+
writeJson(res, 500, { ok: false, error: result.error?.message || String(result.error) })
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
writeJson(res, 200, { ok: true, ...result })
|
|
87
|
+
} catch (err) { writeJson(res, 500, { ok: false, error: err?.message || String(err) }) }
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
}, 'dsh-moa: run pipeline route')
|
|
91
|
+
}
|