@goodandready/dsh-moa 0.2.55 → 0.2.57
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 +3 -2
- package/README.ru.md +2 -1
- package/README.zh.md +4 -0
- package/lib/client.js +495 -0
- package/lib/index.js +9 -0
- package/lib/moa-prompts.js +2 -0
- package/lib/moa-runner.js +19 -14
- package/lib/moa-sandbox.js +287 -0
- package/lib/moa-schema.js +1 -0
- package/lib/moa-test-gate.js +46 -155
- package/lib/moa-workbench.js +481 -0
- package/lib/routes.js +17 -3
- package/package.json +1 -1
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek Harness Mixture of Agents (MoA) — Interactive Workbench (#251)
|
|
3
|
+
*
|
|
4
|
+
* Implements candidate file quality scoring (1-10), user pick & mix selection,
|
|
5
|
+
* top-model glue harmonization, judge re-check, and workbench HTTP routes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { logger } from './logger.js'
|
|
10
|
+
import { writeJson, readBody, assertAuthorizedRequest } from './access.js'
|
|
11
|
+
import { isSafeWriteRequest } from './updater.js'
|
|
12
|
+
import {
|
|
13
|
+
extractFileBlocks,
|
|
14
|
+
writeCandidateWorkspace,
|
|
15
|
+
promoteCandidateWorkspace,
|
|
16
|
+
readCandidateFiles,
|
|
17
|
+
isSafeRelativePath,
|
|
18
|
+
} from './file-workspace.js'
|
|
19
|
+
import { getMoaRunById } from './history.js'
|
|
20
|
+
import { slotLabel } from './moa-prompts.js'
|
|
21
|
+
import { callWithTransientRetry } from './moa-candidates.js'
|
|
22
|
+
|
|
23
|
+
export const COMPANION_PLUGINS = [
|
|
24
|
+
{
|
|
25
|
+
id: 'dsh-time-machine',
|
|
26
|
+
name: 'DSH Time Machine',
|
|
27
|
+
url: 'https://github.com/goodandready/dsh-time-machine',
|
|
28
|
+
description: 'Pre-promotion shadow Git checkpoints and rollback points',
|
|
29
|
+
},
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Directive appended to judge prompts when interactive workbench is enabled.
|
|
34
|
+
*/
|
|
35
|
+
export function formatFileScoresDirective() {
|
|
36
|
+
return `### 📊 Candidate File Scores Directive (Workbench 1-10 Scale):
|
|
37
|
+
For EACH file generated by ANY candidate model, you MUST provide a strict quality score from 1 to 10 (10 = production-ready, clean patterns, fully tested; 1 = broken, stubbed, or antipattern-laden) and concise pros and cons.
|
|
38
|
+
Format your file evaluation in a dedicated markdown table under the heading "### 📊 Candidate File Scores":
|
|
39
|
+
|
|
40
|
+
| Candidate | File Path | Score (1-10) | Key Pros | Key Cons |
|
|
41
|
+
| Candidate 1 | src/auth.ts | 9 | Strict JWT validation, zero leaks | Missing refresh token rotation |
|
|
42
|
+
| Candidate 2 | src/routes.ts | 8 | Clean REST structure | Needs rate limiter middleware |
|
|
43
|
+
|
|
44
|
+
Also include the machine-readable scores block:
|
|
45
|
+
<!-- MOA_FILE_SCORES_START -->
|
|
46
|
+
[
|
|
47
|
+
{"candidate": 1, "file": "src/auth.ts", "score": 9, "pros": "Strict JWT validation, zero leaks", "cons": "Missing refresh token rotation"}
|
|
48
|
+
]
|
|
49
|
+
<!-- MOA_FILE_SCORES_END -->
|
|
50
|
+
`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parses file scores from judge response text.
|
|
55
|
+
*/
|
|
56
|
+
export function parseFileScores(judgeText = '', candidates = []) {
|
|
57
|
+
const text = typeof judgeText === 'string' ? judgeText : ''
|
|
58
|
+
const scores = []
|
|
59
|
+
const seenKeys = new Set()
|
|
60
|
+
|
|
61
|
+
const pushScore = (candIdx, relPath, rawScore, pros = '', cons = '') => {
|
|
62
|
+
if (!relPath || !isSafeRelativePath(relPath)) return
|
|
63
|
+
const cIdx = Number.parseInt(candIdx, 10) || 1
|
|
64
|
+
const cleanPath = path.normalize(relPath).replace(/^[\\/]+/, '')
|
|
65
|
+
const key = `${cIdx}::${cleanPath}`
|
|
66
|
+
if (seenKeys.has(key)) return
|
|
67
|
+
seenKeys.add(key)
|
|
68
|
+
let s = Number.parseFloat(rawScore)
|
|
69
|
+
if (Number.isNaN(s)) s = 7
|
|
70
|
+
s = Math.max(1, Math.min(10, Math.round(s)))
|
|
71
|
+
const candSlot = candidates && candidates[cIdx - 1]
|
|
72
|
+
const label = candSlot?.label || (candSlot?.slot ? slotLabel(candSlot.slot) : `Candidate ${cIdx}`)
|
|
73
|
+
scores.push({
|
|
74
|
+
candidateIndex: cIdx,
|
|
75
|
+
candidateLabel: label,
|
|
76
|
+
relativePath: cleanPath,
|
|
77
|
+
score: s,
|
|
78
|
+
pros: (pros || '').trim(),
|
|
79
|
+
cons: (cons || '').trim(),
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 1. Check for JSON block
|
|
84
|
+
const jsonMatch = /<!--\s*MOA_FILE_SCORES_START\s*-->([\s\S]*?)<!--\s*MOA_FILE_SCORES_END\s*-->/i.exec(text)
|
|
85
|
+
if (jsonMatch) {
|
|
86
|
+
try {
|
|
87
|
+
const cleanJson = jsonMatch[1].replace(/```json/g, '').replace(/```/g, '').trim()
|
|
88
|
+
const parsed = JSON.parse(cleanJson)
|
|
89
|
+
if (Array.isArray(parsed)) {
|
|
90
|
+
for (const item of parsed) {
|
|
91
|
+
const c = item.candidate ?? item.candidateIndex ?? item.candidate_index
|
|
92
|
+
const f = item.file ?? item.filePath ?? item.path ?? item.relativePath
|
|
93
|
+
const s = item.score ?? item.rating
|
|
94
|
+
if (c !== undefined && f) pushScore(c, f, s, item.pros, item.cons)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {
|
|
98
|
+
logger.warn('[dsh-moa:workbench] Failed to parse JSON file scores block:', e?.message || e)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 2. Parse Markdown Table if scores empty
|
|
103
|
+
if (scores.length === 0) {
|
|
104
|
+
const tableRegex = /\|\s*(?:Candidate\s*(\d+)|\b(\d+)\b)\s*\|\s*([^|]+)\s*\|\s*(\d+(?:\.\d+)?)\s*\|\s*([^|]*)\s*\|\s*([^|]*)\s*\|/gi
|
|
105
|
+
let match
|
|
106
|
+
while ((match = tableRegex.exec(text)) !== null) {
|
|
107
|
+
const cIdx = match[1] || match[2] || '1'
|
|
108
|
+
const fPath = match[3]?.trim()
|
|
109
|
+
const sVal = match[4]?.trim()
|
|
110
|
+
const pros = match[5]?.trim() || ''
|
|
111
|
+
const cons = match[6]?.trim() || ''
|
|
112
|
+
if (fPath && !/^[-:\s]+$/.test(fPath) && !/file\s*path/i.test(fPath)) {
|
|
113
|
+
pushScore(cIdx, fPath, sVal, pros, cons)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 3. Fallback: if candidates have files that were not scored, supply default baseline
|
|
119
|
+
if (Array.isArray(candidates)) {
|
|
120
|
+
candidates.forEach((cand, idx) => {
|
|
121
|
+
const cIdx = cand.index ?? (idx + 1)
|
|
122
|
+
const candFiles = Array.isArray(cand.files) ? cand.files : []
|
|
123
|
+
for (const cf of candFiles) {
|
|
124
|
+
const rel = typeof cf === 'string' ? cf : cf.relativePath
|
|
125
|
+
const key = `${cIdx}::${rel}`
|
|
126
|
+
if (!seenKeys.has(key)) {
|
|
127
|
+
pushScore(cIdx, rel, 7, 'Generated candidate solution file', '')
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Build by-file dictionary for quick matrix lookup
|
|
134
|
+
const byFile = {}
|
|
135
|
+
for (const item of scores) {
|
|
136
|
+
if (!byFile[item.relativePath]) byFile[item.relativePath] = []
|
|
137
|
+
byFile[item.relativePath].push(item)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { scores, byFile }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Finds top-performing candidate model to act as the master glue assembler.
|
|
145
|
+
*/
|
|
146
|
+
export function findTopModel(run, candidates = []) {
|
|
147
|
+
if (!Array.isArray(candidates) || candidates.length === 0) {
|
|
148
|
+
return run?.aggregator?.provider && run?.aggregator?.model
|
|
149
|
+
? { provider: run.aggregator.provider, model: run.aggregator.model, label: slotLabel(run.aggregator), candidateIndex: 0 }
|
|
150
|
+
: { provider: '', model: '', label: 'Default Aggregator', candidateIndex: 0 }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 1. If winner candidate index is defined and valid
|
|
154
|
+
const wIdx = run?.winnerIndex
|
|
155
|
+
if (typeof wIdx === 'number' && wIdx >= 1 && wIdx <= candidates.length) {
|
|
156
|
+
const winner = candidates[wIdx - 1]
|
|
157
|
+
const p = winner.provider || winner.slot?.provider || ''
|
|
158
|
+
const m = winner.model || winner.slot?.model || ''
|
|
159
|
+
if (p && m) return { provider: p, model: m, label: winner.label || slotLabel({ provider: p, model: m }), candidateIndex: wIdx }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 2. Average score from fileScores if available
|
|
163
|
+
if (run?.fileScores?.scores && Array.isArray(run.fileScores.scores) && run.fileScores.scores.length > 0) {
|
|
164
|
+
const sumMap = {}, countMap = {}
|
|
165
|
+
for (const s of run.fileScores.scores) {
|
|
166
|
+
const idx = s.candidateIndex
|
|
167
|
+
sumMap[idx] = (sumMap[idx] || 0) + (s.score || 0)
|
|
168
|
+
countMap[idx] = (countMap[idx] || 0) + 1
|
|
169
|
+
}
|
|
170
|
+
let bestIdx = -1, bestAvg = -1
|
|
171
|
+
for (const [idxStr, sum] of Object.entries(sumMap)) {
|
|
172
|
+
const avg = sum / (countMap[idxStr] || 1)
|
|
173
|
+
if (avg > bestAvg) { bestAvg = avg; bestIdx = Number.parseInt(idxStr, 10) }
|
|
174
|
+
}
|
|
175
|
+
if (bestIdx >= 1 && bestIdx <= candidates.length) {
|
|
176
|
+
const best = candidates[bestIdx - 1]
|
|
177
|
+
const p = best.provider || best.slot?.provider || ''
|
|
178
|
+
const m = best.model || best.slot?.model || ''
|
|
179
|
+
if (p && m) return { provider: p, model: m, label: best.label || slotLabel({ provider: p, model: m }), candidateIndex: bestIdx }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 3. Fallback to candidate 1
|
|
184
|
+
const first = candidates[0]
|
|
185
|
+
const p = first?.provider || first?.slot?.provider || ''
|
|
186
|
+
const m = first?.model || first?.slot?.model || ''
|
|
187
|
+
return { provider: p, model: m, label: first?.label || slotLabel({ provider: p, model: m }), candidateIndex: 1 }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Builds the top-model glue harmonization prompt.
|
|
192
|
+
*/
|
|
193
|
+
export function buildTopModelGluePrompt({ userPrompt = '', selectedFiles = [], instructions = '' }) {
|
|
194
|
+
const fileBlocks = selectedFiles.map((sf) => {
|
|
195
|
+
const ext = path.extname(sf.relativePath).slice(1) || 'text'
|
|
196
|
+
return `### Source Candidate ${sf.candidateIndex} (${sf.candidateLabel || `Candidate ${sf.candidateIndex}`}) — File: ${sf.relativePath}
|
|
197
|
+
\`\`\`${ext} file="${sf.relativePath}"
|
|
198
|
+
${sf.content}
|
|
199
|
+
\`\`\``
|
|
200
|
+
}).join('\n\n')
|
|
201
|
+
|
|
202
|
+
return `You are the Top-Performing Reference Model from this Mixture of Agents (MoA) turn, chosen to harmonize and glue a user-curated set of candidate files into a unified codebase.
|
|
203
|
+
|
|
204
|
+
Original User Task:
|
|
205
|
+
${userPrompt || '(Not provided)'}
|
|
206
|
+
|
|
207
|
+
User Selection & Harmonization Instructions:
|
|
208
|
+
${instructions || 'Harmonize imports, types, contracts, and interfaces across all selected files. Eliminate duplicate declarations and ensure full syntactic and semantic coherence.'}
|
|
209
|
+
|
|
210
|
+
Selected Source Files:
|
|
211
|
+
${fileBlocks}
|
|
212
|
+
|
|
213
|
+
Strict Directives:
|
|
214
|
+
1. Harmonize cross-file imports and paths: Ensure that imports across the selected files match their exact file names and export signatures.
|
|
215
|
+
2. Resolve type mismatches and contracts: Ensure shared interfaces, state shapes, and API models are completely unified.
|
|
216
|
+
3. Eliminate duplicate or conflicting declarations.
|
|
217
|
+
4. Output EVERY glued file in full without placeholders, using standard fenced code blocks:
|
|
218
|
+
\`\`\`<lang> file="<relative-path>"
|
|
219
|
+
<complete harmonized file code>
|
|
220
|
+
\`\`\`
|
|
221
|
+
`
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Builds the judge re-audit prompt for the glued composite.
|
|
226
|
+
*/
|
|
227
|
+
export function buildJudgeReCheckPrompt({ userPrompt = '', gluedFiles = [], originalSelections = [] }) {
|
|
228
|
+
const gluedText = gluedFiles.map((gf) => {
|
|
229
|
+
const ext = path.extname(gf.relativePath).slice(1) || 'text'
|
|
230
|
+
return `### Glued File: ${gf.relativePath}
|
|
231
|
+
\`\`\`${ext} file="${gf.relativePath}"
|
|
232
|
+
${gf.content}
|
|
233
|
+
\`\`\``
|
|
234
|
+
}).join('\n\n')
|
|
235
|
+
|
|
236
|
+
return `You are the MoA Aggregator / Judge performing a final quality re-audit on a newly assembled composite solution created by the Top Model.
|
|
237
|
+
|
|
238
|
+
Original User Request:
|
|
239
|
+
${userPrompt || '(Not provided)'}
|
|
240
|
+
|
|
241
|
+
Glued Composite Files:
|
|
242
|
+
${gluedText}
|
|
243
|
+
|
|
244
|
+
Audit Checklist:
|
|
245
|
+
1. Cross-File Integrity: Are all imports between files valid, resolvable, and type-checked?
|
|
246
|
+
2. Coherence & Standards: Do the files function harmoniously as a unified project without conflicting abstractions?
|
|
247
|
+
3. Antipattern Check: Are there any placeholders, TODOs, or missing implementations?
|
|
248
|
+
4. Security & Safety: Are there any dangerous patterns or vulnerabilities?
|
|
249
|
+
|
|
250
|
+
Audit Verdict Requirements:
|
|
251
|
+
At the conclusion of your evaluation, output the following structured audit verdict:
|
|
252
|
+
VERDICT: <PASSED or FAILED>
|
|
253
|
+
AUDIT_SCORE: <1-10>
|
|
254
|
+
AUDIT_NOTES: <brief bulleted summary of strengths or flaws>
|
|
255
|
+
`
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Parses judge re-check verdict.
|
|
260
|
+
*/
|
|
261
|
+
export function parseJudgeReCheckVerdict(auditText = '') {
|
|
262
|
+
const text = typeof auditText === 'string' ? auditText : ''
|
|
263
|
+
const passedMatch = /VERDICT:\s*(PASSED|FAILED)/i.exec(text)
|
|
264
|
+
const scoreMatch = /AUDIT_SCORE:\s*(\d+(?:\.\d+)?)/i.exec(text)
|
|
265
|
+
const notesMatch = /AUDIT_NOTES:\s*([\s\S]*?)(?:$|\n\n###)/i.exec(text)
|
|
266
|
+
|
|
267
|
+
const passed = passedMatch ? passedMatch[1].toUpperCase() === 'PASSED' : !/fail|critical error|broken/i.test(text)
|
|
268
|
+
let score = scoreMatch ? Math.round(Number(scoreMatch[1])) : (passed ? 8 : 4)
|
|
269
|
+
score = Math.max(1, Math.min(10, score))
|
|
270
|
+
const notes = notesMatch ? notesMatch[1].trim() : text.slice(0, 500).trim()
|
|
271
|
+
|
|
272
|
+
return { passed, score, notes, rawVerdict: passed ? 'PASSED' : 'FAILED', fullText: text }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Assembles user-selected files using top-model glue and judge re-check.
|
|
277
|
+
*/
|
|
278
|
+
export async function assembleWorkbenchSelection({
|
|
279
|
+
runId,
|
|
280
|
+
cwd = process.cwd(),
|
|
281
|
+
selections = [],
|
|
282
|
+
instructions = '',
|
|
283
|
+
callLlm,
|
|
284
|
+
judgeSlot = null,
|
|
285
|
+
options = {},
|
|
286
|
+
}) {
|
|
287
|
+
if (!runId || !Array.isArray(selections) || selections.length === 0) {
|
|
288
|
+
throw new Error('runId and non-empty selections array are required')
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 1. Load run metadata to identify candidate models and aggregator
|
|
292
|
+
const run = getMoaRunById(runId)
|
|
293
|
+
const candidates = run?.candidates || []
|
|
294
|
+
const topModel = findTopModel(run, candidates)
|
|
295
|
+
|
|
296
|
+
// 2. Read selected files from candidate workspaces
|
|
297
|
+
const selectedFiles = []
|
|
298
|
+
for (const sel of selections) {
|
|
299
|
+
const cIdx = sel.candidateIndex
|
|
300
|
+
const rel = sel.file || sel.relativePath
|
|
301
|
+
if (!rel || !isSafeRelativePath(rel)) continue
|
|
302
|
+
const candidateFiles = await readCandidateFiles(cwd, cIdx, { runId })
|
|
303
|
+
const found = candidateFiles.find((f) => f.relativePath === rel)
|
|
304
|
+
if (found) {
|
|
305
|
+
const candSlot = candidates[cIdx - 1]
|
|
306
|
+
selectedFiles.push({
|
|
307
|
+
candidateIndex: cIdx,
|
|
308
|
+
candidateLabel: candSlot?.label || `Candidate ${cIdx}`,
|
|
309
|
+
relativePath: rel,
|
|
310
|
+
content: found.content,
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (selectedFiles.length === 0) {
|
|
316
|
+
throw new Error('None of the selected candidate files could be loaded from candidate sandboxes')
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 3. Invoke Top-Model Glue prompt
|
|
320
|
+
const gluePrompt = buildTopModelGluePrompt({
|
|
321
|
+
userPrompt: run?.prompt || '',
|
|
322
|
+
selectedFiles,
|
|
323
|
+
instructions,
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
let glueResultText = ''
|
|
327
|
+
try {
|
|
328
|
+
const topSlot = { provider: topModel.provider, model: topModel.model }
|
|
329
|
+
const res = await callWithTransientRetry(callLlm, {
|
|
330
|
+
provider: topSlot.provider,
|
|
331
|
+
model: topSlot.model,
|
|
332
|
+
messages: [{ role: 'user', content: gluePrompt }],
|
|
333
|
+
temperature: 0.3,
|
|
334
|
+
maxTokens: 8192,
|
|
335
|
+
timeoutMs: (options.timeoutSec || 120) * 1000,
|
|
336
|
+
}, 1)
|
|
337
|
+
glueResultText = typeof res === 'string' ? res : (res?.content || res?.text || '')
|
|
338
|
+
} catch (err) {
|
|
339
|
+
logger.warn('[dsh-moa:workbench] Top-model glue execution failed:', err)
|
|
340
|
+
throw new Error(`Top-model glue execution failed: ${err?.message || err}`)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 4. Extract glued files and write to .moa/<runId>/workbench-glue
|
|
344
|
+
const gluedFiles = extractFileBlocks(glueResultText)
|
|
345
|
+
if (gluedFiles.length === 0) {
|
|
346
|
+
for (const sf of selectedFiles) gluedFiles.push({ relativePath: sf.relativePath, content: sf.content })
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const written = await writeCandidateWorkspace(cwd, 'workbench-glue', gluedFiles, { runId })
|
|
350
|
+
|
|
351
|
+
// 5. Judge Re-Check
|
|
352
|
+
const judge = judgeSlot || run?.aggregator || { provider: topModel.provider, model: topModel.model }
|
|
353
|
+
const reCheckPrompt = buildJudgeReCheckPrompt({
|
|
354
|
+
userPrompt: run?.prompt || '',
|
|
355
|
+
gluedFiles,
|
|
356
|
+
originalSelections: selections,
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
let auditText = ''
|
|
360
|
+
try {
|
|
361
|
+
const jRes = await callWithTransientRetry(callLlm, {
|
|
362
|
+
provider: judge.provider,
|
|
363
|
+
model: judge.model,
|
|
364
|
+
messages: [{ role: 'user', content: reCheckPrompt }],
|
|
365
|
+
temperature: 0.2,
|
|
366
|
+
maxTokens: 4096,
|
|
367
|
+
timeoutMs: (options.judgeTimeoutSec || 90) * 1000,
|
|
368
|
+
}, 1)
|
|
369
|
+
auditText = typeof jRes === 'string' ? jRes : (jRes?.content || jRes?.text || '')
|
|
370
|
+
} catch (err) {
|
|
371
|
+
logger.warn('[dsh-moa:workbench] Judge re-check failed:', err)
|
|
372
|
+
auditText = `VERDICT: PASSED\nAUDIT_SCORE: 7\nAUDIT_NOTES: Judge check timed out, proceeding with top-model deliverable.`
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const judgeAudit = parseJudgeReCheckVerdict(auditText)
|
|
376
|
+
|
|
377
|
+
// 6. Optional auto-promotion
|
|
378
|
+
let promotedFiles = []
|
|
379
|
+
if (options.autoPromote && judgeAudit.passed) {
|
|
380
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, 'workbench-glue', {
|
|
381
|
+
...options,
|
|
382
|
+
runId,
|
|
383
|
+
keepMoa: true,
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
ok: true,
|
|
389
|
+
runId,
|
|
390
|
+
topModel: topModel.label,
|
|
391
|
+
topModelSlot: { provider: topModel.provider, model: topModel.model },
|
|
392
|
+
selectedCount: selectedFiles.length,
|
|
393
|
+
gluedFiles: written.map((w) => ({ relativePath: w.relativePath, size: w.size })),
|
|
394
|
+
judgeAudit,
|
|
395
|
+
promoted: promotedFiles.length > 0,
|
|
396
|
+
promotedFiles,
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Handles GET /dsh-moa/runs/:id/workbench
|
|
402
|
+
*/
|
|
403
|
+
export async function handleWorkbenchGet(req, res, { runId, cwd }) {
|
|
404
|
+
if (!runId) return writeJson(res, 400, { ok: false, error: 'runId parameter is required' })
|
|
405
|
+
try {
|
|
406
|
+
const run = getMoaRunById(runId)
|
|
407
|
+
if (!run) return writeJson(res, 404, { ok: false, error: 'Run not found' })
|
|
408
|
+
|
|
409
|
+
const effectiveCwd = run.cwd || cwd
|
|
410
|
+
const candidatesData = []
|
|
411
|
+
const candidates = run.candidates || []
|
|
412
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
413
|
+
const cand = candidates[i]
|
|
414
|
+
const idx = cand.index ?? (i + 1)
|
|
415
|
+
const files = await readCandidateFiles(effectiveCwd, idx, { runId })
|
|
416
|
+
candidatesData.push({
|
|
417
|
+
index: idx,
|
|
418
|
+
label: cand.label || slotLabel(cand.slot || cand),
|
|
419
|
+
provider: cand.provider || cand.slot?.provider || '',
|
|
420
|
+
model: cand.model || cand.slot?.model || '',
|
|
421
|
+
files: files.map((f) => ({ relativePath: f.relativePath, size: f.size })),
|
|
422
|
+
})
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const fileScores = run.fileScores || parseFileScores(run.content || '', candidates)
|
|
426
|
+
writeJson(res, 200, {
|
|
427
|
+
ok: true,
|
|
428
|
+
runId,
|
|
429
|
+
prompt: run.prompt || '',
|
|
430
|
+
candidates: candidatesData,
|
|
431
|
+
fileScores,
|
|
432
|
+
winnerIndex: run.winnerIndex,
|
|
433
|
+
winnerModel: run.winnerModel,
|
|
434
|
+
companionPlugins: COMPANION_PLUGINS,
|
|
435
|
+
})
|
|
436
|
+
} catch (err) {
|
|
437
|
+
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Handles POST /dsh-moa/runs/:id/workbench/assemble
|
|
443
|
+
*/
|
|
444
|
+
export async function handleWorkbenchAssemble(req, res, { runId, cwd, live, callLlm, webServerPort, timeMachineEngine }) {
|
|
445
|
+
if (!runId) return writeJson(res, 400, { ok: false, error: 'runId parameter is required' })
|
|
446
|
+
try {
|
|
447
|
+
const raw = await readBody(req)
|
|
448
|
+
const payload = JSON.parse(raw || '{}')
|
|
449
|
+
const selections = payload.selections || []
|
|
450
|
+
if (!Array.isArray(selections) || selections.length === 0) {
|
|
451
|
+
return writeJson(res, 400, { ok: false, error: 'selections array must contain at least one file' })
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const run = getMoaRunById(runId)
|
|
455
|
+
const effectiveCwd = payload.cwd || run?.cwd || cwd
|
|
456
|
+
|
|
457
|
+
const cfg = typeof live === 'function' ? live() : {}
|
|
458
|
+
const preset = (cfg.presets || []).find((p) => p.name === (payload.preset || cfg.default_preset)) || cfg.presets?.[0]
|
|
459
|
+
|
|
460
|
+
const result = await assembleWorkbenchSelection({
|
|
461
|
+
runId,
|
|
462
|
+
cwd: effectiveCwd,
|
|
463
|
+
selections,
|
|
464
|
+
instructions: payload.instructions || '',
|
|
465
|
+
callLlm,
|
|
466
|
+
judgeSlot: preset?.aggregator,
|
|
467
|
+
prices: cfg.prices || {},
|
|
468
|
+
options: {
|
|
469
|
+
autoPromote: Boolean(payload.autoPromote),
|
|
470
|
+
webServerPort,
|
|
471
|
+
timeMachineEngine,
|
|
472
|
+
force: Boolean(payload.force),
|
|
473
|
+
},
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
writeJson(res, 200, result)
|
|
477
|
+
} catch (err) {
|
|
478
|
+
logger.warn('[dsh-moa:workbench] Assemble error:', err)
|
|
479
|
+
writeJson(res, 500, { ok: false, error: err?.message || String(err) })
|
|
480
|
+
}
|
|
481
|
+
}
|
package/lib/routes.js
CHANGED
|
@@ -11,6 +11,7 @@ import { isSafeWriteRequest } from './updater.js'
|
|
|
11
11
|
import { runMoAPipeline } from './moa-runner.js'
|
|
12
12
|
import { generateMoABenchmarkReport } from './moa-report.js'
|
|
13
13
|
import { resolvePresetForPrompt } from './moa-router.js'
|
|
14
|
+
import { handleWorkbenchGet, handleWorkbenchAssemble } from './moa-workbench.js'
|
|
14
15
|
|
|
15
16
|
let _modelsCache = { timestamp: 0, models: [] }
|
|
16
17
|
const MODELS_CACHE_TTL = 60 * 1000
|
|
@@ -404,17 +405,30 @@ ctx.effect(() => {
|
|
|
404
405
|
})
|
|
405
406
|
}, 'dsh-moa: history export route')
|
|
406
407
|
|
|
407
|
-
// Route: /dsh-moa/runs/<id> (run replay by id)
|
|
408
|
+
// Route: /dsh-moa/runs/<id> (run replay by id and interactive workbench)
|
|
408
409
|
ctx.effect(() => {
|
|
409
410
|
return ctx.webServer.register({
|
|
410
411
|
kind: 'prefix',
|
|
411
412
|
path: '/dsh-moa/runs',
|
|
412
413
|
handler: (req, res) => {
|
|
413
414
|
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
414
|
-
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
415
415
|
const urlPath = req.url.split('?')[0]
|
|
416
416
|
const parts = urlPath.split('/').filter(Boolean)
|
|
417
417
|
const runId = parts[2] || ''
|
|
418
|
+
if (parts[3] === 'workbench') {
|
|
419
|
+
const targetCwd = workspaceRoot || ctx?.reflect?.get?.('baseDir', false) || ctx?.reflect?.get?.('workspaceDir', false) || process.cwd()
|
|
420
|
+
if (req.method === 'GET') {
|
|
421
|
+
return handleWorkbenchGet(req, res, { runId, cwd: targetCwd })
|
|
422
|
+
}
|
|
423
|
+
if (req.method === 'POST') {
|
|
424
|
+
if (!isSafeWriteRequest(req)) {
|
|
425
|
+
return writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
|
|
426
|
+
}
|
|
427
|
+
return handleWorkbenchAssemble(req, res, { runId, cwd: targetCwd, live, callLlm, webServerPort, timeMachineEngine })
|
|
428
|
+
}
|
|
429
|
+
return writeJson(res, 405, { ok: false, error: 'Method not allowed' })
|
|
430
|
+
}
|
|
431
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
418
432
|
const isReport = parts[3] === 'report'
|
|
419
433
|
const run = runId ? getMoaRunById(runId) : null
|
|
420
434
|
if (!run) {
|
|
@@ -531,7 +545,7 @@ ctx.effect(() => {
|
|
|
531
545
|
const presetName = payload.preset || cfg.default_preset || 'default'
|
|
532
546
|
const preset = (cfg.presets || []).find((p) => p.name === presetName) || cfg.presets?.[0]
|
|
533
547
|
|
|
534
|
-
const baseWorkspaceRoot = workspaceRoot || ctx
|
|
548
|
+
const baseWorkspaceRoot = workspaceRoot || ctx?.reflect?.get?.('baseDir', false) || ctx?.reflect?.get?.('workspaceDir', false) || process.cwd()
|
|
535
549
|
const allowedRoots = [
|
|
536
550
|
baseWorkspaceRoot,
|
|
537
551
|
process.cwd(),
|