@goodandready/dsh-moa 0.2.40 → 0.2.42
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 +407 -98
- package/lib/history.js +169 -35
- package/lib/index.js +21 -3
- package/lib/live-canvas.js +39 -6
- package/lib/moa-multi-judge.js +151 -59
- package/lib/moa-report.js +41 -20
- package/lib/moa-runner.js +11 -6
- package/lib/moa-stream.js +57 -1
- package/lib/pricing.js +10 -6
- package/lib/routes.js +3 -2
- package/lib/updater.js +23 -0
- package/package.json +1 -1
package/lib/moa-multi-judge.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { slotLabel } from './moa-prompts.js'
|
|
7
|
+
import { estimateTokenCost } from './pricing.js'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Extracts code fences and block-level assets from markdown text.
|
|
@@ -40,8 +41,10 @@ export function extractCodeBlocks(text = '') {
|
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
43
|
* Builds composite block directives highlighting complementary modules across candidates.
|
|
44
|
+
* Supports blind evaluation (#167).
|
|
43
45
|
*/
|
|
44
|
-
export function buildCompositeBlockDirectives(candidates = []) {
|
|
46
|
+
export function buildCompositeBlockDirectives(candidates = [], options = {}) {
|
|
47
|
+
const isBlind = Boolean(options === true || options?.blind)
|
|
45
48
|
const fileMap = new Map()
|
|
46
49
|
|
|
47
50
|
for (const c of candidates) {
|
|
@@ -52,7 +55,7 @@ export function buildCompositeBlockDirectives(candidates = []) {
|
|
|
52
55
|
if (!fileMap.has(b.filename)) {
|
|
53
56
|
fileMap.set(b.filename, [])
|
|
54
57
|
}
|
|
55
|
-
fileMap.get(b.filename).push(c.label || `Candidate ${c.index}`)
|
|
58
|
+
fileMap.get(b.filename).push(isBlind ? `Candidate ${c.index}` : (c.label || `Candidate ${c.index}`))
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
}
|
|
@@ -79,17 +82,38 @@ ${fileOverview}============================================
|
|
|
79
82
|
|
|
80
83
|
/**
|
|
81
84
|
* Parses judge output into structured scores and candidate choice.
|
|
85
|
+
* Accepts either candidateCount or candidate array/set to support active candidate indexing (#166).
|
|
82
86
|
*/
|
|
83
|
-
export function parseJudgeEvaluation(rawText = '',
|
|
87
|
+
export function parseJudgeEvaluation(rawText = '', candidatesOrCount = 2) {
|
|
88
|
+
let allowedIndices = null
|
|
89
|
+
let defaultCandidate = 1
|
|
90
|
+
|
|
91
|
+
if (Array.isArray(candidatesOrCount)) {
|
|
92
|
+
const active = candidatesOrCount.filter((c) => c && c.ok !== false)
|
|
93
|
+
allowedIndices = new Set(active.map((c) => c.index))
|
|
94
|
+
if (active.length > 0 && typeof active[0].index === 'number') {
|
|
95
|
+
defaultCandidate = active[0].index
|
|
96
|
+
}
|
|
97
|
+
} else if (candidatesOrCount instanceof Set) {
|
|
98
|
+
allowedIndices = candidatesOrCount
|
|
99
|
+
defaultCandidate = Array.from(allowedIndices)[0] || 1
|
|
100
|
+
} else if (typeof candidatesOrCount === 'number') {
|
|
101
|
+
allowedIndices = new Set(Array.from({ length: candidatesOrCount }, (_, i) => i + 1))
|
|
102
|
+
defaultCandidate = 1
|
|
103
|
+
} else {
|
|
104
|
+
allowedIndices = new Set([1, 2])
|
|
105
|
+
defaultCandidate = 1
|
|
106
|
+
}
|
|
107
|
+
|
|
84
108
|
const scores = {}
|
|
85
|
-
let bestCandidate =
|
|
109
|
+
let bestCandidate = defaultCandidate
|
|
86
110
|
let reasoning = ''
|
|
87
111
|
|
|
88
112
|
const scoreMatches = rawText.matchAll(/Candidate\s*(\d+)[:\s]+(?:Score\s*[:\s]*)?([0-9]+(?:\.[0-9]+)?)/gi)
|
|
89
113
|
for (const m of scoreMatches) {
|
|
90
114
|
const idx = parseInt(m[1], 10)
|
|
91
115
|
const val = parseFloat(m[2])
|
|
92
|
-
if (idx
|
|
116
|
+
if (allowedIndices.has(idx) && !Number.isNaN(val)) {
|
|
93
117
|
scores[idx] = Math.min(10, Math.max(0, val))
|
|
94
118
|
}
|
|
95
119
|
}
|
|
@@ -97,16 +121,17 @@ export function parseJudgeEvaluation(rawText = '', candidateCount = 2) {
|
|
|
97
121
|
const bestMatch = rawText.match(/BEST_CANDIDATE\s*[:\s]+(?:Candidate\s*)?(\d+)/i)
|
|
98
122
|
if (bestMatch) {
|
|
99
123
|
const parsedBest = parseInt(bestMatch[1], 10)
|
|
100
|
-
if (parsedBest
|
|
124
|
+
if (allowedIndices.has(parsedBest)) {
|
|
101
125
|
bestCandidate = parsedBest
|
|
102
126
|
}
|
|
103
127
|
} else {
|
|
104
|
-
// Find candidate with max parsed score
|
|
128
|
+
// Find candidate with max parsed score among allowed indices
|
|
105
129
|
let maxScore = -1
|
|
106
130
|
for (const [idxStr, s] of Object.entries(scores)) {
|
|
107
|
-
|
|
131
|
+
const idxNum = parseInt(idxStr, 10)
|
|
132
|
+
if (allowedIndices.has(idxNum) && s > maxScore) {
|
|
108
133
|
maxScore = s
|
|
109
|
-
bestCandidate =
|
|
134
|
+
bestCandidate = idxNum
|
|
110
135
|
}
|
|
111
136
|
}
|
|
112
137
|
}
|
|
@@ -114,25 +139,27 @@ export function parseJudgeEvaluation(rawText = '', candidateCount = 2) {
|
|
|
114
139
|
const reasonMatch = rawText.match(/CONSENSUS_REASONING\s*[:\s]+([^\n\r]+)/i)
|
|
115
140
|
if (reasonMatch) {
|
|
116
141
|
reasoning = reasonMatch[1].trim()
|
|
117
|
-
} else {
|
|
118
|
-
reasoning = rawText.slice(0, 180).replace(/\n/g, ' ').trim()
|
|
119
142
|
}
|
|
120
143
|
|
|
121
144
|
return { scores, bestCandidate, reasoning }
|
|
122
145
|
}
|
|
123
146
|
|
|
124
147
|
/**
|
|
125
|
-
*
|
|
148
|
+
* Runs a multi-judge consensus panel across candidate solutions.
|
|
149
|
+
* Supports majority, unanimous, and highest_score strategies.
|
|
150
|
+
* Returns consensus, votes, usage, and cost tracking (#165, #167, #168).
|
|
126
151
|
*/
|
|
127
152
|
export async function executeMultiJudgePanel({
|
|
128
153
|
judges = [],
|
|
129
154
|
candidates = [],
|
|
130
155
|
userPrompt = '',
|
|
131
156
|
callLlm,
|
|
132
|
-
strategy = 'majority',
|
|
157
|
+
strategy = 'majority',
|
|
133
158
|
timeoutMs = 45000,
|
|
134
159
|
signal,
|
|
135
160
|
onProgress,
|
|
161
|
+
prices = {},
|
|
162
|
+
blind = false,
|
|
136
163
|
}) {
|
|
137
164
|
const activeCandidates = (candidates || []).filter((c) => c && c.ok)
|
|
138
165
|
if (activeCandidates.length === 0) {
|
|
@@ -140,6 +167,12 @@ export async function executeMultiJudgePanel({
|
|
|
140
167
|
consensus: false,
|
|
141
168
|
winningCandidateIndex: 1,
|
|
142
169
|
strategy,
|
|
170
|
+
isUnanimous: false,
|
|
171
|
+
votes: {},
|
|
172
|
+
averageScores: {},
|
|
173
|
+
judgesResults: [],
|
|
174
|
+
totalUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
175
|
+
totalCostUsd: 0,
|
|
143
176
|
reason: 'no_active_candidates',
|
|
144
177
|
}
|
|
145
178
|
}
|
|
@@ -150,46 +183,52 @@ export async function executeMultiJudgePanel({
|
|
|
150
183
|
winningCandidateIndex: activeCandidates[0].index,
|
|
151
184
|
winningCandidateLabel: activeCandidates[0].label,
|
|
152
185
|
strategy,
|
|
186
|
+
isUnanimous: true,
|
|
187
|
+
votes: { [activeCandidates[0].index]: 1 },
|
|
188
|
+
averageScores: { [activeCandidates[0].index]: 10.0 },
|
|
189
|
+
judgesResults: [],
|
|
190
|
+
totalUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
191
|
+
totalCostUsd: 0,
|
|
153
192
|
reason: 'single_active_candidate',
|
|
154
193
|
}
|
|
155
194
|
}
|
|
156
195
|
|
|
157
|
-
const
|
|
196
|
+
const judgePanel = (judges && judges.length > 0)
|
|
158
197
|
? judges
|
|
159
198
|
: [{ provider: 'default', model: 'aggregator-judge', label: 'Primary Judge' }]
|
|
160
199
|
|
|
200
|
+
// Blind evaluation: mask model labels in judge prompt (#167)
|
|
161
201
|
const candidateSummary = activeCandidates.map((c) => {
|
|
162
|
-
|
|
202
|
+
const label = blind ? `Candidate ${c.index}` : `Candidate ${c.index} (${c.label})`
|
|
203
|
+
return `### ${label}:\n${(c.text || '').slice(0, 4000)}`
|
|
163
204
|
}).join('\n\n---\n\n')
|
|
164
205
|
|
|
165
206
|
const judgePrompt = `You are an impartial Expert Code & Logic Judge evaluating multiple AI candidate solutions.
|
|
166
|
-
User
|
|
167
|
-
${userPrompt
|
|
207
|
+
User Task:
|
|
208
|
+
${userPrompt}
|
|
168
209
|
|
|
169
|
-
|
|
210
|
+
Candidates Under Review:
|
|
170
211
|
${candidateSummary}
|
|
171
212
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
213
|
+
INSTRUCTIONS:
|
|
214
|
+
1. Carefully compare all candidates on correctness, efficiency, architectural cleanliness, and edge-case resilience.
|
|
215
|
+
2. Assign each candidate a score from 0.0 to 10.0.
|
|
216
|
+
3. State your chosen best candidate and a brief reasoning sentence.
|
|
175
217
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
218
|
+
Format your response EXACTLY as follows:
|
|
219
|
+
CANDIDATE_EVALUATION:
|
|
220
|
+
Candidate [index]: Score [0-10]
|
|
221
|
+
...
|
|
222
|
+
BEST_CANDIDATE: Candidate [winning index]
|
|
180
223
|
CONSENSUS_REASONING: [one concise sentence explaining your pick]
|
|
181
224
|
`
|
|
182
225
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const judgePromises = judgeSlots.map(async (judge, jIdx) => {
|
|
188
|
-
const label = slotLabel(judge)
|
|
226
|
+
const judgePromises = judgePanel.map(async (judge, jIdx) => {
|
|
227
|
+
const label = judge.label || slotLabel(judge)
|
|
189
228
|
try {
|
|
190
229
|
const abortCtrl = new AbortController()
|
|
191
230
|
if (signal) {
|
|
192
|
-
signal.addEventListener('abort', () => abortCtrl.abort(), { once: true })
|
|
231
|
+
signal.addEventListener('abort', () => abortCtrl.abort(signal.reason || new Error('Turn aborted')), { once: true })
|
|
193
232
|
}
|
|
194
233
|
const timer = setTimeout(() => abortCtrl.abort(new Error('Judge evaluation timeout')), timeoutMs)
|
|
195
234
|
timer.unref?.()
|
|
@@ -198,22 +237,31 @@ CONSENSUS_REASONING: [one concise sentence explaining your pick]
|
|
|
198
237
|
provider: judge.provider,
|
|
199
238
|
model: judge.model,
|
|
200
239
|
messages: [
|
|
201
|
-
{ role: 'system', content: 'You are an objective
|
|
240
|
+
{ role: 'system', content: 'You are an objective AI code evaluation judge.' },
|
|
202
241
|
{ role: 'user', content: judgePrompt },
|
|
203
242
|
],
|
|
204
|
-
temperature: 0.1,
|
|
205
|
-
maxTokens: 512,
|
|
206
243
|
signal: abortCtrl.signal,
|
|
207
244
|
})
|
|
245
|
+
|
|
208
246
|
clearTimeout(timer)
|
|
209
247
|
|
|
210
248
|
const text = typeof res === 'string' ? res : (res?.content || res?.text || '')
|
|
211
|
-
|
|
249
|
+
// Preserve active candidate indices rather than assuming 1..N (#166)
|
|
250
|
+
const evalResult = parseJudgeEvaluation(text, activeCandidates)
|
|
251
|
+
|
|
252
|
+
// Track usage and cost (#168)
|
|
253
|
+
const usage = (typeof res === 'object' && res?.usage) ? res.usage : {
|
|
254
|
+
inputTokens: Math.max(1, Math.round((judgePrompt.length + 50) / 4)),
|
|
255
|
+
outputTokens: Math.max(1, Math.round(text.length / 4)),
|
|
256
|
+
}
|
|
257
|
+
const costInfo = estimateTokenCost(judge, usage, prices)
|
|
212
258
|
|
|
213
259
|
return {
|
|
214
260
|
judgeIndex: jIdx + 1,
|
|
215
261
|
judgeLabel: label,
|
|
216
262
|
ok: true,
|
|
263
|
+
usage,
|
|
264
|
+
costUsd: costInfo.costUsd || 0,
|
|
217
265
|
...evalResult,
|
|
218
266
|
}
|
|
219
267
|
} catch (err) {
|
|
@@ -221,8 +269,10 @@ CONSENSUS_REASONING: [one concise sentence explaining your pick]
|
|
|
221
269
|
judgeIndex: jIdx + 1,
|
|
222
270
|
judgeLabel: label,
|
|
223
271
|
ok: false,
|
|
224
|
-
bestCandidate:
|
|
272
|
+
bestCandidate: activeCandidates[0].index,
|
|
225
273
|
scores: {},
|
|
274
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
275
|
+
costUsd: 0,
|
|
226
276
|
reasoning: err?.message || String(err),
|
|
227
277
|
}
|
|
228
278
|
}
|
|
@@ -231,64 +281,104 @@ CONSENSUS_REASONING: [one concise sentence explaining your pick]
|
|
|
231
281
|
const results = await Promise.all(judgePromises)
|
|
232
282
|
const validResults = results.filter((r) => r.ok)
|
|
233
283
|
|
|
284
|
+
// Accumulate total usage and cost across all judges (#168)
|
|
285
|
+
let sumInTokens = 0
|
|
286
|
+
let sumOutTokens = 0
|
|
287
|
+
let sumCostUsd = 0
|
|
288
|
+
for (const r of results) {
|
|
289
|
+
if (r.usage) {
|
|
290
|
+
sumInTokens += r.usage.inputTokens || r.usage.promptTokens || 0
|
|
291
|
+
sumOutTokens += r.usage.outputTokens || r.usage.completionTokens || 0
|
|
292
|
+
}
|
|
293
|
+
if (typeof r.costUsd === 'number') {
|
|
294
|
+
sumCostUsd += r.costUsd
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const totalUsage = {
|
|
298
|
+
inputTokens: sumInTokens,
|
|
299
|
+
outputTokens: sumOutTokens,
|
|
300
|
+
totalTokens: sumInTokens + sumOutTokens,
|
|
301
|
+
costUsd: Number(sumCostUsd.toFixed(5)),
|
|
302
|
+
}
|
|
303
|
+
const totalCostUsd = totalUsage.costUsd
|
|
304
|
+
|
|
305
|
+
// #165: If all judges failed, consensus is false with 0 votes
|
|
306
|
+
if (validResults.length === 0) {
|
|
307
|
+
return {
|
|
308
|
+
consensus: false,
|
|
309
|
+
winningCandidateIndex: activeCandidates[0].index,
|
|
310
|
+
winningCandidateLabel: activeCandidates[0].label,
|
|
311
|
+
strategy,
|
|
312
|
+
isUnanimous: false,
|
|
313
|
+
votes: {},
|
|
314
|
+
averageScores: {},
|
|
315
|
+
judgesResults: results,
|
|
316
|
+
totalUsage,
|
|
317
|
+
totalCostUsd,
|
|
318
|
+
reason: 'all_judges_failed',
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
234
322
|
// Tally votes and compute average scores
|
|
235
323
|
const votes = {}
|
|
236
324
|
const totalScores = {}
|
|
237
325
|
const scoreCounts = {}
|
|
238
326
|
|
|
239
|
-
for (const
|
|
240
|
-
votes[
|
|
241
|
-
for (const [
|
|
242
|
-
totalScores[
|
|
243
|
-
scoreCounts[
|
|
327
|
+
for (const vr of validResults) {
|
|
328
|
+
votes[vr.bestCandidate] = (votes[vr.bestCandidate] || 0) + 1
|
|
329
|
+
for (const [candIdx, sc] of Object.entries(vr.scores)) {
|
|
330
|
+
totalScores[candIdx] = (totalScores[candIdx] || 0) + sc
|
|
331
|
+
scoreCounts[candIdx] = (scoreCounts[candIdx] || 0) + 1
|
|
244
332
|
}
|
|
245
333
|
}
|
|
246
334
|
|
|
247
335
|
const averageScores = {}
|
|
248
|
-
for (const
|
|
249
|
-
|
|
250
|
-
averageScores[c.index] = cnt > 0 ? Number((totalScores[c.index] / cnt).toFixed(2)) : 5.0
|
|
336
|
+
for (const [candIdx, sum] of Object.entries(totalScores)) {
|
|
337
|
+
averageScores[candIdx] = Number((sum / (scoreCounts[candIdx] || 1)).toFixed(2))
|
|
251
338
|
}
|
|
252
339
|
|
|
253
340
|
let winner = activeCandidates[0].index
|
|
254
341
|
let isUnanimous = false
|
|
342
|
+
let hasConsensus = true
|
|
255
343
|
|
|
256
344
|
if (strategy === 'highest_score') {
|
|
257
345
|
let highestAvg = -1
|
|
258
346
|
for (const c of activeCandidates) {
|
|
259
|
-
const avg = averageScores[c.index]
|
|
347
|
+
const avg = averageScores[c.index] ?? -1
|
|
260
348
|
if (avg > highestAvg) {
|
|
261
349
|
highestAvg = avg
|
|
262
350
|
winner = c.index
|
|
263
351
|
}
|
|
264
352
|
}
|
|
353
|
+
hasConsensus = validResults.length > 0
|
|
265
354
|
} else {
|
|
266
355
|
// majority or unanimous
|
|
267
356
|
let maxVotes = -1
|
|
268
|
-
for (const
|
|
269
|
-
const
|
|
270
|
-
if (
|
|
271
|
-
maxVotes =
|
|
272
|
-
winner =
|
|
273
|
-
} else if (v === maxVotes && (averageScores[c.index] || 0) > (averageScores[winner] || 0)) {
|
|
274
|
-
winner = c.index
|
|
357
|
+
for (const [cIdxStr, count] of Object.entries(votes)) {
|
|
358
|
+
const cIdx = parseInt(cIdxStr, 10)
|
|
359
|
+
if (count > maxVotes) {
|
|
360
|
+
maxVotes = count
|
|
361
|
+
winner = cIdx
|
|
275
362
|
}
|
|
276
363
|
}
|
|
277
364
|
isUnanimous = (votes[winner] || 0) === validResults.length && validResults.length > 0
|
|
365
|
+
if (strategy === 'unanimous') {
|
|
366
|
+
hasConsensus = isUnanimous && validResults.length > 0
|
|
367
|
+
} else {
|
|
368
|
+
hasConsensus = maxVotes > (validResults.length / 2) || validResults.length === 1
|
|
369
|
+
}
|
|
278
370
|
}
|
|
279
371
|
|
|
280
372
|
const winningCand = activeCandidates.find((c) => c.index === winner) || activeCandidates[0]
|
|
281
|
-
|
|
282
|
-
const consensusReport = `Multi-
|
|
283
|
-
`Winner is Candidate ${winningCand.index} (${winningCand.label}) with ${votes[winner] || 0} votes, ` +
|
|
284
|
-
`avg score ${averageScores[winner] || 0}/10. Strategy: ${strategy}.`
|
|
373
|
+
const winnerVotes = votes[winner] || 0
|
|
374
|
+
const consensusReport = `Multi-judge consensus (${strategy}): Candidate ${winner} (${winningCand.label}) won with ${winnerVotes}/${validResults.length} votes.`
|
|
285
375
|
|
|
286
376
|
if (typeof onProgress === 'function') {
|
|
287
|
-
onProgress(
|
|
377
|
+
onProgress(`⚖️ *${consensusReport}*\n`)
|
|
288
378
|
}
|
|
289
379
|
|
|
290
380
|
return {
|
|
291
|
-
consensus:
|
|
381
|
+
consensus: hasConsensus,
|
|
292
382
|
winningCandidateIndex: winningCand.index,
|
|
293
383
|
winningCandidateLabel: winningCand.label,
|
|
294
384
|
strategy,
|
|
@@ -296,6 +386,8 @@ CONSENSUS_REASONING: [one concise sentence explaining your pick]
|
|
|
296
386
|
votes,
|
|
297
387
|
averageScores,
|
|
298
388
|
judgesResults: results,
|
|
389
|
+
totalUsage,
|
|
390
|
+
totalCostUsd,
|
|
299
391
|
consensusReport,
|
|
300
392
|
}
|
|
301
393
|
}
|
package/lib/moa-report.js
CHANGED
|
@@ -20,17 +20,19 @@ export function generateMoABenchmarkReport({
|
|
|
20
20
|
}) {
|
|
21
21
|
const durationSec = (durationMs / 1000).toFixed(2)
|
|
22
22
|
const totalCost = typeof costUsd === 'number' ? costUsd.toFixed(4) : '0.0000'
|
|
23
|
-
const inTokens = usage?.inputTokens
|
|
24
|
-
const outTokens = usage?.outputTokens
|
|
25
|
-
const totalTokens = usage?.totalTokens
|
|
23
|
+
const inTokens = usage?.inputTokens ?? usage?.totalInputTokens ?? usage?.prompt_tokens ?? 0
|
|
24
|
+
const outTokens = usage?.outputTokens ?? usage?.totalOutputTokens ?? usage?.completion_tokens ?? 0
|
|
25
|
+
const totalTokens = usage?.totalTokens ?? (inTokens + outTokens)
|
|
26
26
|
|
|
27
27
|
const candidateRows = (candidates || []).map((c) => {
|
|
28
28
|
const idx = c.index || '?'
|
|
29
29
|
const label = c.label || `${c.slot?.provider}:${c.slot?.model}`
|
|
30
|
-
const role = c.role_persona || 'general'
|
|
30
|
+
const role = c.role_persona || c.role || 'general'
|
|
31
31
|
const status = c.ok ? '✅ OK' : '❌ Failed'
|
|
32
|
-
const
|
|
33
|
-
const
|
|
32
|
+
const cIn = c.usage?.inputTokens ?? c.usage?.totalInputTokens ?? c.usage?.prompt_tokens ?? 0
|
|
33
|
+
const cOut = c.usage?.outputTokens ?? c.usage?.totalOutputTokens ?? c.usage?.completion_tokens ?? 0
|
|
34
|
+
const candTokens = c.usage?.totalTokens ?? (cIn + cOut)
|
|
35
|
+
const candCost = typeof c.costUsd === 'number' ? `$${c.costUsd.toFixed(4)}` : '$0.0000'
|
|
34
36
|
const score = consensus?.averageScores?.[c.index] !== undefined
|
|
35
37
|
? `${consensus.averageScores[c.index]}/10`
|
|
36
38
|
: 'N/A'
|
|
@@ -39,16 +41,31 @@ export function generateMoABenchmarkReport({
|
|
|
39
41
|
}).join('\n')
|
|
40
42
|
|
|
41
43
|
let testGateSection = '### 🧪 Automated Test Gate\n- **Status**: Not Configured / Skipped'
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
const gates = Array.isArray(testGate) ? testGate : (testGate ? [testGate] : [])
|
|
45
|
+
const activeGates = gates.filter((tg) => tg && (tg.enabled !== false || tg.passed !== undefined))
|
|
46
|
+
if (activeGates.length === 1) {
|
|
47
|
+
const tg = activeGates[0]
|
|
48
|
+
const tgStatus = tg.passed ? '✅ PASSED' : '❌ FAILED'
|
|
44
49
|
testGateSection = `### 🧪 Automated Test Gate
|
|
45
50
|
- **Status**: ${tgStatus}
|
|
46
|
-
- **Test Command**: \`${
|
|
47
|
-
- **Exit Code**: \`${
|
|
51
|
+
- **Test Command**: \`${tg.command || 'N/A'}\`
|
|
52
|
+
- **Exit Code**: \`${tg.exitCode ?? 0}\`
|
|
48
53
|
- **Output Snippet**:
|
|
49
54
|
\`\`\`
|
|
50
|
-
${(
|
|
55
|
+
${(tg.output || tg.summary || 'No output recorded').slice(0, 500)}
|
|
51
56
|
\`\`\``
|
|
57
|
+
} else if (activeGates.length > 1) {
|
|
58
|
+
const allPassed = activeGates.every((tg) => tg.passed)
|
|
59
|
+
const passedCount = activeGates.filter((tg) => tg.passed).length
|
|
60
|
+
const overallStatus = allPassed ? '✅ ALL PASSED' : `❌ ${activeGates.length - passedCount}/${activeGates.length} FAILED`
|
|
61
|
+
const details = activeGates.map((tg, idx) => {
|
|
62
|
+
const label = tg.label || tg.model || `Candidate ${tg.candidateIndex ?? idx + 1}`
|
|
63
|
+
const tgStatus = tg.passed ? '✅ PASSED' : '❌ FAILED'
|
|
64
|
+
return `- **${label}**: ${tgStatus} (Command: \`${tg.command || 'N/A'}\`, Exit Code: \`${tg.exitCode ?? 0}\`)\n\`\`\`\n${(tg.output || tg.summary || 'No output recorded').slice(0, 300)}\n\`\`\``
|
|
65
|
+
}).join('\n')
|
|
66
|
+
testGateSection = `### 🧪 Automated Test Gate
|
|
67
|
+
- **Status**: ${overallStatus}
|
|
68
|
+
${details}`
|
|
52
69
|
}
|
|
53
70
|
|
|
54
71
|
let consensusSection = '### ⚖️ Multi-Judge Consensus\n- **Status**: Standard single-aggregator synthesis'
|
|
@@ -104,15 +121,19 @@ ${testGateSection}
|
|
|
104
121
|
durationMs,
|
|
105
122
|
usage: { inputTokens: inTokens, outputTokens: outTokens, totalTokens },
|
|
106
123
|
costUsd,
|
|
107
|
-
candidates: (candidates || []).map((c) =>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
124
|
+
candidates: (candidates || []).map((c) => {
|
|
125
|
+
const cIn = c.usage?.inputTokens ?? c.usage?.totalInputTokens ?? c.usage?.prompt_tokens ?? 0
|
|
126
|
+
const cOut = c.usage?.outputTokens ?? c.usage?.totalOutputTokens ?? c.usage?.completion_tokens ?? 0
|
|
127
|
+
return {
|
|
128
|
+
index: c.index,
|
|
129
|
+
label: c.label,
|
|
130
|
+
role: c.role_persona || c.role,
|
|
131
|
+
ok: c.ok,
|
|
132
|
+
costUsd: typeof c.costUsd === 'number' ? c.costUsd : 0,
|
|
133
|
+
usage: { inputTokens: cIn, outputTokens: cOut, totalTokens: c.usage?.totalTokens ?? (cIn + cOut) },
|
|
134
|
+
score: consensus?.averageScores?.[c.index] ?? null,
|
|
135
|
+
}
|
|
136
|
+
}),
|
|
116
137
|
winningIndex,
|
|
117
138
|
winningLabel,
|
|
118
139
|
consensus,
|
package/lib/moa-runner.js
CHANGED
|
@@ -18,7 +18,7 @@ export { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSu
|
|
|
18
18
|
export { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
|
|
19
19
|
export { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel } from './moa-candidates.js'
|
|
20
20
|
export { executeTestGateForCandidates, runCandidateTestGate } from './moa-test-gate.js'
|
|
21
|
-
export { streamMoATurn } from './moa-stream.js'
|
|
21
|
+
export { streamMoATurn, streamPromoteTurn } from './moa-stream.js'
|
|
22
22
|
/**
|
|
23
23
|
* Executes the full Mixture of Agents pipeline.
|
|
24
24
|
*/
|
|
@@ -229,6 +229,7 @@ export async function runMoAPipeline({
|
|
|
229
229
|
const durationMs = Date.now() - startTime
|
|
230
230
|
try {
|
|
231
231
|
await recordMoaRunAsync({
|
|
232
|
+
cwd,
|
|
232
233
|
prompt: userPrompt,
|
|
233
234
|
preset: preset?.name || 'default',
|
|
234
235
|
isRefinement,
|
|
@@ -281,13 +282,14 @@ export async function runMoAPipeline({
|
|
|
281
282
|
await cleanMoaWorkspaces(cwd, { runId })
|
|
282
283
|
}
|
|
283
284
|
|
|
284
|
-
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs)
|
|
285
|
+
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs, { winningIndex: single.index })
|
|
285
286
|
const totalTokens = single.usage?.totalTokens || 0
|
|
286
287
|
const totalCostUsd = Number((single.costUsd || 0).toFixed(5))
|
|
287
288
|
const durationMs = Date.now() - startTime
|
|
288
289
|
|
|
289
290
|
try {
|
|
290
291
|
await recordMoaRunAsync({
|
|
292
|
+
cwd,
|
|
291
293
|
prompt: userPrompt,
|
|
292
294
|
preset: preset?.name || 'default',
|
|
293
295
|
isRefinement,
|
|
@@ -402,6 +404,8 @@ export async function runMoAPipeline({
|
|
|
402
404
|
userPrompt,
|
|
403
405
|
callLlm,
|
|
404
406
|
strategy: preset.judge_voting_strategy || 'majority',
|
|
407
|
+
prices: prices || {},
|
|
408
|
+
blind: Boolean(preset.blind_evaluation),
|
|
405
409
|
signal,
|
|
406
410
|
onProgress,
|
|
407
411
|
})
|
|
@@ -409,7 +413,7 @@ export async function runMoAPipeline({
|
|
|
409
413
|
|
|
410
414
|
// 7d. Feature 3: Composite Block Merge Directive
|
|
411
415
|
const compositeMergeDirective = (preset?.composite_merge_enabled && successfulRefs.length > 1)
|
|
412
|
-
? buildCompositeBlockDirectives(successfulRefs)
|
|
416
|
+
? buildCompositeBlockDirectives(successfulRefs, { blind: Boolean(preset.blind_evaluation) })
|
|
413
417
|
: null
|
|
414
418
|
|
|
415
419
|
// 8. Synthesis phase via primary judge or fallback chain
|
|
@@ -510,9 +514,9 @@ export async function runMoAPipeline({
|
|
|
510
514
|
await cleanMoaWorkspaces(cwd, { runId })
|
|
511
515
|
}
|
|
512
516
|
|
|
513
|
-
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs)
|
|
517
|
+
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs, { winningIndex, synthesizedFiles })
|
|
514
518
|
const durationMs = Date.now() - startTime
|
|
515
|
-
const usageSummary = summarizeMoAUsage(referenceOutputs, aggUsage)
|
|
519
|
+
const usageSummary = summarizeMoAUsage(referenceOutputs, aggUsage, multiJudgeResult?.totalUsage)
|
|
516
520
|
const winningRef = referenceOutputs[winningIndex - 1]
|
|
517
521
|
const winnerModel = winningRef?.label || slotLabel(effectiveReferences[0] || referenceModels[0])
|
|
518
522
|
const finalAggLabel = slotLabel(chosenJudge)
|
|
@@ -532,7 +536,7 @@ export async function runMoAPipeline({
|
|
|
532
536
|
winningIndex,
|
|
533
537
|
winningLabel: winnerModel,
|
|
534
538
|
consensus: multiJudgeResult,
|
|
535
|
-
testGate: referenceOutputs.
|
|
539
|
+
testGate: referenceOutputs.filter((r) => r.testResult).map((r) => r.testResult),
|
|
536
540
|
isComposite: Boolean(preset?.composite_merge_enabled),
|
|
537
541
|
})
|
|
538
542
|
}
|
|
@@ -540,6 +544,7 @@ export async function runMoAPipeline({
|
|
|
540
544
|
try {
|
|
541
545
|
await recordMoaRunAsync({
|
|
542
546
|
id: runId,
|
|
547
|
+
cwd,
|
|
543
548
|
prompt: userPrompt,
|
|
544
549
|
preset: preset?.name || 'default',
|
|
545
550
|
isRefinement,
|
package/lib/moa-stream.js
CHANGED
|
@@ -3,7 +3,8 @@ import crypto from 'node:crypto'
|
|
|
3
3
|
* Zero-latency Live Streaming Generator for MoA Turns
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { cleanMoaWorkspaces } from './file-workspace.js'
|
|
6
|
+
import { cleanMoaWorkspaces, promoteCandidateWorkspace } from './file-workspace.js'
|
|
7
|
+
import { isTestEnvironment } from './history.js'
|
|
7
8
|
import { formatMoAResponse } from './moa-parser.js'
|
|
8
9
|
import { runMoAPipeline } from './moa-runner.js'
|
|
9
10
|
|
|
@@ -132,3 +133,58 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
|
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Streams a /moa promote candidate turn directly to chat with zero LLM calls (#177).
|
|
139
|
+
*/
|
|
140
|
+
export async function* streamPromoteTurn({ runId, candidateIndex, cwd, webServerPort, timeMachineEngine, force = isTestEnvironment() }, options = {}) {
|
|
141
|
+
const signal = options?.signal
|
|
142
|
+
if (signal?.aborted) return
|
|
143
|
+
|
|
144
|
+
yield { type: 'block-start', index: 0, blockType: 'text' }
|
|
145
|
+
|
|
146
|
+
if (typeof candidateIndex !== 'number' || isNaN(candidateIndex) || candidateIndex < 1) {
|
|
147
|
+
const errText = '⚠️ **MoA Promote Error**: Valid candidate index required (>= 1).'
|
|
148
|
+
yield { type: 'text-delta', index: 0, text: errText }
|
|
149
|
+
yield { type: 'block-end', index: 0, block: { type: 'text', text: errText } }
|
|
150
|
+
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
yield { type: 'text-delta', index: 0, text: `🔄 *Promoting candidate ${candidateIndex}${runId ? ` for run \`${runId}\`` : ''}...*\n\n` }
|
|
156
|
+
|
|
157
|
+
const promotedFiles = await promoteCandidateWorkspace(cwd, candidateIndex, {
|
|
158
|
+
runId,
|
|
159
|
+
keepMoa: true,
|
|
160
|
+
port: webServerPort,
|
|
161
|
+
timeMachineEngine,
|
|
162
|
+
force: Boolean(force || options?.force),
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
if (!promotedFiles || promotedFiles.length === 0) {
|
|
166
|
+
const noFilesText = `⚠️ **MoA Promote Notice**: No candidate files found for candidate ${candidateIndex}${runId ? ` in run \`${runId}\`` : ''}. Workspace may have expired or run does not exist.`
|
|
167
|
+
yield { type: 'text-delta', index: 0, text: noFilesText }
|
|
168
|
+
yield { type: 'block-end', index: 0, block: { type: 'text', text: noFilesText } }
|
|
169
|
+
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const fileList = promotedFiles.map((f) => `\`${f}\``).join(', ')
|
|
174
|
+
const checkpointId = promotedFiles.checkpoint || null
|
|
175
|
+
let successMsg = `✅ **MoA Candidate ${candidateIndex} promoted successfully!**\n\n`
|
|
176
|
+
successMsg += `> 📦 **Files applied to workspace**: ${fileList}\n`
|
|
177
|
+
if (checkpointId) {
|
|
178
|
+
successMsg += `> 🛡️ **Time Machine Checkpoint**: \`${checkpointId}\`\n`
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
yield { type: 'text-delta', index: 0, text: successMsg }
|
|
182
|
+
yield { type: 'block-end', index: 0, block: { type: 'text', text: successMsg } }
|
|
183
|
+
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
184
|
+
} catch (err) {
|
|
185
|
+
const errText = `⚠️ **MoA Promote Failed**: ${err?.message || String(err)}`
|
|
186
|
+
yield { type: 'text-delta', index: 0, text: errText }
|
|
187
|
+
yield { type: 'block-end', index: 0, block: { type: 'text', text: errText } }
|
|
188
|
+
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
189
|
+
}
|
|
190
|
+
}
|
package/lib/pricing.js
CHANGED
|
@@ -310,12 +310,16 @@ export function estimateTokenCost(slot = {}, usage = {}, customPrices = {}, cach
|
|
|
310
310
|
|
|
311
311
|
|
|
312
312
|
|
|
313
|
-
export function summarizeMoAUsage(referenceOutputs = [], aggUsage = null) {
|
|
313
|
+
export function summarizeMoAUsage(referenceOutputs = [], aggUsage = null, multiJudgeUsage = null) {
|
|
314
314
|
const normAgg = aggUsage || { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
|
|
315
|
-
const
|
|
316
|
-
const
|
|
317
|
-
const
|
|
318
|
-
const
|
|
315
|
+
const normMj = multiJudgeUsage || { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
|
|
316
|
+
const totalInputTokens = (referenceOutputs || []).reduce((acc, r) => acc + (r.usage?.inputTokens || r.usage?.promptTokens || r.usage?.prompt_tokens || 0), 0) + (normAgg.inputTokens || normAgg.promptTokens || normAgg.prompt_tokens || 0) + (normMj.inputTokens || normMj.promptTokens || normMj.prompt_tokens || 0)
|
|
317
|
+
const totalOutputTokens = (referenceOutputs || []).reduce((acc, r) => acc + (r.usage?.outputTokens || r.usage?.completionTokens || r.usage?.completion_tokens || 0), 0) + (normAgg.outputTokens || normAgg.completionTokens || normAgg.completion_tokens || 0) + (normMj.outputTokens || normMj.completionTokens || normMj.completion_tokens || 0)
|
|
318
|
+
const candTokens = (referenceOutputs || []).reduce((acc, r) => acc + (r.usage?.totalTokens ?? ((r.usage?.inputTokens || 0) + (r.usage?.outputTokens || 0))), 0)
|
|
319
|
+
const aggTokens = normAgg.totalTokens ?? ((normAgg.inputTokens || 0) + (normAgg.outputTokens || 0))
|
|
320
|
+
const mjTokens = normMj.totalTokens ?? ((normMj.inputTokens || 0) + (normMj.outputTokens || 0))
|
|
321
|
+
const totalTokens = candTokens + aggTokens + mjTokens || (totalInputTokens + totalOutputTokens)
|
|
322
|
+
const totalCostUsd = Number(((referenceOutputs || []).reduce((acc, r) => acc + (r.costUsd || 0), 0) + (normAgg.costUsd || 0) + (normMj.costUsd || 0)).toFixed(5))
|
|
319
323
|
const candidates = (referenceOutputs || []).map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd }))
|
|
320
|
-
return { totalTokens, totalInputTokens, totalOutputTokens, totalCostUsd, candidates, aggregator: normAgg }
|
|
324
|
+
return { totalTokens, totalInputTokens, totalOutputTokens, totalCostUsd, candidates, aggregator: normAgg, multiJudge: normMj }
|
|
321
325
|
}
|