@goodandready/dsh-moa 0.2.25 → 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/README.md +30 -0
- package/README.ru.md +30 -0
- package/lib/client.js +248 -0
- package/lib/history.js +11 -0
- package/lib/index.js +41 -0
- package/lib/moa-budget.js +97 -0
- package/lib/moa-candidates.js +90 -22
- package/lib/moa-context.js +70 -0
- package/lib/moa-multi-judge.js +301 -0
- package/lib/moa-prompts.js +25 -6
- package/lib/moa-report.js +124 -0
- package/lib/moa-router.js +102 -0
- package/lib/moa-runner.js +131 -152
- package/lib/moa-stream.js +117 -0
- package/lib/moa-test-gate.js +236 -0
- package/lib/routes.js +58 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import fsSync from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawn } from 'node:child_process'
|
|
6
|
+
import { assertPathContained } from './file-workspace.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolves the directory for candidate sandbox.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveCandidateDir(baseDir, candidateIndex) {
|
|
12
|
+
if (!baseDir || candidateIndex === null || candidateIndex === undefined) return null
|
|
13
|
+
const targetStr = String(candidateIndex).trim()
|
|
14
|
+
const sanitizedTarget = targetStr.replace(/[^a-zA-Z0-9_-]/g, '')
|
|
15
|
+
const sub = /^\d+$/.test(sanitizedTarget) ? `candidate-${sanitizedTarget}` : (sanitizedTarget || 'candidate-1')
|
|
16
|
+
const moaRoot = path.join(baseDir, '.moa')
|
|
17
|
+
return assertPathContained(moaRoot, sub)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Executes a test command in a candidate sandbox or ephemeral test staging directory.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} params
|
|
24
|
+
* @param {string} params.cwd - Project root workspace directory
|
|
25
|
+
* @param {number|string} params.candidateIndex - Index of candidate (1, 2, ...)
|
|
26
|
+
* @param {string} params.testCommand - Shell command to execute (e.g. "npm test", "node --test")
|
|
27
|
+
* @param {number} [params.timeoutMs=15000] - Execution timeout in ms
|
|
28
|
+
* @param {number} [params.maxOutputChars=2000] - Maximum captured output chars
|
|
29
|
+
* @param {function} [params.execFn] - Optional mock/test runner function
|
|
30
|
+
* @returns {Promise<object|null>} Test result object or null if skipped
|
|
31
|
+
*/
|
|
32
|
+
export async function runCandidateTestGate({
|
|
33
|
+
cwd,
|
|
34
|
+
candidateIndex,
|
|
35
|
+
testCommand,
|
|
36
|
+
timeoutMs = 15000,
|
|
37
|
+
maxOutputChars = 2000,
|
|
38
|
+
execFn = null,
|
|
39
|
+
}) {
|
|
40
|
+
if (!cwd || !testCommand || typeof testCommand !== 'string') {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const trimmedCmd = testCommand.trim()
|
|
45
|
+
if (!trimmedCmd) return null
|
|
46
|
+
|
|
47
|
+
const candidateDir = resolveCandidateDir(cwd, candidateIndex)
|
|
48
|
+
if (!candidateDir || !fsSync.existsSync(candidateDir)) {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const startTime = Date.now()
|
|
53
|
+
let stageDir = null
|
|
54
|
+
let executionDir = candidateDir
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const basePkg = path.join(cwd, 'package.json')
|
|
58
|
+
const candPkg = path.join(candidateDir, 'package.json')
|
|
59
|
+
if (fsSync.existsSync(basePkg) && !fsSync.existsSync(candPkg)) {
|
|
60
|
+
stageDir = await fs.mkdtemp(path.join(os.tmpdir(), `moa-test-stage-${candidateIndex}-`))
|
|
61
|
+
|
|
62
|
+
await fs.cp(cwd, stageDir, {
|
|
63
|
+
recursive: true,
|
|
64
|
+
filter: (src) => {
|
|
65
|
+
const rel = path.relative(cwd, src)
|
|
66
|
+
if (!rel) return true
|
|
67
|
+
const parts = rel.split(path.sep)
|
|
68
|
+
if (parts[0] === 'node_modules' || parts[0] === '.git' || parts[0] === '.moa') {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
return true
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
await fs.cp(candidateDir, stageDir, { recursive: true })
|
|
76
|
+
executionDir = stageDir
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const resolvedCmd = trimmedCmd
|
|
80
|
+
.replaceAll('{candidateDir}', candidateDir)
|
|
81
|
+
.replaceAll('{baseDir}', cwd)
|
|
82
|
+
.replaceAll('{stageDir}', executionDir)
|
|
83
|
+
|
|
84
|
+
let exitCode = 0
|
|
85
|
+
let stdout = ''
|
|
86
|
+
let stderr = ''
|
|
87
|
+
|
|
88
|
+
if (typeof execFn === 'function') {
|
|
89
|
+
const customRes = await execFn({
|
|
90
|
+
cwd: executionDir,
|
|
91
|
+
candidateDir,
|
|
92
|
+
command: resolvedCmd,
|
|
93
|
+
timeoutMs,
|
|
94
|
+
})
|
|
95
|
+
exitCode = customRes?.exitCode ?? (customRes?.passed ? 0 : 1)
|
|
96
|
+
stdout = customRes?.stdout || ''
|
|
97
|
+
stderr = customRes?.stderr || ''
|
|
98
|
+
} else {
|
|
99
|
+
const nodePath = path.join(cwd, 'node_modules')
|
|
100
|
+
const env = {
|
|
101
|
+
...process.env,
|
|
102
|
+
MOA_CANDIDATE_INDEX: String(candidateIndex),
|
|
103
|
+
MOA_CANDIDATE_DIR: candidateDir,
|
|
104
|
+
MOA_BASE_DIR: cwd,
|
|
105
|
+
NODE_PATH: process.env.NODE_PATH ? `${nodePath}:${process.env.NODE_PATH}` : nodePath,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const runPromise = new Promise((resolve) => {
|
|
109
|
+
let child
|
|
110
|
+
try {
|
|
111
|
+
child = spawn(resolvedCmd, {
|
|
112
|
+
cwd: executionDir,
|
|
113
|
+
shell: true,
|
|
114
|
+
env,
|
|
115
|
+
timeout: timeoutMs,
|
|
116
|
+
})
|
|
117
|
+
} catch (spawnErr) {
|
|
118
|
+
resolve({ exitCode: 1, stdout: '', stderr: spawnErr.message || String(spawnErr) })
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const outChunks = []
|
|
123
|
+
const errChunks = []
|
|
124
|
+
|
|
125
|
+
child.stdout?.on('data', (d) => outChunks.push(d))
|
|
126
|
+
child.stderr?.on('data', (d) => errChunks.push(d))
|
|
127
|
+
|
|
128
|
+
child.on('error', (err) => {
|
|
129
|
+
resolve({ exitCode: 1, stdout: Buffer.concat(outChunks).toString('utf8'), stderr: err.message || String(err) })
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
child.on('close', (code, sig) => {
|
|
133
|
+
const combinedErr = Buffer.concat(errChunks).toString('utf8')
|
|
134
|
+
const finalErr = sig ? `${combinedErr}\nProcess terminated by signal ${sig}`.trim() : combinedErr
|
|
135
|
+
resolve({
|
|
136
|
+
exitCode: code ?? (sig ? 128 : 1),
|
|
137
|
+
stdout: Buffer.concat(outChunks).toString('utf8'),
|
|
138
|
+
stderr: finalErr,
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const execResult = await runPromise
|
|
144
|
+
exitCode = execResult.exitCode
|
|
145
|
+
stdout = execResult.stdout
|
|
146
|
+
stderr = execResult.stderr
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const durationMs = Date.now() - startTime
|
|
150
|
+
const combinedOutput = [stdout, stderr].filter(Boolean).join('\n').trim()
|
|
151
|
+
const truncatedOutput = combinedOutput.length > maxOutputChars
|
|
152
|
+
? `${combinedOutput.slice(0, maxOutputChars)}\n...[output truncated by Test Gate]`
|
|
153
|
+
: combinedOutput
|
|
154
|
+
|
|
155
|
+
const passed = exitCode === 0
|
|
156
|
+
const summary = passed
|
|
157
|
+
? `PASS (${durationMs}ms)`
|
|
158
|
+
: `FAIL (code ${exitCode}, ${durationMs}ms)`
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
candidateIndex,
|
|
162
|
+
passed,
|
|
163
|
+
exitCode,
|
|
164
|
+
durationMs,
|
|
165
|
+
command: resolvedCmd,
|
|
166
|
+
output: truncatedOutput,
|
|
167
|
+
summary,
|
|
168
|
+
}
|
|
169
|
+
} catch (err) {
|
|
170
|
+
const durationMs = Date.now() - startTime
|
|
171
|
+
return {
|
|
172
|
+
candidateIndex,
|
|
173
|
+
passed: false,
|
|
174
|
+
exitCode: 1,
|
|
175
|
+
durationMs,
|
|
176
|
+
command: trimmedCmd,
|
|
177
|
+
output: `Test Gate Error: ${err.message || String(err)}`,
|
|
178
|
+
summary: `ERROR (${err.message || 'unknown'})`,
|
|
179
|
+
}
|
|
180
|
+
} finally {
|
|
181
|
+
if (stageDir) {
|
|
182
|
+
try {
|
|
183
|
+
await fs.rm(stageDir, { recursive: true, force: true })
|
|
184
|
+
} catch {}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Runs Test Execution Gate across all successful candidates with generated files.
|
|
191
|
+
*/
|
|
192
|
+
export async function executeTestGateForCandidates({
|
|
193
|
+
cwd,
|
|
194
|
+
referenceOutputs = [],
|
|
195
|
+
preset = {},
|
|
196
|
+
options = {},
|
|
197
|
+
onProgress = null,
|
|
198
|
+
}) {
|
|
199
|
+
const isEnabled = Boolean(options.testGateEnabled ?? preset.test_gate_enabled)
|
|
200
|
+
const testCmd = options.testCommand || preset.test_command
|
|
201
|
+
if (!isEnabled || !testCmd || !cwd) {
|
|
202
|
+
return []
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const timeoutMs = (preset.test_gate_timeout_sec || 15) * 1000
|
|
206
|
+
const maxOutputChars = preset.test_gate_max_output_chars || 2000
|
|
207
|
+
|
|
208
|
+
if (typeof onProgress === 'function') {
|
|
209
|
+
onProgress('🧪 *Test Execution Gate: Running test suite against candidate sandboxes...*\n')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const results = []
|
|
213
|
+
for (const ref of referenceOutputs) {
|
|
214
|
+
if (!ref.ok || !ref.files || ref.files.length === 0) continue
|
|
215
|
+
|
|
216
|
+
const res = await runCandidateTestGate({
|
|
217
|
+
cwd,
|
|
218
|
+
candidateIndex: ref.index,
|
|
219
|
+
testCommand: testCmd,
|
|
220
|
+
timeoutMs,
|
|
221
|
+
maxOutputChars,
|
|
222
|
+
execFn: options.execFn,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
if (res) {
|
|
226
|
+
ref.testResult = res
|
|
227
|
+
results.push(res)
|
|
228
|
+
if (typeof onProgress === 'function') {
|
|
229
|
+
const icon = res.passed ? '✅' : '❌'
|
|
230
|
+
onProgress(`🧪 *Candidate ${ref.index}: ${icon} ${res.summary}*\n`)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return results
|
|
236
|
+
}
|
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({
|