@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.
@@ -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({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.25",
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",