@goodandready/dsh-moa 0.2.41 → 0.2.43
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 +7 -0
- package/README.ru.md +7 -0
- package/README.zh.md +11 -0
- package/lib/client.js +421 -107
- package/lib/file-workspace.js +40 -43
- package/lib/history.js +8 -1
- package/lib/index.js +55 -4
- package/lib/live-canvas.js +39 -6
- package/lib/moa-budget.js +28 -19
- package/lib/moa-candidates.js +3 -0
- package/lib/moa-multi-judge.js +2 -1
- package/lib/moa-prompts.js +5 -1
- package/lib/moa-runner.js +48 -46
- package/lib/moa-stream.js +59 -1
- package/lib/moa-test-gate.js +11 -0
- package/lib/routes.js +10 -3
- package/lib/updater.js +23 -0
- package/package.json +1 -1
package/lib/file-workspace.js
CHANGED
|
@@ -78,7 +78,17 @@ 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
|
|
81
|
+
const moaPath = path.resolve(baseDir, '.moa')
|
|
82
|
+
try {
|
|
83
|
+
const lst = fsSync.lstatSync(moaPath)
|
|
84
|
+
if (lst.isSymbolicLink()) {
|
|
85
|
+
const realMoa = fsSync.realpathSync(moaPath), realBase = fsSync.realpathSync(path.resolve(baseDir))
|
|
86
|
+
if (realMoa !== realBase && !realMoa.startsWith(realBase + path.sep)) fsSync.unlinkSync(moaPath)
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (err?.code !== 'ENOENT') throw err
|
|
90
|
+
}
|
|
91
|
+
const moaRoot = assertPathContained(baseDir, '.moa')
|
|
82
92
|
const runRoot = safeRunId ? assertPathContained(moaRoot, safeRunId) : moaRoot
|
|
83
93
|
return { moaRoot, runRoot, safeRunId, sub, candidateDir: assertPathContained(runRoot, sub) }
|
|
84
94
|
}
|
|
@@ -97,18 +107,16 @@ export function extractFileBlocks(text) {
|
|
|
97
107
|
}
|
|
98
108
|
}
|
|
99
109
|
|
|
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])
|
|
110
|
+
const patterns = [
|
|
111
|
+
/```[\w-]*\s+(?:file|path|filepath)=["']?([^\s"'\n\r]+)["']?[\r\n]+([\s\S]*?)```/gi,
|
|
112
|
+
/(?:\/\/|#|<!--)\s*(?:File|Path):\s*([^\s\n\r>]+)(?:\s*-->)?[\r\n]+```[\w-]*[\r\n]+([\s\S]*?)```/gi,
|
|
113
|
+
/#{1,4}\s+(?:File|Path):\s*([^\s\n\r]+)[\r\n]+```[\w-]*[\r\n]+([\s\S]*?)```/gi,
|
|
114
|
+
/```[\w-]*[\r\n]+(?:\/\/|#|<!--)\s*(?:File|Path):\s*([^\s\n\r>]+)(?:\s*-->)?[\r\n]+([\s\S]*?)```/gi,
|
|
115
|
+
]
|
|
116
|
+
for (const regex of patterns) {
|
|
117
|
+
let match
|
|
118
|
+
while ((match = regex.exec(text)) !== null) addFile(match[1].trim(), match[2])
|
|
119
|
+
}
|
|
112
120
|
|
|
113
121
|
if (files.length === 0) {
|
|
114
122
|
const htmlBlockRegex = /```(?:html)?[\r\n]+([\s\S]*?)```/gi
|
|
@@ -211,15 +219,12 @@ export async function collectProjectContext(baseDir, maxCharBudget = 16000) {
|
|
|
211
219
|
* Checks if a task prompt is an iterative refinement / modification.
|
|
212
220
|
*/
|
|
213
221
|
export function isRefinementTask(userPrompt = '', projectFiles = null) {
|
|
214
|
-
if (Array.isArray(projectFiles) && projectFiles.length === 0) return false
|
|
222
|
+
if ((Array.isArray(projectFiles) && projectFiles.length === 0) || !userPrompt?.trim()) return false
|
|
215
223
|
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))
|
|
224
|
+
const fresh = ['from scratch', 'new project', 'new app', 'create a new', 'build a new', 'generate a new', '从头开始', '新建项目', '创建项目', '新应用', '新建应用', 'с нуля', 'новый проект', 'новое приложение', 'создай проект', 'создай приложение', 'создай игру', 'создай сервис', 'сделай проект', 'сделай приложение', 'сделай игру']
|
|
225
|
+
if (fresh.some((phrase) => p.includes(phrase))) return false
|
|
226
|
+
const mod = ['add', 'change', 'update', 'fix', 'modify', 'refactor', 'improve', 'enhance', 'adjust', 'style', 'css', 'color', 'theme', 'bug', '添加', '修改', '更新', '修复', '重构', '改进', '调整', '样式', '颜色', '主题', 'добавь', 'измени', 'поменяй', 'исправь', 'обнови', 'переделай', 'доработай', 'удали', 'темн', 'светл', 'цвет', 'кнопк', 'баг']
|
|
227
|
+
return mod.some((kw) => p.includes(kw))
|
|
223
228
|
}
|
|
224
229
|
|
|
225
230
|
/**
|
|
@@ -344,6 +349,9 @@ async function doPromoteCandidateWorkspace(baseDir, winningIndex, options = { ke
|
|
|
344
349
|
logger.warn(errMsg)
|
|
345
350
|
throw new Error(errMsg)
|
|
346
351
|
}
|
|
352
|
+
if (options.signal?.aborted) {
|
|
353
|
+
throw (options.signal.reason instanceof Error ? options.signal.reason : new Error('Turn aborted'))
|
|
354
|
+
}
|
|
347
355
|
|
|
348
356
|
const promoted = []
|
|
349
357
|
const baseReal = await fs.realpath(baseDir).catch(() => path.resolve(baseDir))
|
|
@@ -392,11 +400,9 @@ export async function cleanMoaWorkspaces(baseDir, options = {}) {
|
|
|
392
400
|
const safeRunId = rawRunId ? String(rawRunId).trim().replace(/[^a-zA-Z0-9_-]/g, '') : null
|
|
393
401
|
const moaRoot = path.join(baseDir, '.moa')
|
|
394
402
|
if (safeRunId) {
|
|
395
|
-
|
|
396
|
-
await bestEffort('cleanMoaWorkspaces runId', () => fs.rm(runRoot, { recursive: true, force: true }))
|
|
403
|
+
await bestEffort('cleanMoaWorkspaces runId', () => fs.rm(path.join(moaRoot, safeRunId), { recursive: true, force: true }))
|
|
397
404
|
await bestEffort('clean empty moaRoot', async () => {
|
|
398
|
-
|
|
399
|
-
if (remaining.length === 0) await fs.rmdir(moaRoot)
|
|
405
|
+
if ((await fs.readdir(moaRoot)).length === 0) await fs.rmdir(moaRoot)
|
|
400
406
|
})
|
|
401
407
|
} else {
|
|
402
408
|
await bestEffort('cleanMoaWorkspaces', () => fs.rm(moaRoot, { recursive: true, force: true }))
|
|
@@ -563,33 +569,24 @@ export function computeLineDiff(oldText = '', newText = '') {
|
|
|
563
569
|
const subN = subOld.length, subM = subNew.length
|
|
564
570
|
const middleItems = []
|
|
565
571
|
|
|
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) {
|
|
572
|
+
if (subN === 0) for (let j = 0; j < subM; j++) middleItems.push({ type: 'added', line: subNew[j], newNum: start + j + 1 })
|
|
573
|
+
else if (subM === 0) for (let i = 0; i < subN; i++) middleItems.push({ type: 'removed', line: subOld[i], oldNum: start + i + 1 })
|
|
574
|
+
else if (subN > 2000 || subM > 2000) {
|
|
571
575
|
for (let i = 0; i < subN; i++) middleItems.push({ type: 'removed', line: subOld[i], oldNum: start + i + 1 })
|
|
572
576
|
for (let j = 0; j < subM; j++) middleItems.push({ type: 'added', line: subNew[j], newNum: start + j + 1 })
|
|
573
577
|
} else {
|
|
574
578
|
const dp = Array.from({ length: subN + 1 }, () => new Uint16Array(subM + 1))
|
|
575
579
|
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
|
-
}
|
|
580
|
+
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
581
|
}
|
|
580
|
-
|
|
581
|
-
let i = subN, j = subM
|
|
582
|
-
const stack = []
|
|
582
|
+
let i = subN, j = subM; const stack = []
|
|
583
583
|
while (i > 0 || j > 0) {
|
|
584
584
|
if (i > 0 && j > 0 && subOld[i - 1] === subNew[j - 1]) {
|
|
585
|
-
stack.push({ type: 'same', line: subOld[i
|
|
586
|
-
i--; j--
|
|
585
|
+
stack.push({ type: 'same', line: subOld[--i], oldNum: start + i + 1, newNum: start + j }); j--
|
|
587
586
|
} 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--
|
|
587
|
+
stack.push({ type: 'added', line: subNew[--j], newNum: start + j + 1 })
|
|
588
|
+
} else if (i > 0) {
|
|
589
|
+
stack.push({ type: 'removed', line: subOld[--i], oldNum: start + i + 1 })
|
|
593
590
|
}
|
|
594
591
|
}
|
|
595
592
|
middleItems.push(...stack.reverse())
|
package/lib/history.js
CHANGED
|
@@ -86,6 +86,7 @@ function normalizeRunRecord(record) {
|
|
|
86
86
|
preset: record.preset || 'default',
|
|
87
87
|
isRefinement: Boolean(record.isRefinement),
|
|
88
88
|
isFastMode: Boolean(record.isFastMode),
|
|
89
|
+
cwd: record.cwd ? path.resolve(record.cwd) : undefined,
|
|
89
90
|
candidates: Array.isArray(record.candidates)
|
|
90
91
|
? record.candidates.map((c, idx) => ({
|
|
91
92
|
index: typeof c.index === 'number' ? c.index : (idx + 1),
|
|
@@ -357,7 +358,13 @@ export function getMoaHistory(limit = 20, offset = 0, filePath = getDefaultHisto
|
|
|
357
358
|
})
|
|
358
359
|
.filter(Boolean)
|
|
359
360
|
|
|
360
|
-
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
|
|
361
368
|
if (typeof total !== 'number') {
|
|
362
369
|
total = tailLines.length < needed ? tailLines.length : countFileLinesSync(filePath, stat)
|
|
363
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'
|
|
@@ -12,12 +13,14 @@ import { createLlmCaller } from './moa-llm.js'
|
|
|
12
13
|
import {
|
|
13
14
|
parseMoACommand,
|
|
14
15
|
streamMoATurn,
|
|
16
|
+
streamPromoteTurn,
|
|
15
17
|
} from './moa-runner.js'
|
|
16
18
|
import { registerMoaRoutes } from './routes.js'
|
|
17
19
|
import { resolvePresetForPrompt } from './moa-router.js'
|
|
18
20
|
import { createLiveCanvasClient } from './live-canvas.js'
|
|
19
21
|
import { registerPluginUpdater } from './updater.js'
|
|
20
22
|
import { bestEffort } from './best-effort.js'
|
|
23
|
+
import { getMoaHistory } from './history.js'
|
|
21
24
|
|
|
22
25
|
export const name = '@goodandready/dsh-moa'
|
|
23
26
|
export { collectProjectContext, formatProjectContext } from './file-workspace.js'
|
|
@@ -38,12 +41,24 @@ export function apply(ctx, config) {
|
|
|
38
41
|
refreshCatalogInBackground()
|
|
39
42
|
|
|
40
43
|
let localOverrides = null
|
|
44
|
+
let baseAtOverride = {}
|
|
41
45
|
const getSourceConfig = () => {
|
|
42
46
|
let liveCandidate = ctx.fiber?.config
|
|
43
47
|
if (!liveCandidate && ('config' in ctx)) {
|
|
44
48
|
bestEffort('ctx.config probe', () => { liveCandidate = ctx.config }, ctx.logger)
|
|
45
49
|
}
|
|
46
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
|
+
}
|
|
47
62
|
return localOverrides ? { ...liveSource, ...localOverrides } : liveSource
|
|
48
63
|
}
|
|
49
64
|
const live = () => {
|
|
@@ -85,6 +100,15 @@ export function apply(ctx, config) {
|
|
|
85
100
|
} else if (settingsService && typeof settingsService.update === 'function') {
|
|
86
101
|
await settingsService.update(NS, plain)
|
|
87
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
|
+
}
|
|
88
112
|
localOverrides = { ...(localOverrides || {}), ...plain }
|
|
89
113
|
return live()
|
|
90
114
|
}
|
|
@@ -174,14 +198,36 @@ export function apply(ctx, config) {
|
|
|
174
198
|
if (cfg.enabled !== false) {
|
|
175
199
|
const effectiveDefault = cfg.default_preset || 'default'
|
|
176
200
|
const parsed = parseMoACommand(userText, cfg.presets || [], effectiveDefault) || { prompt: userText, presetName: effectiveDefault, isExplicit: false }
|
|
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
|
+
}
|
|
207
|
+
const sessCwd = event.cwd || event.session?.cwd || (event.session?.path ? path.dirname(event.session.path) : undefined) || process.cwd()
|
|
208
|
+
const turnContext = {
|
|
209
|
+
isPromoteTurn: true,
|
|
210
|
+
runId: promoteRunId,
|
|
211
|
+
candidateIndex: parsed.candidateIndex,
|
|
212
|
+
cwd: sessCwd,
|
|
213
|
+
webServerPort: ctx.webServer?.port,
|
|
214
|
+
timeMachineEngine: ctx.reflect?.get?.('timeMachineEngine', false),
|
|
215
|
+
}
|
|
216
|
+
if (event.signal) moaPendingTurns.set(event.signal, turnContext)
|
|
217
|
+
if (event.session?.id) setSessionTurn(event.session.id, turnContext)
|
|
218
|
+
return next()
|
|
219
|
+
}
|
|
177
220
|
const isExplicitPreset = Boolean(parsed.isExplicit || userText.includes('--preset') || userText.includes('-p '))
|
|
178
221
|
if (!isExplicitPreset && (cfg.smart_routing_enabled || cfg.presets?.some((p) => p.smart_routing_enabled))) {
|
|
179
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
|
|
180
226
|
const routed = await resolvePresetForPrompt({
|
|
181
227
|
prompt: parsed.prompt || userText,
|
|
182
228
|
presets: cfg.presets || [],
|
|
183
229
|
defaultPreset: parsed.presetName || effectiveDefault,
|
|
184
|
-
routingModel
|
|
230
|
+
routingModel,
|
|
185
231
|
callLlm,
|
|
186
232
|
enabled: true,
|
|
187
233
|
})
|
|
@@ -201,8 +247,10 @@ export function apply(ctx, config) {
|
|
|
201
247
|
const aggProvider = targetPreset?.aggregator?.provider || ''
|
|
202
248
|
const aggModel = targetPreset?.aggregator?.model || ''
|
|
203
249
|
const sessCwd = event.cwd || event.session?.cwd || (event.session?.path ? path.dirname(event.session.path) : undefined) || process.cwd()
|
|
250
|
+
const runId = crypto.randomUUID()
|
|
204
251
|
|
|
205
252
|
const turnContext = {
|
|
253
|
+
runId,
|
|
206
254
|
targetPreset,
|
|
207
255
|
userPrompt: parsed.prompt || userText,
|
|
208
256
|
messages: messages.slice(0, -1),
|
|
@@ -234,11 +282,11 @@ export function apply(ctx, config) {
|
|
|
234
282
|
const turnContext = (payload.signal ? moaPendingTurns.get(payload.signal) : null)
|
|
235
283
|
|| (payload.session?.id ? getSessionTurn(payload.session.id) : null)
|
|
236
284
|
|
|
237
|
-
if (turnContext) {
|
|
285
|
+
if (turnContext && !turnContext.isPromoteTurn) {
|
|
238
286
|
return {
|
|
239
287
|
...resolved,
|
|
240
|
-
provider: turnContext.aggregator.provider,
|
|
241
|
-
model: turnContext.aggregator.model,
|
|
288
|
+
provider: turnContext.aggregator?.provider || resolved.provider,
|
|
289
|
+
model: turnContext.aggregator?.model || resolved.model,
|
|
242
290
|
}
|
|
243
291
|
}
|
|
244
292
|
return resolved
|
|
@@ -254,6 +302,9 @@ export function apply(ctx, config) {
|
|
|
254
302
|
if (options.signal) moaPendingTurns.delete(options.signal)
|
|
255
303
|
if (options.session?.id) consumeSessionTurn(options.session.id)
|
|
256
304
|
|
|
305
|
+
if (turnContext.isPromoteTurn) {
|
|
306
|
+
return streamPromoteTurn(turnContext, options)
|
|
307
|
+
}
|
|
257
308
|
return streamMoATurn(turnContext, options)
|
|
258
309
|
}
|
|
259
310
|
|
package/lib/live-canvas.js
CHANGED
|
@@ -1,17 +1,50 @@
|
|
|
1
1
|
import path from "node:path"
|
|
2
|
+
import fs from "node:fs/promises"
|
|
2
3
|
|
|
3
|
-
export async function createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs) {
|
|
4
|
+
export async function createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs, options = {}) {
|
|
4
5
|
if (!liveCanvas || typeof liveCanvas.createPreviewFromContent !== "function") return null
|
|
5
6
|
const htmlRel = (promotedFiles || []).find((f) => f.endsWith(".html") || f.endsWith(".htm"))
|
|
6
7
|
if (!htmlRel) return null
|
|
8
|
+
|
|
7
9
|
let content = null
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
|
|
11
|
+
// 1. Try reading directly from disk in cwd (the promoted winner or curator synthesis)
|
|
12
|
+
if (cwd) {
|
|
13
|
+
try {
|
|
14
|
+
const fullPath = path.resolve(cwd, htmlRel)
|
|
15
|
+
content = await fs.readFile(fullPath, "utf8")
|
|
16
|
+
} catch {
|
|
17
|
+
content = null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 2. Fallback to synthesized files / winning files
|
|
22
|
+
if (!content) {
|
|
23
|
+
const winningFiles = options.winningFiles || options.synthesizedFiles
|
|
24
|
+
if (Array.isArray(winningFiles)) {
|
|
25
|
+
const block = winningFiles.find((f) => f.relativePath === htmlRel)
|
|
26
|
+
if (block?.content) content = block.content
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 3. Fallback to winning candidate by index
|
|
31
|
+
if (!content && typeof options.winningIndex === 'number' && referenceOutputs) {
|
|
32
|
+
const winnerRef = referenceOutputs[options.winningIndex - 1]
|
|
33
|
+
const block = (winnerRef?.files || []).find((f) => f.relativePath === htmlRel)
|
|
34
|
+
if (block?.content) content = block.content
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 4. Fallback to searching referenceOutputs
|
|
38
|
+
if (!content) {
|
|
39
|
+
for (const r of referenceOutputs || []) {
|
|
40
|
+
const block = (r?.files || []).find((f) => f.relativePath === htmlRel)
|
|
41
|
+
if (block?.content) {
|
|
42
|
+
content = block.content
|
|
43
|
+
break
|
|
44
|
+
}
|
|
13
45
|
}
|
|
14
46
|
}
|
|
47
|
+
|
|
15
48
|
if (!content) return null
|
|
16
49
|
try {
|
|
17
50
|
return await liveCanvas.createPreviewFromContent({ content, title: htmlRel, filePath: path.join(cwd, htmlRel) })
|
package/lib/moa-budget.js
CHANGED
|
@@ -74,25 +74,29 @@ export function applyBudgetGuardrails({
|
|
|
74
74
|
// Base candidate cost per slot (Round 1)
|
|
75
75
|
const { totalEstimatedUsd, slotEstimates } = estimateCandidateRunCost(references, prices, promptTokens, expectedOutputTokens)
|
|
76
76
|
|
|
77
|
-
// Estimate
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
77
|
+
// Estimate cost for downstream phases (Aggregator / Judge panel)
|
|
78
|
+
function calcDownstreamUsd(numCandidates) {
|
|
79
|
+
let downstream = 0
|
|
80
|
+
if (aggregator?.provider && aggregator?.model) {
|
|
81
|
+
const aggInput = promptTokens + expectedOutputTokens * numCandidates
|
|
82
|
+
const aggCost = estimateTokenCost(aggregator, { inputTokens: aggInput, outputTokens: expectedOutputTokens }, prices)
|
|
83
|
+
downstream += aggCost?.costUsd || 0
|
|
84
|
+
}
|
|
85
|
+
if (Array.isArray(judgeModels) && judgeModels.length > 0) {
|
|
86
|
+
for (const jm of judgeModels) {
|
|
87
|
+
if (jm?.provider && jm?.model) {
|
|
88
|
+
const jInput = promptTokens + expectedOutputTokens * numCandidates
|
|
89
|
+
const jCost = estimateTokenCost(jm, { inputTokens: jInput, outputTokens: expectedOutputTokens }, prices)
|
|
90
|
+
downstream += jCost?.costUsd || 0
|
|
91
|
+
}
|
|
90
92
|
}
|
|
91
93
|
}
|
|
94
|
+
return downstream
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
// Multiplier if peer critique (Round 2) is enabled: each candidate runs another turn with input from all peers
|
|
95
98
|
const roundMultiplier = peerCritiqueEnabled ? 2 : 1
|
|
99
|
+
const downstreamUsd = calcDownstreamUsd(references.length)
|
|
96
100
|
const fullEstimatedUsd = Number(((totalEstimatedUsd * roundMultiplier) + downstreamUsd).toFixed(6))
|
|
97
101
|
|
|
98
102
|
if (fullEstimatedUsd <= maxBudgetUsd) {
|
|
@@ -116,38 +120,43 @@ export function applyBudgetGuardrails({
|
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
// Action is 'trim': Sort by estimated cost ascending (cheapest first)
|
|
123
|
+
// Factor downstream synthesis cost into candidate selection (#158)
|
|
119
124
|
const sorted = [...slotEstimates].sort((a, b) => a.costUsd - b.costUsd)
|
|
120
125
|
const kept = []
|
|
121
|
-
let
|
|
126
|
+
let cumulativeCandidateCost = 0
|
|
122
127
|
|
|
123
128
|
for (const item of sorted) {
|
|
124
129
|
const candidateCost = item.costUsd * roundMultiplier
|
|
125
|
-
|
|
130
|
+
const nextDownstream = calcDownstreamUsd(kept.length + 1)
|
|
131
|
+
if (cumulativeCandidateCost + candidateCost + nextDownstream <= maxBudgetUsd) {
|
|
126
132
|
kept.push(item.slot)
|
|
127
|
-
|
|
133
|
+
cumulativeCandidateCost += candidateCost
|
|
128
134
|
}
|
|
129
135
|
}
|
|
130
136
|
|
|
131
137
|
if (kept.length === 0) {
|
|
138
|
+
const minCandidateCost = (sorted[0]?.costUsd || 0) * roundMultiplier
|
|
139
|
+
const minTotal = minCandidateCost + calcDownstreamUsd(1)
|
|
132
140
|
return {
|
|
133
141
|
allowed: false,
|
|
134
142
|
references: [],
|
|
135
143
|
action: 'trim',
|
|
136
144
|
originalCount: references.length,
|
|
137
145
|
trimmedCount: 0,
|
|
138
|
-
estimatedCostUsd: Number(
|
|
146
|
+
estimatedCostUsd: Number(minTotal.toFixed(6)),
|
|
139
147
|
maxBudgetUsd,
|
|
140
|
-
reason: `Cannot fit any candidate model within budget ($${maxBudgetUsd.toFixed(4)}). Cheapest candidate requires $${
|
|
148
|
+
reason: `Cannot fit any candidate model within budget ($${maxBudgetUsd.toFixed(4)}). Cheapest candidate requires $${minTotal.toFixed(4)}.`,
|
|
141
149
|
}
|
|
142
150
|
}
|
|
143
151
|
|
|
152
|
+
const finalEstimatedUsd = Number((cumulativeCandidateCost + calcDownstreamUsd(kept.length)).toFixed(6))
|
|
144
153
|
return {
|
|
145
154
|
allowed: true,
|
|
146
155
|
references: kept,
|
|
147
156
|
action: 'trim',
|
|
148
157
|
originalCount: references.length,
|
|
149
158
|
trimmedCount: kept.length,
|
|
150
|
-
estimatedCostUsd:
|
|
159
|
+
estimatedCostUsd: finalEstimatedUsd,
|
|
151
160
|
maxBudgetUsd,
|
|
152
161
|
reason: `Trimmed candidate pool from ${references.length} to ${kept.length} models to stay within budget ($${maxBudgetUsd.toFixed(4)}).`,
|
|
153
162
|
}
|
package/lib/moa-candidates.js
CHANGED
|
@@ -122,9 +122,11 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
122
122
|
})
|
|
123
123
|
reject(new Error(`Timeout after ${Math.round(timeoutMs / 1000)}s`))
|
|
124
124
|
}, timeoutMs)
|
|
125
|
+
candidateTimer?.unref?.()
|
|
125
126
|
})
|
|
126
127
|
|
|
127
128
|
const res = await Promise.race([callPromise, timeoutPromise])
|
|
129
|
+
if (candidateTimer) { clearTimeout(candidateTimer); candidateTimer = null }
|
|
128
130
|
const text = typeof res === 'string' ? res : (res?.content || res?.text || '')
|
|
129
131
|
|
|
130
132
|
const fallbackUsage = (typeof res === 'object' && res?.usage) ? res.usage : {
|
|
@@ -275,6 +277,7 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
275
277
|
}
|
|
276
278
|
}
|
|
277
279
|
} finally {
|
|
280
|
+
if (candidateTimer) clearTimeout(candidateTimer)
|
|
278
281
|
notifyFinished()
|
|
279
282
|
}
|
|
280
283
|
}
|
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:
|