@goodandready/dsh-moa 0.2.42 → 0.2.44
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/client.js +15 -10
- package/lib/file-workspace.js +30 -43
- package/lib/history.js +7 -1
- package/lib/index.js +35 -2
- package/lib/moa-budget.js +32 -19
- package/lib/moa-candidates.js +2 -0
- package/lib/moa-llm.js +11 -0
- package/lib/moa-multi-judge.js +2 -1
- package/lib/moa-prompts.js +5 -1
- package/lib/moa-runner.js +81 -77
- package/lib/moa-stream.js +6 -4
- package/lib/moa-test-gate.js +36 -0
- package/lib/routes.js +17 -17
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -101,6 +101,8 @@ window.__ModuleLoader__.load({
|
|
|
101
101
|
'proposers.add_btn': '+ Add Candidate Model',
|
|
102
102
|
'proposers.temp_label': 'Candidates Temperature',
|
|
103
103
|
'proposers.temp_hint': 'Recommended: 0.6 – 0.8 for creative, independent solution exploration.',
|
|
104
|
+
'proposers.temperature_gradient': 'Temperature Gradient Exploration (0.2 → 0.9 across candidates)',
|
|
105
|
+
'proposers.candidate_temp_tooltip': 'Candidate Temperature (leave empty for default/gradient)',
|
|
104
106
|
'stats.title': '📈 MoA Analytics & Telemetry',
|
|
105
107
|
'stats.desc': 'Session telemetry, historical runs, and model win-rate tracking.',
|
|
106
108
|
'stats.total_runs': 'Total Runs',
|
|
@@ -277,6 +279,8 @@ window.__ModuleLoader__.load({
|
|
|
277
279
|
'proposers.add_btn': '+ 添加候选模型',
|
|
278
280
|
'proposers.temp_label': '候选模型采样温度 (Temperature)',
|
|
279
281
|
'proposers.temp_hint': '推荐:0.6 – 0.8,鼓励创造性、发散性的独立探索。',
|
|
282
|
+
'proposers.temperature_gradient': '温度梯度探索(候选模型采样温度从 0.2 到 0.9 递增)',
|
|
283
|
+
'proposers.candidate_temp_tooltip': '候选模型采样温度(留空使用默认/梯度)',
|
|
280
284
|
'stats.title': '📈 MoA 数据分析与遥测',
|
|
281
285
|
'stats.desc': '会话遥测、历史运行追踪及模型胜率统计。',
|
|
282
286
|
'stats.total_runs': '总运行次数',
|
|
@@ -1962,7 +1966,7 @@ window.__ModuleLoader__.load({
|
|
|
1962
1966
|
onChange: (e) => updateCurrentPreset((p) => ({ ...p, temperature_gradient_enabled: e.target.checked })),
|
|
1963
1967
|
style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
|
|
1964
1968
|
}),
|
|
1965
|
-
'Temperature Gradient Exploration (0.2 → 0.9 across candidates)'
|
|
1969
|
+
t('proposers.temperature_gradient') || 'Temperature Gradient Exploration (0.2 → 0.9 across candidates)'
|
|
1966
1970
|
)
|
|
1967
1971
|
),
|
|
1968
1972
|
React.createElement(
|
|
@@ -1990,7 +1994,7 @@ window.__ModuleLoader__.load({
|
|
|
1990
1994
|
min: '0',
|
|
1991
1995
|
max: '2',
|
|
1992
1996
|
placeholder: 'Temp',
|
|
1993
|
-
title: 'Candidate Temperature (leave empty for default/gradient)',
|
|
1997
|
+
title: t('proposers.candidate_temp_tooltip') || 'Candidate Temperature (leave empty for default/gradient)',
|
|
1994
1998
|
className: 'moa-input',
|
|
1995
1999
|
style: { width: 50, height: 32, fontSize: 12, padding: '0 4px', textAlign: 'center' },
|
|
1996
2000
|
value: (typeof ref.temperature === 'number' && ref.temperature >= 0) ? ref.temperature : '',
|
|
@@ -2571,7 +2575,7 @@ window.__ModuleLoader__.load({
|
|
|
2571
2575
|
}
|
|
2572
2576
|
})
|
|
2573
2577
|
.catch((err) => {
|
|
2574
|
-
setSaveStatus(
|
|
2578
|
+
setSaveStatus((t('status.error_saving') || 'Error saving: ') + (err?.message || String(err)))
|
|
2575
2579
|
})
|
|
2576
2580
|
}
|
|
2577
2581
|
|
|
@@ -2612,13 +2616,14 @@ window.__ModuleLoader__.load({
|
|
|
2612
2616
|
}
|
|
2613
2617
|
render() {
|
|
2614
2618
|
if (this.state.hasError) {
|
|
2619
|
+
const t = typeof this.props?.t === 'function' ? this.props.t : (typeof makeT === 'function' ? makeT(en, null) : ((k) => k))
|
|
2615
2620
|
return React.createElement(
|
|
2616
2621
|
'div',
|
|
2617
2622
|
{
|
|
2618
2623
|
className: 'moa-alert-err',
|
|
2619
2624
|
style: { margin: '12px 0', padding: '14px', borderRadius: '8px' },
|
|
2620
2625
|
},
|
|
2621
|
-
React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ MoA UI Error:'),
|
|
2626
|
+
React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, t('error_boundary.title') || '⚠️ MoA UI Error:'),
|
|
2622
2627
|
React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
|
|
2623
2628
|
React.createElement(
|
|
2624
2629
|
'button',
|
|
@@ -2628,7 +2633,7 @@ window.__ModuleLoader__.load({
|
|
|
2628
2633
|
style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
|
|
2629
2634
|
onClick: () => this.setState({ hasError: false, error: null }),
|
|
2630
2635
|
},
|
|
2631
|
-
'Retry'
|
|
2636
|
+
t('error_boundary.retry') || 'Retry'
|
|
2632
2637
|
)
|
|
2633
2638
|
)
|
|
2634
2639
|
}
|
|
@@ -2675,12 +2680,12 @@ window.__ModuleLoader__.load({
|
|
|
2675
2680
|
'div',
|
|
2676
2681
|
{ className: 'moa-body' },
|
|
2677
2682
|
state.status === 'loading'
|
|
2678
|
-
? React.createElement('div', { style: { padding: '16px 0', color: 'var(--dsw-alias-label-secondary)', fontSize: 13 } }, 'Loading MoA settings…')
|
|
2683
|
+
? React.createElement('div', { style: { padding: '16px 0', color: 'var(--dsw-alias-label-secondary)', fontSize: 13 } }, state.t('status.loading') || 'Loading MoA settings…')
|
|
2679
2684
|
: state.status === 'unavailable'
|
|
2680
2685
|
? React.createElement(
|
|
2681
2686
|
'div',
|
|
2682
2687
|
{ style: { padding: '16px 0', display: 'flex', flexDirection: 'column', gap: 10 } },
|
|
2683
|
-
React.createElement('div', { className: 'moa-alert-err' }, 'MoA service temporarily unavailable'),
|
|
2688
|
+
React.createElement('div', { className: 'moa-alert-err' }, state.t('status.unavailable') || 'MoA service temporarily unavailable'),
|
|
2684
2689
|
React.createElement(
|
|
2685
2690
|
'button',
|
|
2686
2691
|
{
|
|
@@ -2689,7 +2694,7 @@ window.__ModuleLoader__.load({
|
|
|
2689
2694
|
style: { width: 'fit-content' },
|
|
2690
2695
|
onClick: state.reload,
|
|
2691
2696
|
},
|
|
2692
|
-
state.t('actions.retry')
|
|
2697
|
+
state.t('status.retry') || state.t('actions.retry')
|
|
2693
2698
|
)
|
|
2694
2699
|
)
|
|
2695
2700
|
: React.createElement(MoAEditor, state)
|
|
@@ -2738,7 +2743,7 @@ window.__ModuleLoader__.load({
|
|
|
2738
2743
|
locale: NS,
|
|
2739
2744
|
inject: () => ({ ctx }),
|
|
2740
2745
|
},
|
|
2741
|
-
(props) => React.createElement(MoAErrorBoundary,
|
|
2746
|
+
(props) => React.createElement(MoAErrorBoundary, props, React.createElement(MoACard, props))
|
|
2742
2747
|
)
|
|
2743
2748
|
} catch (err) {
|
|
2744
2749
|
console.warn('[dsh-moa] Failed to register ' + slotName + ':', err)
|
|
@@ -2761,7 +2766,7 @@ window.__ModuleLoader__.load({
|
|
|
2761
2766
|
locale: NS,
|
|
2762
2767
|
inject: () => ({ ctx }),
|
|
2763
2768
|
},
|
|
2764
|
-
(props) => React.createElement(MoAErrorBoundary,
|
|
2769
|
+
(props) => React.createElement(MoAErrorBoundary, props, React.createElement(MoACard, props))
|
|
2765
2770
|
)
|
|
2766
2771
|
} catch (err) {
|
|
2767
2772
|
console.warn('[dsh-moa] Failed to register plugins.item:', err)
|
package/lib/file-workspace.js
CHANGED
|
@@ -78,7 +78,7 @@ export function resolveCandidateFolder(baseDir, candidateTarget, options = {}) {
|
|
|
78
78
|
const sub = /^\d+$/.test(sanitizedTarget) ? `candidate-${sanitizedTarget}` : (sanitizedTarget || 'candidate-1')
|
|
79
79
|
const rawRunId = typeof options === 'string' ? options : options?.runId
|
|
80
80
|
const safeRunId = rawRunId ? String(rawRunId).trim().replace(/[^a-zA-Z0-9_-]/g, '') : null
|
|
81
|
-
const moaRoot =
|
|
81
|
+
const moaRoot = assertPathContained(baseDir, '.moa')
|
|
82
82
|
const runRoot = safeRunId ? assertPathContained(moaRoot, safeRunId) : moaRoot
|
|
83
83
|
return { moaRoot, runRoot, safeRunId, sub, candidateDir: assertPathContained(runRoot, sub) }
|
|
84
84
|
}
|
|
@@ -97,18 +97,16 @@ export function extractFileBlocks(text) {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const inlineRegex = /```[\w-]*[\r\n]+(?:\/\/|#|<!--)\s*(?:File|Path):\s*([^\s\n\r>]+)(?:\s*-->)?[\r\n]+([\s\S]*?)```/gi
|
|
111
|
-
while ((match = inlineRegex.exec(text)) !== null) addFile(match[1].trim(), match[2])
|
|
100
|
+
const patterns = [
|
|
101
|
+
/```[\w-]*\s+(?:file|path|filepath)=["']?([^\s"'\n\r]+)["']?[\r\n]+([\s\S]*?)```/gi,
|
|
102
|
+
/(?:\/\/|#|<!--)\s*(?:File|Path):\s*([^\s\n\r>]+)(?:\s*-->)?[\r\n]+```[\w-]*[\r\n]+([\s\S]*?)```/gi,
|
|
103
|
+
/#{1,4}\s+(?:File|Path):\s*([^\s\n\r]+)[\r\n]+```[\w-]*[\r\n]+([\s\S]*?)```/gi,
|
|
104
|
+
/```[\w-]*[\r\n]+(?:\/\/|#|<!--)\s*(?:File|Path):\s*([^\s\n\r>]+)(?:\s*-->)?[\r\n]+([\s\S]*?)```/gi,
|
|
105
|
+
]
|
|
106
|
+
for (const regex of patterns) {
|
|
107
|
+
let match
|
|
108
|
+
while ((match = regex.exec(text)) !== null) addFile(match[1].trim(), match[2])
|
|
109
|
+
}
|
|
112
110
|
|
|
113
111
|
if (files.length === 0) {
|
|
114
112
|
const htmlBlockRegex = /```(?:html)?[\r\n]+([\s\S]*?)```/gi
|
|
@@ -211,15 +209,12 @@ export async function collectProjectContext(baseDir, maxCharBudget = 16000) {
|
|
|
211
209
|
* Checks if a task prompt is an iterative refinement / modification.
|
|
212
210
|
*/
|
|
213
211
|
export function isRefinementTask(userPrompt = '', projectFiles = null) {
|
|
214
|
-
if (Array.isArray(projectFiles) && projectFiles.length === 0) return false
|
|
212
|
+
if ((Array.isArray(projectFiles) && projectFiles.length === 0) || !userPrompt?.trim()) return false
|
|
215
213
|
const p = userPrompt.toLowerCase().trim()
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const modKeywords = ['add', 'change', 'update', 'fix', 'modify', 'refactor', 'improve', 'enhance', 'adjust', 'style', 'css', 'color', 'theme', 'bug', '添加', '修改', '更新', '修复', '重构', '改进', '调整', '样式', '颜色', '主题', 'добавь', 'измени', 'поменяй', 'исправь', 'обнови', 'переделай', 'доработай', 'удали', 'темн', 'светл', 'цвет', 'кнопк', 'баг']
|
|
222
|
-
return modKeywords.some((kw) => p.includes(kw))
|
|
214
|
+
const fresh = ['from scratch', 'new project', 'new app', 'create a new', 'build a new', 'generate a new', '从头开始', '新建项目', '创建项目', '新应用', '新建应用', 'с нуля', 'новый проект', 'новое приложение', 'создай проект', 'создай приложение', 'создай игру', 'создай сервис', 'сделай проект', 'сделай приложение', 'сделай игру']
|
|
215
|
+
if (fresh.some((phrase) => p.includes(phrase))) return false
|
|
216
|
+
const mod = ['add', 'change', 'update', 'fix', 'modify', 'refactor', 'improve', 'enhance', 'adjust', 'style', 'css', 'color', 'theme', 'bug', '添加', '修改', '更新', '修复', '重构', '改进', '调整', '样式', '颜色', '主题', 'добавь', 'измени', 'поменяй', 'исправь', 'обнови', 'переделай', 'доработай', 'удали', 'темн', 'светл', 'цвет', 'кнопк', 'баг']
|
|
217
|
+
return mod.some((kw) => p.includes(kw))
|
|
223
218
|
}
|
|
224
219
|
|
|
225
220
|
/**
|
|
@@ -344,6 +339,9 @@ async function doPromoteCandidateWorkspace(baseDir, winningIndex, options = { ke
|
|
|
344
339
|
logger.warn(errMsg)
|
|
345
340
|
throw new Error(errMsg)
|
|
346
341
|
}
|
|
342
|
+
if (options.signal?.aborted) {
|
|
343
|
+
throw (options.signal.reason instanceof Error ? options.signal.reason : new Error('Turn aborted'))
|
|
344
|
+
}
|
|
347
345
|
|
|
348
346
|
const promoted = []
|
|
349
347
|
const baseReal = await fs.realpath(baseDir).catch(() => path.resolve(baseDir))
|
|
@@ -392,11 +390,9 @@ export async function cleanMoaWorkspaces(baseDir, options = {}) {
|
|
|
392
390
|
const safeRunId = rawRunId ? String(rawRunId).trim().replace(/[^a-zA-Z0-9_-]/g, '') : null
|
|
393
391
|
const moaRoot = path.join(baseDir, '.moa')
|
|
394
392
|
if (safeRunId) {
|
|
395
|
-
|
|
396
|
-
await bestEffort('cleanMoaWorkspaces runId', () => fs.rm(runRoot, { recursive: true, force: true }))
|
|
393
|
+
await bestEffort('cleanMoaWorkspaces runId', () => fs.rm(path.join(moaRoot, safeRunId), { recursive: true, force: true }))
|
|
397
394
|
await bestEffort('clean empty moaRoot', async () => {
|
|
398
|
-
|
|
399
|
-
if (remaining.length === 0) await fs.rmdir(moaRoot)
|
|
395
|
+
if ((await fs.readdir(moaRoot)).length === 0) await fs.rmdir(moaRoot)
|
|
400
396
|
})
|
|
401
397
|
} else {
|
|
402
398
|
await bestEffort('cleanMoaWorkspaces', () => fs.rm(moaRoot, { recursive: true, force: true }))
|
|
@@ -563,33 +559,24 @@ export function computeLineDiff(oldText = '', newText = '') {
|
|
|
563
559
|
const subN = subOld.length, subM = subNew.length
|
|
564
560
|
const middleItems = []
|
|
565
561
|
|
|
566
|
-
if (subN === 0) {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
for (let i = 0; i < subN; i++) middleItems.push({ type: 'removed', line: subOld[i], oldNum: start + i + 1 })
|
|
570
|
-
} else if (subN > 2000 || subM > 2000) {
|
|
562
|
+
if (subN === 0) for (let j = 0; j < subM; j++) middleItems.push({ type: 'added', line: subNew[j], newNum: start + j + 1 })
|
|
563
|
+
else if (subM === 0) for (let i = 0; i < subN; i++) middleItems.push({ type: 'removed', line: subOld[i], oldNum: start + i + 1 })
|
|
564
|
+
else if (subN > 2000 || subM > 2000) {
|
|
571
565
|
for (let i = 0; i < subN; i++) middleItems.push({ type: 'removed', line: subOld[i], oldNum: start + i + 1 })
|
|
572
566
|
for (let j = 0; j < subM; j++) middleItems.push({ type: 'added', line: subNew[j], newNum: start + j + 1 })
|
|
573
567
|
} else {
|
|
574
568
|
const dp = Array.from({ length: subN + 1 }, () => new Uint16Array(subM + 1))
|
|
575
569
|
for (let i = 0; i < subN; i++) {
|
|
576
|
-
for (let j = 0; j < subM; j++)
|
|
577
|
-
dp[i + 1][j + 1] = subOld[i] === subNew[j] ? dp[i][j] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1])
|
|
578
|
-
}
|
|
570
|
+
for (let j = 0; j < subM; j++) dp[i + 1][j + 1] = subOld[i] === subNew[j] ? dp[i][j] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1])
|
|
579
571
|
}
|
|
580
|
-
|
|
581
|
-
let i = subN, j = subM
|
|
582
|
-
const stack = []
|
|
572
|
+
let i = subN, j = subM; const stack = []
|
|
583
573
|
while (i > 0 || j > 0) {
|
|
584
574
|
if (i > 0 && j > 0 && subOld[i - 1] === subNew[j - 1]) {
|
|
585
|
-
stack.push({ type: 'same', line: subOld[i
|
|
586
|
-
i--; j--
|
|
575
|
+
stack.push({ type: 'same', line: subOld[--i], oldNum: start + i + 1, newNum: start + j }); j--
|
|
587
576
|
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
588
|
-
stack.push({ type: 'added', line: subNew[j
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
stack.push({ type: 'removed', line: subOld[i - 1], oldNum: start + i })
|
|
592
|
-
i--
|
|
577
|
+
stack.push({ type: 'added', line: subNew[--j], newNum: start + j + 1 })
|
|
578
|
+
} else if (i > 0) {
|
|
579
|
+
stack.push({ type: 'removed', line: subOld[--i], oldNum: start + i + 1 })
|
|
593
580
|
}
|
|
594
581
|
}
|
|
595
582
|
middleItems.push(...stack.reverse())
|
package/lib/history.js
CHANGED
|
@@ -358,7 +358,13 @@ export function getMoaHistory(limit = 20, offset = 0, filePath = getDefaultHisto
|
|
|
358
358
|
})
|
|
359
359
|
.filter(Boolean)
|
|
360
360
|
|
|
361
|
-
let total = _historyCache
|
|
361
|
+
let total = (_historyCache &&
|
|
362
|
+
_historyCache.filePath === filePath &&
|
|
363
|
+
_historyCache.mtimeMs === stat.mtimeMs &&
|
|
364
|
+
_historyCache.size === stat.size &&
|
|
365
|
+
typeof _historyCache.totalCount === 'number')
|
|
366
|
+
? _historyCache.totalCount
|
|
367
|
+
: undefined
|
|
362
368
|
if (typeof total !== 'number') {
|
|
363
369
|
total = tailLines.length < needed ? tailLines.length : countFileLinesSync(filePath, stat)
|
|
364
370
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
1
2
|
import path from 'node:path'
|
|
2
3
|
import { setLogger, logger } from './logger.js'
|
|
3
4
|
import { refreshCatalogInBackground } from './pricing.js'
|
|
@@ -19,6 +20,7 @@ import { resolvePresetForPrompt } from './moa-router.js'
|
|
|
19
20
|
import { createLiveCanvasClient } from './live-canvas.js'
|
|
20
21
|
import { registerPluginUpdater } from './updater.js'
|
|
21
22
|
import { bestEffort } from './best-effort.js'
|
|
23
|
+
import { getMoaHistory } from './history.js'
|
|
22
24
|
|
|
23
25
|
export const name = '@goodandready/dsh-moa'
|
|
24
26
|
export { collectProjectContext, formatProjectContext } from './file-workspace.js'
|
|
@@ -39,12 +41,24 @@ export function apply(ctx, config) {
|
|
|
39
41
|
refreshCatalogInBackground()
|
|
40
42
|
|
|
41
43
|
let localOverrides = null
|
|
44
|
+
let baseAtOverride = {}
|
|
42
45
|
const getSourceConfig = () => {
|
|
43
46
|
let liveCandidate = ctx.fiber?.config
|
|
44
47
|
if (!liveCandidate && ('config' in ctx)) {
|
|
45
48
|
bestEffort('ctx.config probe', () => { liveCandidate = ctx.config }, ctx.logger)
|
|
46
49
|
}
|
|
47
50
|
const liveSource = plainConfig(liveCandidate ?? config ?? {})
|
|
51
|
+
if (localOverrides) {
|
|
52
|
+
for (const key of Object.keys(localOverrides)) {
|
|
53
|
+
if (baseAtOverride[key] !== undefined && JSON.stringify(liveSource[key]) !== baseAtOverride[key]) {
|
|
54
|
+
delete localOverrides[key]
|
|
55
|
+
delete baseAtOverride[key]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (Object.keys(localOverrides).length === 0) {
|
|
59
|
+
localOverrides = null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
48
62
|
return localOverrides ? { ...liveSource, ...localOverrides } : liveSource
|
|
49
63
|
}
|
|
50
64
|
const live = () => {
|
|
@@ -86,6 +100,15 @@ export function apply(ctx, config) {
|
|
|
86
100
|
} else if (settingsService && typeof settingsService.update === 'function') {
|
|
87
101
|
await settingsService.update(NS, plain)
|
|
88
102
|
}
|
|
103
|
+
let liveCandidate = ctx.fiber?.config
|
|
104
|
+
if (!liveCandidate && ('config' in ctx)) {
|
|
105
|
+
bestEffort('ctx.config probe', () => { liveCandidate = ctx.config }, ctx.logger)
|
|
106
|
+
}
|
|
107
|
+
const currentLive = plainConfig(liveCandidate ?? config ?? {})
|
|
108
|
+
baseAtOverride = baseAtOverride || {}
|
|
109
|
+
for (const key of Object.keys(plain)) {
|
|
110
|
+
baseAtOverride[key] = JSON.stringify(currentLive[key])
|
|
111
|
+
}
|
|
89
112
|
localOverrides = { ...(localOverrides || {}), ...plain }
|
|
90
113
|
return live()
|
|
91
114
|
}
|
|
@@ -176,10 +199,15 @@ export function apply(ctx, config) {
|
|
|
176
199
|
const effectiveDefault = cfg.default_preset || 'default'
|
|
177
200
|
const parsed = parseMoACommand(userText, cfg.presets || [], effectiveDefault) || { prompt: userText, presetName: effectiveDefault, isExplicit: false }
|
|
178
201
|
if (parsed.isPromote) {
|
|
202
|
+
let promoteRunId = parsed.runId
|
|
203
|
+
if (!promoteRunId) {
|
|
204
|
+
const latestRun = getMoaHistory(1, 0)?.runs?.[0]
|
|
205
|
+
if (latestRun?.id) promoteRunId = latestRun.id
|
|
206
|
+
}
|
|
179
207
|
const sessCwd = event.cwd || event.session?.cwd || (event.session?.path ? path.dirname(event.session.path) : undefined) || process.cwd()
|
|
180
208
|
const turnContext = {
|
|
181
209
|
isPromoteTurn: true,
|
|
182
|
-
runId:
|
|
210
|
+
runId: promoteRunId,
|
|
183
211
|
candidateIndex: parsed.candidateIndex,
|
|
184
212
|
cwd: sessCwd,
|
|
185
213
|
webServerPort: ctx.webServer?.port,
|
|
@@ -192,11 +220,14 @@ export function apply(ctx, config) {
|
|
|
192
220
|
const isExplicitPreset = Boolean(parsed.isExplicit || userText.includes('--preset') || userText.includes('-p '))
|
|
193
221
|
if (!isExplicitPreset && (cfg.smart_routing_enabled || cfg.presets?.some((p) => p.smart_routing_enabled))) {
|
|
194
222
|
try {
|
|
223
|
+
const activePreset = (cfg.presets || []).find((p) => p.name === (parsed.presetName || effectiveDefault)) || cfg.presets?.[0]
|
|
224
|
+
const routingModel = (activePreset?.smart_routing_model?.model && activePreset.smart_routing_model)
|
|
225
|
+
|| (cfg.smart_routing_model?.model && cfg.smart_routing_model) || activePreset?.smart_routing_model || cfg.smart_routing_model
|
|
195
226
|
const routed = await resolvePresetForPrompt({
|
|
196
227
|
prompt: parsed.prompt || userText,
|
|
197
228
|
presets: cfg.presets || [],
|
|
198
229
|
defaultPreset: parsed.presetName || effectiveDefault,
|
|
199
|
-
routingModel
|
|
230
|
+
routingModel,
|
|
200
231
|
callLlm,
|
|
201
232
|
enabled: true,
|
|
202
233
|
})
|
|
@@ -216,8 +247,10 @@ export function apply(ctx, config) {
|
|
|
216
247
|
const aggProvider = targetPreset?.aggregator?.provider || ''
|
|
217
248
|
const aggModel = targetPreset?.aggregator?.model || ''
|
|
218
249
|
const sessCwd = event.cwd || event.session?.cwd || (event.session?.path ? path.dirname(event.session.path) : undefined) || process.cwd()
|
|
250
|
+
const runId = crypto.randomUUID()
|
|
219
251
|
|
|
220
252
|
const turnContext = {
|
|
253
|
+
runId,
|
|
221
254
|
targetPreset,
|
|
222
255
|
userPrompt: parsed.prompt || userText,
|
|
223
256
|
messages: messages.slice(0, -1),
|
package/lib/moa-budget.js
CHANGED
|
@@ -40,6 +40,7 @@ export function applyBudgetGuardrails({
|
|
|
40
40
|
action = 'trim', // 'trim' | 'abort'
|
|
41
41
|
prices = {},
|
|
42
42
|
promptLength = 1000,
|
|
43
|
+
projectContext = '',
|
|
43
44
|
messages = [],
|
|
44
45
|
maxTokens = 2048,
|
|
45
46
|
peerCritiqueEnabled = false,
|
|
@@ -57,6 +58,9 @@ export function applyBudgetGuardrails({
|
|
|
57
58
|
|
|
58
59
|
// Calculate full input tokens from userPrompt length + messages context
|
|
59
60
|
let contextChars = Number(promptLength) || 1000
|
|
61
|
+
if (typeof projectContext === 'string' && projectContext.length > 0 && !String(promptLength).includes(String(projectContext.length))) {
|
|
62
|
+
contextChars += projectContext.length
|
|
63
|
+
}
|
|
60
64
|
if (Array.isArray(messages) && messages.length > 0) {
|
|
61
65
|
for (const m of messages) {
|
|
62
66
|
if (typeof m?.content === 'string') {
|
|
@@ -74,25 +78,29 @@ export function applyBudgetGuardrails({
|
|
|
74
78
|
// Base candidate cost per slot (Round 1)
|
|
75
79
|
const { totalEstimatedUsd, slotEstimates } = estimateCandidateRunCost(references, prices, promptTokens, expectedOutputTokens)
|
|
76
80
|
|
|
77
|
-
// Estimate
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
// Estimate cost for downstream phases (Aggregator / Judge panel)
|
|
82
|
+
function calcDownstreamUsd(numCandidates) {
|
|
83
|
+
let downstream = 0
|
|
84
|
+
if (aggregator?.provider && aggregator?.model) {
|
|
85
|
+
const aggInput = promptTokens + expectedOutputTokens * numCandidates
|
|
86
|
+
const aggCost = estimateTokenCost(aggregator, { inputTokens: aggInput, outputTokens: expectedOutputTokens }, prices)
|
|
87
|
+
downstream += aggCost?.costUsd || 0
|
|
88
|
+
}
|
|
89
|
+
if (Array.isArray(judgeModels) && judgeModels.length > 0) {
|
|
90
|
+
for (const jm of judgeModels) {
|
|
91
|
+
if (jm?.provider && jm?.model) {
|
|
92
|
+
const jInput = promptTokens + expectedOutputTokens * numCandidates
|
|
93
|
+
const jCost = estimateTokenCost(jm, { inputTokens: jInput, outputTokens: expectedOutputTokens }, prices)
|
|
94
|
+
downstream += jCost?.costUsd || 0
|
|
95
|
+
}
|
|
90
96
|
}
|
|
91
97
|
}
|
|
98
|
+
return downstream
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
// Multiplier if peer critique (Round 2) is enabled: each candidate runs another turn with input from all peers
|
|
95
102
|
const roundMultiplier = peerCritiqueEnabled ? 2 : 1
|
|
103
|
+
const downstreamUsd = calcDownstreamUsd(references.length)
|
|
96
104
|
const fullEstimatedUsd = Number(((totalEstimatedUsd * roundMultiplier) + downstreamUsd).toFixed(6))
|
|
97
105
|
|
|
98
106
|
if (fullEstimatedUsd <= maxBudgetUsd) {
|
|
@@ -116,38 +124,43 @@ export function applyBudgetGuardrails({
|
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
// Action is 'trim': Sort by estimated cost ascending (cheapest first)
|
|
127
|
+
// Factor downstream synthesis cost into candidate selection (#158)
|
|
119
128
|
const sorted = [...slotEstimates].sort((a, b) => a.costUsd - b.costUsd)
|
|
120
129
|
const kept = []
|
|
121
|
-
let
|
|
130
|
+
let cumulativeCandidateCost = 0
|
|
122
131
|
|
|
123
132
|
for (const item of sorted) {
|
|
124
133
|
const candidateCost = item.costUsd * roundMultiplier
|
|
125
|
-
|
|
134
|
+
const nextDownstream = calcDownstreamUsd(kept.length + 1)
|
|
135
|
+
if (cumulativeCandidateCost + candidateCost + nextDownstream <= maxBudgetUsd) {
|
|
126
136
|
kept.push(item.slot)
|
|
127
|
-
|
|
137
|
+
cumulativeCandidateCost += candidateCost
|
|
128
138
|
}
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
if (kept.length === 0) {
|
|
142
|
+
const minCandidateCost = (sorted[0]?.costUsd || 0) * roundMultiplier
|
|
143
|
+
const minTotal = minCandidateCost + calcDownstreamUsd(1)
|
|
132
144
|
return {
|
|
133
145
|
allowed: false,
|
|
134
146
|
references: [],
|
|
135
147
|
action: 'trim',
|
|
136
148
|
originalCount: references.length,
|
|
137
149
|
trimmedCount: 0,
|
|
138
|
-
estimatedCostUsd: Number(
|
|
150
|
+
estimatedCostUsd: Number(minTotal.toFixed(6)),
|
|
139
151
|
maxBudgetUsd,
|
|
140
|
-
reason: `Cannot fit any candidate model within budget ($${maxBudgetUsd.toFixed(4)}). Cheapest candidate requires $${
|
|
152
|
+
reason: `Cannot fit any candidate model within budget ($${maxBudgetUsd.toFixed(4)}). Cheapest candidate requires $${minTotal.toFixed(4)}.`,
|
|
141
153
|
}
|
|
142
154
|
}
|
|
143
155
|
|
|
156
|
+
const finalEstimatedUsd = Number((cumulativeCandidateCost + calcDownstreamUsd(kept.length)).toFixed(6))
|
|
144
157
|
return {
|
|
145
158
|
allowed: true,
|
|
146
159
|
references: kept,
|
|
147
160
|
action: 'trim',
|
|
148
161
|
originalCount: references.length,
|
|
149
162
|
trimmedCount: kept.length,
|
|
150
|
-
estimatedCostUsd:
|
|
163
|
+
estimatedCostUsd: finalEstimatedUsd,
|
|
151
164
|
maxBudgetUsd,
|
|
152
165
|
reason: `Trimmed candidate pool from ${references.length} to ${kept.length} models to stay within budget ($${maxBudgetUsd.toFixed(4)}).`,
|
|
153
166
|
}
|
package/lib/moa-candidates.js
CHANGED
|
@@ -125,6 +125,7 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
125
125
|
})
|
|
126
126
|
|
|
127
127
|
const res = await Promise.race([callPromise, timeoutPromise])
|
|
128
|
+
if (candidateTimer) { clearTimeout(candidateTimer); candidateTimer = null }
|
|
128
129
|
const text = typeof res === 'string' ? res : (res?.content || res?.text || '')
|
|
129
130
|
|
|
130
131
|
const fallbackUsage = (typeof res === 'object' && res?.usage) ? res.usage : {
|
|
@@ -275,6 +276,7 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
275
276
|
}
|
|
276
277
|
}
|
|
277
278
|
} finally {
|
|
279
|
+
if (candidateTimer) clearTimeout(candidateTimer)
|
|
278
280
|
notifyFinished()
|
|
279
281
|
}
|
|
280
282
|
}
|
package/lib/moa-llm.js
CHANGED
|
@@ -139,3 +139,14 @@ export function createLlmCaller(ctx) {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
export function callConfigEquals(a, b) {
|
|
145
|
+
if (!a || !b) return false
|
|
146
|
+
if (a.provider !== b.provider) return false
|
|
147
|
+
if (a.model !== b.model) return false
|
|
148
|
+
if (a.temperature !== b.temperature) return false
|
|
149
|
+
if (a.maxTokens !== b.maxTokens) return false
|
|
150
|
+
if (a.reasoningEffort !== b.reasoningEffort) return false
|
|
151
|
+
return true
|
|
152
|
+
}
|
package/lib/moa-multi-judge.js
CHANGED
|
@@ -371,7 +371,8 @@ CONSENSUS_REASONING: [one concise sentence explaining your pick]
|
|
|
371
371
|
|
|
372
372
|
const winningCand = activeCandidates.find((c) => c.index === winner) || activeCandidates[0]
|
|
373
373
|
const winnerVotes = votes[winner] || 0
|
|
374
|
-
const
|
|
374
|
+
const winnerLabel = blind ? '' : ` (${winningCand.label})`
|
|
375
|
+
const consensusReport = `Multi-judge consensus (${strategy}): Candidate ${winner}${winnerLabel} won with ${winnerVotes}/${validResults.length} votes.`
|
|
375
376
|
|
|
376
377
|
if (typeof onProgress === 'function') {
|
|
377
378
|
onProgress(`⚖️ *${consensusReport}*\n`)
|
package/lib/moa-prompts.js
CHANGED
|
@@ -302,6 +302,10 @@ export function buildSynthesisPrompt(userPrompt, referenceOutputs = [], judgeCri
|
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
const isBlind = Boolean(options.blindEvaluation)
|
|
305
|
+
let consensusReportText = options.consensusReport
|
|
306
|
+
if (isBlind && consensusReportText) {
|
|
307
|
+
consensusReportText = consensusReportText.replace(/\s*\([^)]+\)/g, '')
|
|
308
|
+
}
|
|
305
309
|
|
|
306
310
|
const joined = referenceOutputs
|
|
307
311
|
.map((r, i) => {
|
|
@@ -344,7 +348,7 @@ ${LANGUAGE_MIRRORING_DIRECTIVE}
|
|
|
344
348
|
${referenceOutputs.some((r) => r.testResult) ? `\n${TEST_GATE_DIRECTIVE}\n` : ''}
|
|
345
349
|
${referenceOutputs.some((r) => r.syntaxWarning) ? `\n${SYNTAX_CORRECTION_DIRECTIVE}\n` : ''}
|
|
346
350
|
${options.priorTurnBaseline ? `\n### 🔄 Prior Turn Context Baseline:\n${options.priorTurnBaseline}\n` : ''}
|
|
347
|
-
${
|
|
351
|
+
${consensusReportText ? `\n### ⚖️ Multi-Judge Consensus Advisory:\n${consensusReportText}\n` : ''}
|
|
348
352
|
${options.compositeMergeDirective ? `\n${options.compositeMergeDirective}\n` : ''}
|
|
349
353
|
Instructions:
|
|
350
354
|
Your response MUST be structured into three clear parts:
|
package/lib/moa-runner.js
CHANGED
|
@@ -38,7 +38,8 @@ export async function runMoAPipeline({
|
|
|
38
38
|
webServerPort = null, timeMachineEngine = null, timeMachineUrl = null,
|
|
39
39
|
force = isTestEnvironment() ? true : false, checkpoint = true, checkpointFn = null, runId: customRunId = null,
|
|
40
40
|
}) {
|
|
41
|
-
const
|
|
41
|
+
const runId = (customRunId && String(customRunId).trim()) || crypto.randomUUID()
|
|
42
|
+
const promoteOpts = { webServerPort, timeMachineEngine, timeMachineUrl, force: Boolean(force), checkpoint, checkpointFn, signal }
|
|
42
43
|
if (signal?.aborted) return { error: new Error('Turn aborted') }
|
|
43
44
|
const startTime = Date.now()
|
|
44
45
|
// 1. Resolve configurations
|
|
@@ -58,15 +59,34 @@ export async function runMoAPipeline({
|
|
|
58
59
|
const candidateRetries = 1, refTimeoutSec = typeof preset?.reference_timeout_sec === 'number' ? preset.reference_timeout_sec : 60
|
|
59
60
|
const aggTimeoutSec = typeof preset?.aggregator_timeout_sec === 'number' ? preset.aggregator_timeout_sec : 180
|
|
60
61
|
const isBlindEvaluation = Boolean(preset?.blind_evaluation), isPeerCritiqueEnabled = Boolean(preset?.peer_critique_enabled)
|
|
61
|
-
const allowCandidateOverride = Boolean(preset?.allow_candidate_override)
|
|
62
|
-
//
|
|
62
|
+
const allowCandidateOverride = Boolean(preset?.allow_candidate_override)
|
|
63
|
+
// 1. Collect project context for refinement tasks
|
|
64
|
+
const collectedCtx = await collectProjectContext(cwd, 16000)
|
|
65
|
+
const isRefinement = Boolean(collectedCtx?.files?.length > 0 && isRefinementTask(userPrompt, collectedCtx.files))
|
|
66
|
+
let projectContext = ''
|
|
67
|
+
if (collectedCtx?.skippedFiles > 0 && typeof onProgress === 'function') {
|
|
68
|
+
const preview = collectedCtx.skippedList.slice(0, 3).join(', ')
|
|
69
|
+
const more = collectedCtx.skippedList.length > 3 ? ` (+${collectedCtx.skippedList.length - 3})` : ''
|
|
70
|
+
onProgress(`⚠️ *Workspace scan note: ${collectedCtx.skippedFiles} file(s) skipped (${preview}${more})*\n`)
|
|
71
|
+
}
|
|
72
|
+
if (isRefinement) {
|
|
73
|
+
if (typeof onProgress === 'function') {
|
|
74
|
+
onProgress('🔍 *Reading project files for refinement context...*\n')
|
|
75
|
+
}
|
|
76
|
+
projectContext = formatProjectContext(collectedCtx.files, {
|
|
77
|
+
skippedFiles: collectedCtx.skippedFiles,
|
|
78
|
+
skippedList: collectedCtx.skippedList,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
// 2. Budget Guardrail check
|
|
63
82
|
const budgetCheck = applyBudgetGuardrails({
|
|
64
83
|
references: referenceModels,
|
|
65
84
|
enabled: preset?.budget_guard_enabled,
|
|
66
85
|
maxBudgetUsd: preset?.max_budget_usd,
|
|
67
86
|
action: preset?.budget_action || 'trim',
|
|
68
87
|
prices,
|
|
69
|
-
promptLength: userPrompt?.length || 1000,
|
|
88
|
+
promptLength: (userPrompt?.length || 1000) + (projectContext ? projectContext.length : 0),
|
|
89
|
+
projectContext,
|
|
70
90
|
messages,
|
|
71
91
|
maxTokens,
|
|
72
92
|
peerCritiqueEnabled: isPeerCritiqueEnabled,
|
|
@@ -74,20 +94,12 @@ export async function runMoAPipeline({
|
|
|
74
94
|
judgeModels: preset?.multi_judge_enabled ? preset?.judge_models : [],
|
|
75
95
|
})
|
|
76
96
|
if (!budgetCheck.allowed) {
|
|
77
|
-
return {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
aggregator: slotLabel(primaryJudge),
|
|
81
|
-
references: [],
|
|
82
|
-
presetName: preset?.name || 'default',
|
|
83
|
-
isRefinement: false,
|
|
84
|
-
isFastMode,
|
|
85
|
-
winningIndex: 0,
|
|
86
|
-
winnerModel: 'none',
|
|
87
|
-
promotedFiles: [],
|
|
97
|
+
return { kind: 'failure', content: `🛑 Budget Guardrail Abort: ${budgetCheck.reason}`,
|
|
98
|
+
aggregator: slotLabel(primaryJudge), references: [], presetName: preset?.name || 'default',
|
|
99
|
+
isRefinement, isFastMode, winningIndex: 0, winnerModel: 'none', promotedFiles: [],
|
|
88
100
|
usage: { totalTokens: 0, totalCostUsd: 0, candidates: [], aggregator: { totalTokens: 0, costUsd: 0 } },
|
|
89
|
-
skippedFiles: 0,
|
|
90
|
-
skippedList: [],
|
|
101
|
+
skippedFiles: collectedCtx?.skippedFiles || 0,
|
|
102
|
+
skippedList: collectedCtx?.skippedList || [],
|
|
91
103
|
durationMs: Date.now() - startTime,
|
|
92
104
|
}
|
|
93
105
|
}
|
|
@@ -95,24 +107,6 @@ export async function runMoAPipeline({
|
|
|
95
107
|
if (budgetCheck.action === 'trim' && typeof onProgress === 'function') {
|
|
96
108
|
onProgress(`✂️ *${budgetCheck.reason}*\n`)
|
|
97
109
|
}
|
|
98
|
-
// 2. Collect project context for refinement tasks
|
|
99
|
-
const collectedCtx = await collectProjectContext(cwd, 16000)
|
|
100
|
-
const isRefinement = Boolean(collectedCtx?.files?.length > 0 && isRefinementTask(userPrompt, collectedCtx.files))
|
|
101
|
-
let projectContext = ''
|
|
102
|
-
if (collectedCtx?.skippedFiles > 0 && typeof onProgress === 'function') {
|
|
103
|
-
const preview = collectedCtx.skippedList.slice(0, 3).join(', ')
|
|
104
|
-
const more = collectedCtx.skippedList.length > 3 ? ` (+${collectedCtx.skippedList.length - 3})` : ''
|
|
105
|
-
onProgress(`⚠️ *Workspace scan note: ${collectedCtx.skippedFiles} file(s) skipped (${preview}${more})*\n`)
|
|
106
|
-
}
|
|
107
|
-
if (isRefinement) {
|
|
108
|
-
if (typeof onProgress === 'function') {
|
|
109
|
-
onProgress('🔍 *Reading project files for refinement context...*\n')
|
|
110
|
-
}
|
|
111
|
-
projectContext = formatProjectContext(collectedCtx.files, {
|
|
112
|
-
skippedFiles: collectedCtx.skippedFiles,
|
|
113
|
-
skippedList: collectedCtx.skippedList,
|
|
114
|
-
})
|
|
115
|
-
}
|
|
116
110
|
// 3. Build prompts & Feature 8 multi-turn context
|
|
117
111
|
const priorTurnBaseline = preset?.multi_turn_enabled !== false ? extractPriorTurnBaseline(messages) : null
|
|
118
112
|
const prunedHistory = preset?.multi_turn_enabled !== false ? pruneMultiTurnMessages(messages) : messages
|
|
@@ -275,12 +269,30 @@ export async function runMoAPipeline({
|
|
|
275
269
|
return { error: new Error('Turn aborted') }
|
|
276
270
|
}
|
|
277
271
|
const single = successfulRefs[0]
|
|
278
|
-
let promotedFiles = []
|
|
272
|
+
let promotedFiles = [], promoteErr = null
|
|
279
273
|
if (single.files?.length > 0) {
|
|
280
|
-
|
|
281
|
-
|
|
274
|
+
try {
|
|
275
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, single.index, { ...promoteOpts, runId, keepMoa: allowCandidateOverride })
|
|
276
|
+
} catch (e) { promoteErr = e }
|
|
277
|
+
} else if (!allowCandidateOverride) {
|
|
282
278
|
await cleanMoaWorkspaces(cwd, { runId })
|
|
283
279
|
}
|
|
280
|
+
if (promoteErr) {
|
|
281
|
+
if (!allowCandidateOverride) await cleanMoaWorkspaces(cwd, { runId })
|
|
282
|
+
if (signal?.aborted || promoteErr?.message?.includes('aborted') || promoteErr?.name === 'AbortError' || promoteErr?.message === 'Turn aborted') {
|
|
283
|
+
return { error: new Error('Turn aborted') }
|
|
284
|
+
}
|
|
285
|
+
return { kind: 'failure', error: promoteErr,
|
|
286
|
+
content: '🛑 Checkpoint or Promotion Failure: ' + (promoteErr?.message || String(promoteErr)),
|
|
287
|
+
aggregator: 'Fast Mode (Direct)', references: referenceOutputs,
|
|
288
|
+
presetName: preset?.name || 'default', isRefinement, isFastMode: true,
|
|
289
|
+
winningIndex: single.index, winnerModel: single.label, promotedFiles: [], runId,
|
|
290
|
+
usage: { totalTokens: 0, totalCostUsd: 0, candidates: [], aggregator: { totalTokens: 0, costUsd: 0 } },
|
|
291
|
+
skippedFiles: collectedCtx?.skippedFiles || 0,
|
|
292
|
+
skippedList: collectedCtx?.skippedList || [],
|
|
293
|
+
durationMs: Date.now() - startTime,
|
|
294
|
+
}
|
|
295
|
+
}
|
|
284
296
|
|
|
285
297
|
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs, { winningIndex: single.index })
|
|
286
298
|
const totalTokens = single.usage?.totalTokens || 0
|
|
@@ -289,44 +301,27 @@ export async function runMoAPipeline({
|
|
|
289
301
|
|
|
290
302
|
try {
|
|
291
303
|
await recordMoaRunAsync({
|
|
292
|
-
cwd,
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
isFastMode: true,
|
|
297
|
-
candidates: candidatesForHistory(referenceOutputs),
|
|
298
|
-
aggregator: null,
|
|
299
|
-
winnerIndex: single.index,
|
|
300
|
-
winnerModel: single.label,
|
|
301
|
-
promotedFiles,
|
|
302
|
-
totalTokens,
|
|
303
|
-
totalCostUsd,
|
|
304
|
-
durationMs,
|
|
304
|
+
id: runId, cwd, prompt: userPrompt, preset: preset?.name || 'default',
|
|
305
|
+
isRefinement, isFastMode: true, candidates: candidatesForHistory(referenceOutputs),
|
|
306
|
+
aggregator: null, winnerIndex: single.index, winnerModel: single.label,
|
|
307
|
+
promotedFiles, totalTokens, totalCostUsd, durationMs,
|
|
305
308
|
}, historyFilePath || undefined)
|
|
306
309
|
} catch (histErr) {
|
|
307
310
|
logger.warn('[dsh-moa] Failed to record fast synthesis run in history:', histErr?.message || histErr)
|
|
308
311
|
}
|
|
309
312
|
|
|
310
313
|
return {
|
|
311
|
-
kind: 'synthesis',
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
presetName: preset?.name || 'default',
|
|
316
|
-
isRefinement,
|
|
317
|
-
isFastMode: true,
|
|
318
|
-
winningIndex: single.index,
|
|
319
|
-
winnerModel: single.label,
|
|
320
|
-
promotedFiles,
|
|
314
|
+
kind: 'synthesis', content: single.text, aggregator: 'Fast Mode (Direct)',
|
|
315
|
+
references: referenceOutputs, presetName: preset?.name || 'default',
|
|
316
|
+
isRefinement, isFastMode: true, winningIndex: single.index,
|
|
317
|
+
winnerModel: single.label, promotedFiles, runId,
|
|
321
318
|
...(livePreview ? { liveCanvas: livePreview } : {}),
|
|
322
319
|
usage: {
|
|
323
|
-
totalTokens,
|
|
324
|
-
totalCostUsd,
|
|
320
|
+
totalTokens, totalCostUsd,
|
|
325
321
|
candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
|
|
326
322
|
aggregator: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
327
323
|
},
|
|
328
|
-
skippedFiles: collectedCtx?.skippedFiles || 0,
|
|
329
|
-
skippedList: collectedCtx?.skippedList || [],
|
|
324
|
+
skippedFiles: collectedCtx?.skippedFiles || 0, skippedList: collectedCtx?.skippedList || [],
|
|
330
325
|
durationMs,
|
|
331
326
|
}
|
|
332
327
|
}
|
|
@@ -502,16 +497,26 @@ export async function runMoAPipeline({
|
|
|
502
497
|
let promotedFiles = []
|
|
503
498
|
|
|
504
499
|
const keepWorkspaces = allowCandidateOverride
|
|
505
|
-
if (
|
|
506
|
-
await
|
|
507
|
-
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
500
|
+
if (signal?.aborted) {
|
|
501
|
+
if (!keepWorkspaces) await cleanMoaWorkspaces(cwd, { runId })
|
|
502
|
+
return { error: new Error('Turn aborted') }
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
if (synthesizedFiles.length > 0) {
|
|
506
|
+
await writeCandidateWorkspace(cwd, 'curator-synthesis', synthesizedFiles, { runId })
|
|
507
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, 'curator-synthesis', { ...promoteOpts, runId, keepMoa: keepWorkspaces })
|
|
508
|
+
} else if (referenceOutputs[winningIndex - 1]?.files?.length > 0) {
|
|
509
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, winningIndex, { ...promoteOpts, runId, keepMoa: keepWorkspaces })
|
|
510
|
+
} else if (successfulRefs[0]?.files?.length > 0) {
|
|
511
|
+
const fallbackIdx = successfulRefs[0].index
|
|
512
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, fallbackIdx, { ...promoteOpts, runId, keepMoa: keepWorkspaces })
|
|
513
|
+
} else if (!keepWorkspaces) {
|
|
514
|
+
await cleanMoaWorkspaces(cwd, { runId })
|
|
515
|
+
}
|
|
516
|
+
} catch (promoteErr) {
|
|
517
|
+
if (!keepWorkspaces) await cleanMoaWorkspaces(cwd, { runId })
|
|
518
|
+
if (signal?.aborted || promoteErr?.message === 'Turn aborted') return { error: new Error('Turn aborted') }
|
|
519
|
+
throw promoteErr
|
|
515
520
|
}
|
|
516
521
|
|
|
517
522
|
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs, { winningIndex, synthesizedFiles })
|
|
@@ -582,8 +587,7 @@ export async function runMoAPipeline({
|
|
|
582
587
|
consensus: multiJudgeResult,
|
|
583
588
|
...(livePreview ? { liveCanvas: livePreview } : {}),
|
|
584
589
|
usage: usageSummary,
|
|
585
|
-
skippedFiles: collectedCtx?.skippedFiles || 0,
|
|
586
|
-
skippedList: collectedCtx?.skippedList || [],
|
|
590
|
+
skippedFiles: collectedCtx?.skippedFiles || 0, skippedList: collectedCtx?.skippedList || [],
|
|
587
591
|
durationMs,
|
|
588
592
|
}
|
|
589
593
|
}
|
package/lib/moa-stream.js
CHANGED
|
@@ -4,7 +4,7 @@ import crypto from 'node:crypto'
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { cleanMoaWorkspaces, promoteCandidateWorkspace } from './file-workspace.js'
|
|
7
|
-
import { isTestEnvironment } from './history.js'
|
|
7
|
+
import { isTestEnvironment, getMoaHistory } from './history.js'
|
|
8
8
|
import { formatMoAResponse } from './moa-parser.js'
|
|
9
9
|
import { runMoAPipeline } from './moa-runner.js'
|
|
10
10
|
|
|
@@ -57,6 +57,7 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
|
|
|
57
57
|
timeMachineEngine,
|
|
58
58
|
force,
|
|
59
59
|
signal: combinedSignal,
|
|
60
|
+
runId,
|
|
60
61
|
})
|
|
61
62
|
.catch((err) => ({ error: err }))
|
|
62
63
|
.finally(() => {
|
|
@@ -152,10 +153,11 @@ export async function* streamPromoteTurn({ runId, candidateIndex, cwd, webServer
|
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
try {
|
|
155
|
-
|
|
156
|
+
const effectiveRunId = runId || getMoaHistory(1, 0).runs[0]?.id || null
|
|
157
|
+
yield { type: 'text-delta', index: 0, text: `🔄 *Promoting candidate ${candidateIndex}${effectiveRunId ? ` for run \`${effectiveRunId}\`` : ''}...*\n\n` }
|
|
156
158
|
|
|
157
159
|
const promotedFiles = await promoteCandidateWorkspace(cwd, candidateIndex, {
|
|
158
|
-
runId,
|
|
160
|
+
runId: effectiveRunId,
|
|
159
161
|
keepMoa: true,
|
|
160
162
|
port: webServerPort,
|
|
161
163
|
timeMachineEngine,
|
|
@@ -163,7 +165,7 @@ export async function* streamPromoteTurn({ runId, candidateIndex, cwd, webServer
|
|
|
163
165
|
})
|
|
164
166
|
|
|
165
167
|
if (!promotedFiles || promotedFiles.length === 0) {
|
|
166
|
-
const noFilesText = `⚠️ **MoA Promote Notice**: No candidate files found for candidate ${candidateIndex}${
|
|
168
|
+
const noFilesText = `⚠️ **MoA Promote Notice**: No candidate files found for candidate ${candidateIndex}${effectiveRunId ? ` in run \`${effectiveRunId}\`` : ''}. Workspace may have expired or run does not exist.`
|
|
167
169
|
yield { type: 'text-delta', index: 0, text: noFilesText }
|
|
168
170
|
yield { type: 'block-end', index: 0, block: { type: 'text', text: noFilesText } }
|
|
169
171
|
yield { type: 'finish', reason: { kind: 'stop' } }
|
package/lib/moa-test-gate.js
CHANGED
|
@@ -115,14 +115,50 @@ export async function runCandidateTestGate({
|
|
|
115
115
|
stderr = customRes?.stderr || ''
|
|
116
116
|
} else {
|
|
117
117
|
const nodePath = path.join(cwd, 'node_modules')
|
|
118
|
+
const allowedReads = [
|
|
119
|
+
executionDir,
|
|
120
|
+
cwd,
|
|
121
|
+
...(candidateDir && candidateDir !== executionDir ? [candidateDir] : []),
|
|
122
|
+
...(process.platform === 'win32'
|
|
123
|
+
? [process.env.SYSTEMROOT || 'C:\\Windows', path.dirname(process.execPath)]
|
|
124
|
+
: ['/usr', '/lib', '/lib64', '/etc', path.resolve(path.dirname(process.execPath), '..')]),
|
|
125
|
+
].filter(Boolean)
|
|
126
|
+
const permOpts = `--permission ${allowedReads.map((p) => `--allow-fs-read=${p}`).join(' ')} --allow-fs-write=${executionDir}`
|
|
127
|
+
|
|
118
128
|
const env = getScrubbedEnv({
|
|
119
129
|
MOA_CANDIDATE_INDEX: String(candidateIndex),
|
|
120
130
|
MOA_CANDIDATE_DIR: candidateDir,
|
|
121
131
|
MOA_BASE_DIR: cwd,
|
|
122
132
|
...(runId ? { MOA_RUN_ID: String(runId) } : {}),
|
|
123
133
|
NODE_PATH: process.env.NODE_PATH ? `${nodePath}:${process.env.NODE_PATH}` : nodePath,
|
|
134
|
+
NODE_OPTIONS: process.env.NODE_OPTIONS ? `${process.env.NODE_OPTIONS} ${permOpts}` : permOpts,
|
|
124
135
|
})
|
|
125
136
|
|
|
137
|
+
const allowedRoots = [executionDir, cwd, ...(candidateDir && candidateDir !== executionDir ? [candidateDir] : [])]
|
|
138
|
+
const cmdTokens = resolvedCmd.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []
|
|
139
|
+
for (const rawToken of cmdTokens) {
|
|
140
|
+
const token = rawToken.replace(/^["']|["']$/g, '')
|
|
141
|
+
if (path.isAbsolute(token)) {
|
|
142
|
+
if (/^\/(usr\/(bin|lib|include)|bin|lib|lib64|etc)/.test(token) && !/\.(txt|json|env|npmrc|key|ssh|md)/i.test(token)) {
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
const real = path.resolve(token)
|
|
146
|
+
const allowed = allowedRoots.some((d) => d && (real === d || real.startsWith(d + path.sep)))
|
|
147
|
+
if (!allowed) {
|
|
148
|
+
const durationMs = Date.now() - startTime
|
|
149
|
+
return {
|
|
150
|
+
candidateIndex,
|
|
151
|
+
passed: false,
|
|
152
|
+
exitCode: 1,
|
|
153
|
+
durationMs,
|
|
154
|
+
command: resolvedCmd,
|
|
155
|
+
output: 'Access denied: test command specifies path outside workspace sandbox',
|
|
156
|
+
summary: `FAIL (code 1, ${durationMs}ms)`,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
126
162
|
const runPromise = new Promise((resolve) => {
|
|
127
163
|
let child
|
|
128
164
|
let timeoutHandle = null
|
package/lib/routes.js
CHANGED
|
@@ -2,12 +2,7 @@
|
|
|
2
2
|
* DeepSeek Harness Mixture of Agents (MoA) — HTTP Route Handlers
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
getMoaHistory,
|
|
7
|
-
getMoaLeaderboard,
|
|
8
|
-
getMoaRunById,
|
|
9
|
-
exportMoaHistory,
|
|
10
|
-
} from './history.js'
|
|
5
|
+
import { getMoaHistory, getMoaLeaderboard, getMoaRunById, exportMoaHistory } from './history.js'
|
|
11
6
|
import { promoteCandidateWorkspace, readCandidateFiles, computeLineDiff, isSafeRelativePath } from './file-workspace.js'
|
|
12
7
|
import { isSafeWriteRequest } from './updater.js'
|
|
13
8
|
import { runMoAPipeline } from './moa-runner.js'
|
|
@@ -18,10 +13,7 @@ let _modelsCache = { timestamp: 0, models: [] }
|
|
|
18
13
|
const MODELS_CACHE_TTL = 60 * 1000
|
|
19
14
|
|
|
20
15
|
function writeJson(res, status, body) {
|
|
21
|
-
res.writeHead(status, {
|
|
22
|
-
'Content-Type': 'application/json; charset=utf-8',
|
|
23
|
-
'Cache-Control': 'no-store',
|
|
24
|
-
})
|
|
16
|
+
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' })
|
|
25
17
|
res.end(JSON.stringify(body))
|
|
26
18
|
}
|
|
27
19
|
|
|
@@ -30,10 +22,7 @@ function readBody(req) {
|
|
|
30
22
|
let acc = ''
|
|
31
23
|
req.on('data', (chunk) => {
|
|
32
24
|
acc += chunk
|
|
33
|
-
if (acc.length > 5 * 1024 * 1024) {
|
|
34
|
-
req.destroy()
|
|
35
|
-
reject(new Error('Payload Too Large'))
|
|
36
|
-
}
|
|
25
|
+
if (acc.length > 5 * 1024 * 1024) { req.destroy(); reject(new Error('Payload Too Large')) }
|
|
37
26
|
})
|
|
38
27
|
req.on('end', () => resolve(acc))
|
|
39
28
|
req.on('error', reject)
|
|
@@ -356,9 +345,11 @@ ctx.effect(() => {
|
|
|
356
345
|
}
|
|
357
346
|
try {
|
|
358
347
|
const u = new URL(req.url, 'http://localhost')
|
|
359
|
-
const
|
|
348
|
+
const rawRunId = u.searchParams.get('runId')
|
|
349
|
+
const latestRun = (!rawRunId || rawRunId === 'latest' || rawRunId === 'active') ? getMoaHistory(1, 0).runs[0] : null
|
|
350
|
+
const runId = (rawRunId && rawRunId !== 'latest' && rawRunId !== 'active') ? rawRunId : latestRun?.id
|
|
360
351
|
const run = runId ? getMoaRunById(runId) : null
|
|
361
|
-
const cwd = run?.cwd || u.searchParams.get('cwd') || process.cwd()
|
|
352
|
+
const cwd = run?.cwd || latestRun?.cwd || u.searchParams.get('cwd') || process.cwd()
|
|
362
353
|
const sanitizeTarget = (val, def) => {
|
|
363
354
|
const clean = String(val || '').trim().replace(/[^a-zA-Z0-9_-]/g, '')
|
|
364
355
|
return clean || def
|
|
@@ -511,11 +502,14 @@ ctx.effect(() => {
|
|
|
511
502
|
const raw = await readBody(req)
|
|
512
503
|
const body = JSON.parse(raw || '{}')
|
|
513
504
|
const prompt = body?.prompt || ''
|
|
505
|
+
const activePreset = (cfg.presets || []).find((p) => p.name === (cfg.default_preset || 'default')) || cfg.presets?.[0]
|
|
506
|
+
const routingModel = (activePreset?.smart_routing_model?.model && activePreset.smart_routing_model)
|
|
507
|
+
|| (cfg.smart_routing_model?.model && cfg.smart_routing_model) || activePreset?.smart_routing_model || cfg.smart_routing_model
|
|
514
508
|
const result = await resolvePresetForPrompt({
|
|
515
509
|
prompt,
|
|
516
510
|
presets: cfg.presets || [],
|
|
517
511
|
defaultPreset: cfg.default_preset || 'default',
|
|
518
|
-
routingModel
|
|
512
|
+
routingModel,
|
|
519
513
|
callLlm,
|
|
520
514
|
enabled: cfg.enabled !== false,
|
|
521
515
|
})
|
|
@@ -575,12 +569,18 @@ ctx.effect(() => {
|
|
|
575
569
|
timeMachineEngine: timeMachineEngine || ctx.reflect?.get?.('timeMachineEngine', false),
|
|
576
570
|
timeMachineUrl: payload.timeMachineUrl, force: payload.force,
|
|
577
571
|
checkpoint: payload.checkpoint, checkpointFn: payload.checkpointFn,
|
|
572
|
+
runId: payload.runId,
|
|
578
573
|
})
|
|
579
574
|
|
|
580
575
|
if (result?.skippedFiles > 0) {
|
|
581
576
|
ctx.logger?.warn?.(`[dsh-moa] Workspace scan skipped ${result.skippedFiles} file(s): ${result.skippedList.join(', ')}`)
|
|
582
577
|
}
|
|
583
578
|
|
|
579
|
+
if (result?.error) {
|
|
580
|
+
writeJson(res, 500, { ok: false, error: result.error?.message || String(result.error) })
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
|
|
584
584
|
writeJson(res, 200, { ok: true, ...result })
|
|
585
585
|
} catch (err) {
|
|
586
586
|
writeJson(res, 500, { ok: false, error: err?.message || String(err) })
|