@goodandready/dsh-moa 0.2.14 → 0.2.15
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/docs/design/DESIGN.md +6 -0
- package/docs/plans/63-stability-polish-plan.md +15 -0
- package/lib/file-workspace.js +14 -2
- package/lib/history.js +10 -0
- package/lib/live-canvas.js +22 -0
- package/lib/moa-prompts.js +7 -4
- package/lib/moa-runner.js +50 -55
- package/lib/pricing.js +10 -1
- package/package.json +1 -1
package/docs/design/DESIGN.md
CHANGED
|
@@ -71,3 +71,9 @@
|
|
|
71
71
|
- 2026-09-13 — Расширение функционала MoA (Gitea Issue #59): интерактивный селектор альтернативного кандидата в чате (User Candidate Override: allow_candidate_override), пакет из 10 специализированных пресетов с ролями кандидатов (role_persona), локальный гейт синтаксической проверки (Syntax Pre-check Gate) с директивой автоисправления синтаксиса судьёй, режим взаимного рецензирования («Консилиум» / Peer Critique Round 2: peer_critique_enabled), фильтрация лидерборда по пресетам и выгрузка телеметрии в CSV/JSON.
|
|
72
72
|
|
|
73
73
|
- 2026-09-13 — Приведение локализации плагина в строгое соответствие со стандартами dhs-plugin-release-workflow и dsh-plugin-authoring (Gitea Issue #61): добавление полного китайского словаря (zh) в lib/client.js наряду с каноническим английским (en), удаление захардкоженных русских строк в серверной части (lib/moa-parser.js, lib/moa-runner.js), интернационализация эвристик в lib/file-workspace.js и lib/moa-prompts.js (en + zh), создание issue в goodandready/dsh-russian-lang для русификации.
|
|
74
|
+
|
|
75
|
+
## 2026-09-15: Pipeline Stability & Quality Polish (#63)
|
|
76
|
+
- **Refinement Context**: `formatProjectContext(files)` serializes workspace files as fenced Markdown code blocks. `moa-runner.js` passes collected files into `isRefinementTask(prompt, files)`.
|
|
77
|
+
- **Round 2 Persistence**: Candidate file refinements from Round 2 are persisted to `.moa/candidate-N/` via `writeCandidateWorkspace`.
|
|
78
|
+
- **AbortSignal Lifecycle**: `signal` is propagated end-to-end to terminate parallel LLM calls immediately when user aborts or disconnects.
|
|
79
|
+
- **Token Protection**: Peer critique prompts summarize oversized competitor code blocks (>3000 chars) to prevent context window exhaustion.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Implementation Plan - Issue #63: Pipeline Stability and Quality Polish
|
|
2
|
+
|
|
3
|
+
## 1. Context & Objectives
|
|
4
|
+
Polish runtime stability, error recovery, token efficiency, and file generation integrity across `@goodandready/dsh-moa`:
|
|
5
|
+
1. **Refinement Context & Heuristic**: Fix `isRefinementTask` parameter handling in `moa-runner.js` and format collected files into Markdown code fences via `formatProjectContext(files)`, preventing `[object Object]` prompt injection.
|
|
6
|
+
2. **Round 2 Workspace Sync**: Save refined files to `.moa/candidate-N/` in Consilium Round 2 so promoted winner files contain peer-critiqued code rather than stale Round 1 drafts.
|
|
7
|
+
3. **End-to-End AbortSignal Propagation**: Propagate `signal` from `streamMoATurn` through `runMoAPipeline`, `runReferencesParallel`, Round 2 calls, and judge synthesis, cancelling in-flight requests and preventing token waste.
|
|
8
|
+
4. **Context Window Protection**: Apply `stripOrSummarizeCode` in Round 2 peer review and judge synthesis prompts when candidate outputs exceed threshold.
|
|
9
|
+
5. **Architectural Standard**: Maintain file size of `lib/moa-runner.js` strictly under 800 lines.
|
|
10
|
+
|
|
11
|
+
## 2. Modules Impacted
|
|
12
|
+
- `lib/file-workspace.js`: Add `formatProjectContext`, enhance `isRefinementTask`.
|
|
13
|
+
- `lib/moa-prompts.js`: Context protection in `buildPeerCritiquePrompt` & `buildSynthesisPrompt`.
|
|
14
|
+
- `lib/moa-runner.js`: `signal` propagation, Round 2 workspace persistence, refinement context formatting.
|
|
15
|
+
- `test/`: Add regression tests for all fixes.
|
package/lib/file-workspace.js
CHANGED
|
@@ -90,6 +90,18 @@ function sanitizePath(filePath) {
|
|
|
90
90
|
* Collects readable text files from current workspace to feed as context for candidate models (#6).
|
|
91
91
|
* Ignores node_modules, .git, .moa, binary files, large bundles.
|
|
92
92
|
*/
|
|
93
|
+
/**
|
|
94
|
+
* Formats collected project files into structured Markdown code blocks
|
|
95
|
+
* suitable for inclusion in candidate system prompts.
|
|
96
|
+
*/
|
|
97
|
+
export function formatProjectContext(projectFiles = []) {
|
|
98
|
+
if (!Array.isArray(projectFiles) || projectFiles.length === 0) return ''
|
|
99
|
+
const fence = '```'
|
|
100
|
+
return projectFiles
|
|
101
|
+
.map((f) => `### File: ${f.relativePath}\n${fence}\n${f.content}\n${fence}`)
|
|
102
|
+
.join('\n\n')
|
|
103
|
+
}
|
|
104
|
+
|
|
93
105
|
export async function collectProjectContext(baseDir, maxCharBudget = 16000) {
|
|
94
106
|
if (!baseDir) return { files: [], totalChars: 0 }
|
|
95
107
|
|
|
@@ -145,8 +157,8 @@ export async function collectProjectContext(baseDir, maxCharBudget = 16000) {
|
|
|
145
157
|
* Checks if a task prompt is an iterative refinement / modification
|
|
146
158
|
* of an existing project rather than a greenfield project creation (#4).
|
|
147
159
|
*/
|
|
148
|
-
export function isRefinementTask(userPrompt = '', projectFiles =
|
|
149
|
-
if (
|
|
160
|
+
export function isRefinementTask(userPrompt = '', projectFiles = null) {
|
|
161
|
+
if (Array.isArray(projectFiles) && projectFiles.length === 0) return false
|
|
150
162
|
const p = userPrompt.toLowerCase().trim()
|
|
151
163
|
if (!p) return false
|
|
152
164
|
|
package/lib/history.js
CHANGED
|
@@ -298,3 +298,13 @@ export function exportMoaHistory(filePath = DEFAULT_HISTORY_FILE, options = {})
|
|
|
298
298
|
return options?.format === 'csv' ? '' : '[]'
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
|
+
|
|
302
|
+
export function candidatesForHistory(referenceOutputs) {
|
|
303
|
+
return (referenceOutputs || []).map((r) => ({
|
|
304
|
+
provider: r.slot?.provider || "",
|
|
305
|
+
model: r.slot?.model || "",
|
|
306
|
+
files: (r.files || []).map((f) => f.relativePath),
|
|
307
|
+
usage: r.usage || { inputTokens: 0, outputTokens: 0 },
|
|
308
|
+
costUsd: r.costUsd || 0,
|
|
309
|
+
}))
|
|
310
|
+
}
|
package/lib/live-canvas.js
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
|
|
3
|
+
export async function createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs) {
|
|
4
|
+
if (!liveCanvas || typeof liveCanvas.createPreviewFromContent !== "function") return null
|
|
5
|
+
const htmlRel = (promotedFiles || []).find((f) => f.endsWith(".html") || f.endsWith(".htm"))
|
|
6
|
+
if (!htmlRel) return null
|
|
7
|
+
let content = null
|
|
8
|
+
for (const r of referenceOutputs || []) {
|
|
9
|
+
const block = (r?.files || []).find((f) => f.relativePath === htmlRel)
|
|
10
|
+
if (block?.content) {
|
|
11
|
+
content = block.content
|
|
12
|
+
break
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (!content) return null
|
|
16
|
+
try {
|
|
17
|
+
return await liveCanvas.createPreviewFromContent({ content, title: htmlRel, filePath: path.join(cwd, htmlRel) })
|
|
18
|
+
} catch {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
1
23
|
/**
|
|
2
24
|
* Optional Live Canvas preview client (@goodandready/dsh-live-canvas).
|
|
3
25
|
*
|
package/lib/moa-prompts.js
CHANGED
|
@@ -191,8 +191,11 @@ export function buildPeerCritiquePrompt(userPrompt, myProposal, otherProposals =
|
|
|
191
191
|
const othersText = otherProposals
|
|
192
192
|
.map((p, idx) => {
|
|
193
193
|
const label = isBlind ? `Candidate ${idx + 1}` : (p.label || `Candidate ${idx + 1}`)
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
let text = p.text || ''
|
|
195
|
+
if (text.length > 3000) {
|
|
196
|
+
text = stripOrSummarizeCode(text)
|
|
197
|
+
}
|
|
198
|
+
return `### ${label} Alternative Proposal:\n${text}`
|
|
196
199
|
})
|
|
197
200
|
.join('\n\n---\n\n')
|
|
198
201
|
|
|
@@ -224,7 +227,7 @@ export function buildCuratorSynthesisPrompt(userPrompt, referenceOutputs = [], j
|
|
|
224
227
|
? ` [Files created: ${r.files.map((f) => f.relativePath).join(', ')}]`
|
|
225
228
|
: ''
|
|
226
229
|
let textContent = r.text
|
|
227
|
-
if (
|
|
230
|
+
if (textContent.length > 3000) {
|
|
228
231
|
textContent = stripOrSummarizeCode(textContent)
|
|
229
232
|
}
|
|
230
233
|
const syntaxNote = r.syntaxWarning ? ` [⚠️ Syntax Warning: ${r.syntaxWarning}]` : ''
|
|
@@ -291,7 +294,7 @@ export function buildSynthesisPrompt(userPrompt, referenceOutputs = [], judgeCri
|
|
|
291
294
|
? ` [Files created: ${r.files.map((f) => f.relativePath).join(', ')}]`
|
|
292
295
|
: ''
|
|
293
296
|
let textContent = r.text
|
|
294
|
-
if (
|
|
297
|
+
if (textContent.length > 3000) {
|
|
295
298
|
textContent = stripOrSummarizeCode(textContent)
|
|
296
299
|
}
|
|
297
300
|
const syntaxNote = r.syntaxWarning ? ` [⚠️ Syntax Warning: ${r.syntaxWarning}]` : ''
|
package/lib/moa-runner.js
CHANGED
|
@@ -1,27 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DeepSeek Harness Mixture of Agents (MoA) — Runner Engine
|
|
3
|
-
*
|
|
4
|
-
* Implements the full ensemble pipeline:
|
|
5
|
-
* - Parallel fan-out to reference models with transient retry & quorum straggler mitigation
|
|
6
|
-
* - Prompt caching aligned message structures
|
|
7
|
-
* - Curator synthesis & antipatterns evaluation
|
|
8
|
-
* - Aggregator fallback chain for resilience & malformed output recovery
|
|
9
|
-
* - Live token streaming for aggregator
|
|
10
|
-
* - File promotion & Live Canvas sandbox preview
|
|
3
|
+
* Parallel fan-out, Consilium Round 2 peer critique, aggregator synthesis & streaming.
|
|
11
4
|
*/
|
|
12
5
|
|
|
13
6
|
import path from 'node:path'
|
|
14
7
|
import crypto from 'node:crypto'
|
|
15
|
-
import { extractFileBlocks, collectProjectContext, isRefinementTask, writeCandidateWorkspace, promoteCandidateWorkspace, cleanMoaWorkspaces, verifyFileSyntax } from './file-workspace.js'
|
|
16
|
-
import { estimateTokenCost } from './pricing.js'
|
|
17
|
-
import { recordMoaRun, recordMoaRunAsync } from './history.js'
|
|
8
|
+
import { extractFileBlocks, collectProjectContext, formatProjectContext, isRefinementTask, writeCandidateWorkspace, promoteCandidateWorkspace, cleanMoaWorkspaces, verifyFileSyntax } from './file-workspace.js'
|
|
9
|
+
import { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
|
|
10
|
+
import { recordMoaRun, recordMoaRunAsync, candidatesForHistory } from './history.js'
|
|
11
|
+
import { createPromotedPreview } from './live-canvas.js'
|
|
18
12
|
import { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildCuratorSynthesisPrompt, buildSynthesisPrompt, buildPeerCritiquePrompt, ROLE_PERSONA_PROMPTS, SYSTEM_ROLE_PROPOSER, ANTIPATTERNS_RUBRIC } from './moa-prompts.js'
|
|
19
13
|
import { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
|
|
20
14
|
|
|
21
15
|
// Re-exports for consumers & backward compatibility
|
|
22
16
|
export { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildCuratorSynthesisPrompt, buildSynthesisPrompt, ANTIPATTERNS_RUBRIC } from './moa-prompts.js'
|
|
23
17
|
export { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
|
|
24
|
-
export { estimateTokenCost } from './pricing.js'
|
|
18
|
+
export { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
|
|
25
19
|
|
|
26
20
|
export const DEFAULT_PROMPT = 'Please propose an optimal, well-structured, production-ready solution with full code and explanations.'
|
|
27
21
|
export const REFERENCE_SYSTEM_PROMPT = SYSTEM_ROLE_PROPOSER
|
|
@@ -32,14 +26,16 @@ export const REFERENCE_SYSTEM_PROMPT = SYSTEM_ROLE_PROPOSER
|
|
|
32
26
|
export async function callWithTransientRetry(callLlmFn, callArgs, maxRetries = 0, retryDelayMs = 1200) {
|
|
33
27
|
let attempt = 0
|
|
34
28
|
while (true) {
|
|
29
|
+
if (callArgs?.signal?.aborted) throw new Error('Aborted')
|
|
35
30
|
try {
|
|
36
31
|
return await callLlmFn(callArgs)
|
|
37
32
|
} catch (err) {
|
|
38
33
|
attempt++
|
|
39
34
|
const msg = err?.message || String(err)
|
|
40
35
|
const isTransient = /429|rate limit|502|503|504|econnreset|etimedout|socket hang up/i.test(msg)
|
|
41
|
-
if (attempt <= maxRetries && isTransient) {
|
|
36
|
+
if (attempt <= maxRetries && isTransient && !callArgs?.signal?.aborted) {
|
|
42
37
|
await new Promise((r) => setTimeout(r, retryDelayMs))
|
|
38
|
+
if (callArgs?.signal?.aborted) throw new Error('Aborted')
|
|
43
39
|
continue
|
|
44
40
|
}
|
|
45
41
|
throw err
|
|
@@ -66,6 +62,28 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
66
62
|
let finishedCount = 0
|
|
67
63
|
const results = new Array(total)
|
|
68
64
|
const abortControllers = references.map(() => new AbortController())
|
|
65
|
+
if (options.signal) {
|
|
66
|
+
if (options.signal.aborted) {
|
|
67
|
+
return references.map((r, i) => ({
|
|
68
|
+
index: i + 1,
|
|
69
|
+
provider: r.provider,
|
|
70
|
+
model: r.model,
|
|
71
|
+
label: slotLabel(r),
|
|
72
|
+
ok: false,
|
|
73
|
+
text: '(aborted)',
|
|
74
|
+
error: 'Turn aborted',
|
|
75
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
76
|
+
costUsd: 0,
|
|
77
|
+
}))
|
|
78
|
+
}
|
|
79
|
+
if (typeof options.signal.addEventListener === 'function') {
|
|
80
|
+
options.signal.addEventListener('abort', () => {
|
|
81
|
+
for (const ac of abortControllers) {
|
|
82
|
+
try { ac.abort(new Error('Turn aborted')) } catch {}
|
|
83
|
+
}
|
|
84
|
+
}, { once: true })
|
|
85
|
+
}
|
|
86
|
+
}
|
|
69
87
|
|
|
70
88
|
let onTaskFinished = null
|
|
71
89
|
const notifyFinished = () => {
|
|
@@ -221,35 +239,7 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
221
239
|
return results
|
|
222
240
|
}
|
|
223
241
|
|
|
224
|
-
function candidatesForHistory(referenceOutputs) {
|
|
225
|
-
return (referenceOutputs || []).map((r) => ({
|
|
226
|
-
provider: r.slot?.provider || '',
|
|
227
|
-
model: r.slot?.model || '',
|
|
228
|
-
files: (r.files || []).map((f) => f.relativePath),
|
|
229
|
-
usage: r.usage || { inputTokens: 0, outputTokens: 0 },
|
|
230
|
-
costUsd: r.costUsd || 0,
|
|
231
|
-
}))
|
|
232
|
-
}
|
|
233
242
|
|
|
234
|
-
async function createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs) {
|
|
235
|
-
if (!liveCanvas || typeof liveCanvas.createPreviewFromContent !== 'function') return null
|
|
236
|
-
const htmlRel = (promotedFiles || []).find((f) => f.endsWith('.html') || f.endsWith('.htm'))
|
|
237
|
-
if (!htmlRel) return null
|
|
238
|
-
let content = null
|
|
239
|
-
for (const r of referenceOutputs || []) {
|
|
240
|
-
const block = (r?.files || []).find((f) => f.relativePath === htmlRel)
|
|
241
|
-
if (block?.content) {
|
|
242
|
-
content = block.content
|
|
243
|
-
break
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
if (!content) return null
|
|
247
|
-
try {
|
|
248
|
-
return await liveCanvas.createPreviewFromContent({ content, title: htmlRel, filePath: path.join(cwd, htmlRel) })
|
|
249
|
-
} catch {
|
|
250
|
-
return null
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
243
|
|
|
254
244
|
/**
|
|
255
245
|
* Executes the full Mixture of Agents pipeline.
|
|
@@ -265,7 +255,9 @@ export async function runMoAPipeline({
|
|
|
265
255
|
historyFilePath = null,
|
|
266
256
|
liveCanvas = null,
|
|
267
257
|
onStreamDelta = null,
|
|
258
|
+
signal = null,
|
|
268
259
|
}) {
|
|
260
|
+
if (signal?.aborted) return { error: new Error('Turn aborted') }
|
|
269
261
|
const startTime = Date.now()
|
|
270
262
|
|
|
271
263
|
// 1. Resolve configurations
|
|
@@ -298,13 +290,14 @@ export async function runMoAPipeline({
|
|
|
298
290
|
const runId = crypto.randomUUID()
|
|
299
291
|
|
|
300
292
|
// 2. Collect project context for refinement tasks
|
|
301
|
-
const
|
|
293
|
+
const collectedCtx = await collectProjectContext(cwd, 16000)
|
|
294
|
+
const isRefinement = Boolean(collectedCtx?.files?.length > 0 && isRefinementTask(userPrompt, collectedCtx.files))
|
|
302
295
|
let projectContext = ''
|
|
303
296
|
if (isRefinement) {
|
|
304
297
|
if (typeof onProgress === 'function') {
|
|
305
298
|
onProgress('🔍 *Reading project files for refinement context...*\n')
|
|
306
299
|
}
|
|
307
|
-
projectContext =
|
|
300
|
+
projectContext = formatProjectContext(collectedCtx.files)
|
|
308
301
|
}
|
|
309
302
|
|
|
310
303
|
// 3. Build prompts & evaluate broad questionnaire needs
|
|
@@ -330,11 +323,17 @@ export async function runMoAPipeline({
|
|
|
330
323
|
quorumEnabled: isQuorumEnabled,
|
|
331
324
|
gracePeriodSec,
|
|
332
325
|
maxRetries: candidateRetries,
|
|
326
|
+
signal,
|
|
333
327
|
},
|
|
334
328
|
callLlm,
|
|
335
329
|
onProgress
|
|
336
330
|
)
|
|
337
331
|
|
|
332
|
+
if (signal?.aborted) {
|
|
333
|
+
await cleanMoaWorkspaces(cwd)
|
|
334
|
+
return { error: new Error('Turn aborted') }
|
|
335
|
+
}
|
|
336
|
+
|
|
338
337
|
// Fail fast if all candidates failed
|
|
339
338
|
const successfulRefs = referenceOutputs.filter((r) => r.ok)
|
|
340
339
|
if (successfulRefs.length === 0) {
|
|
@@ -518,6 +517,7 @@ export async function runMoAPipeline({
|
|
|
518
517
|
temperature: refTemp,
|
|
519
518
|
maxTokens,
|
|
520
519
|
timeoutMs: refTimeoutSec * 1000,
|
|
520
|
+
signal,
|
|
521
521
|
}, candidateRetries)
|
|
522
522
|
const refinedText = typeof res === 'string' ? res : (res?.content || res?.text || cand.text)
|
|
523
523
|
cand.text = refinedText
|
|
@@ -525,6 +525,7 @@ export async function runMoAPipeline({
|
|
|
525
525
|
if (r2Files.length > 0) {
|
|
526
526
|
cand.files = r2Files
|
|
527
527
|
cand.syntaxWarning = (verifyFileSyntax(r2Files) || []).map((w) => `${w.file}: ${w.error}`).join('; ')
|
|
528
|
+
await writeCandidateWorkspace(cwd, cand.index, r2Files)
|
|
528
529
|
}
|
|
529
530
|
if (typeof res === 'object' && res?.usage) {
|
|
530
531
|
cand.usage.inputTokens = (cand.usage.inputTokens || 0) + (res.usage.inputTokens || 0)
|
|
@@ -567,6 +568,7 @@ export async function runMoAPipeline({
|
|
|
567
568
|
temperature: aggTemp,
|
|
568
569
|
maxTokens,
|
|
569
570
|
timeoutMs: aggTimeoutSec * 1000,
|
|
571
|
+
signal,
|
|
570
572
|
onStreamDelta: (delta) => {
|
|
571
573
|
if (isStreamAggregator && typeof onStreamDelta === 'function') {
|
|
572
574
|
onStreamDelta(delta)
|
|
@@ -626,15 +628,12 @@ export async function runMoAPipeline({
|
|
|
626
628
|
|
|
627
629
|
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs)
|
|
628
630
|
|
|
629
|
-
const totalTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0) + aggUsage.totalTokens
|
|
630
|
-
const totalCostUsd = Number((referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + aggUsage.costUsd).toFixed(5))
|
|
631
631
|
const durationMs = Date.now() - startTime
|
|
632
|
-
|
|
632
|
+
const usageSummary = summarizeMoAUsage(referenceOutputs, aggUsage)
|
|
633
633
|
const winningRef = referenceOutputs[winningIndex - 1]
|
|
634
634
|
const winnerModel = winningRef?.label || slotLabel(referenceModels[0])
|
|
635
635
|
const finalAggLabel = slotLabel(chosenJudge)
|
|
636
636
|
|
|
637
|
-
// Record run to history
|
|
638
637
|
try {
|
|
639
638
|
recordMoaRun({
|
|
640
639
|
id: runId,
|
|
@@ -646,8 +645,8 @@ export async function runMoAPipeline({
|
|
|
646
645
|
winnerIndex: winningIndex,
|
|
647
646
|
winnerModel,
|
|
648
647
|
promotedFiles,
|
|
649
|
-
totalTokens,
|
|
650
|
-
totalCostUsd,
|
|
648
|
+
totalTokens: usageSummary.totalTokens,
|
|
649
|
+
totalCostUsd: usageSummary.totalCostUsd,
|
|
651
650
|
durationMs,
|
|
652
651
|
}, historyFilePath || undefined)
|
|
653
652
|
} catch (histErr) {
|
|
@@ -670,12 +669,7 @@ export async function runMoAPipeline({
|
|
|
670
669
|
runId,
|
|
671
670
|
allowCandidateOverride,
|
|
672
671
|
...(livePreview ? { liveCanvas: livePreview } : {}),
|
|
673
|
-
usage:
|
|
674
|
-
totalTokens,
|
|
675
|
-
totalCostUsd,
|
|
676
|
-
candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
|
|
677
|
-
aggregator: aggUsage,
|
|
678
|
-
},
|
|
672
|
+
usage: usageSummary,
|
|
679
673
|
durationMs,
|
|
680
674
|
}
|
|
681
675
|
}
|
|
@@ -716,6 +710,7 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
|
|
|
716
710
|
prices,
|
|
717
711
|
historyFilePath,
|
|
718
712
|
liveCanvas,
|
|
713
|
+
signal,
|
|
719
714
|
})
|
|
720
715
|
.catch((err) => ({ error: err }))
|
|
721
716
|
.finally(() => {
|
package/lib/pricing.js
CHANGED
|
@@ -205,4 +205,13 @@ export function estimateTokenCost(slot = {}, usage = {}, customPrices = {}, cach
|
|
|
205
205
|
rates,
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
|
-
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
export function summarizeMoAUsage(referenceOutputs = [], aggUsage = null) {
|
|
212
|
+
const normAgg = aggUsage || { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
|
|
213
|
+
const totalTokens = (referenceOutputs || []).reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0) + normAgg.totalTokens
|
|
214
|
+
const totalCostUsd = Number(((referenceOutputs || []).reduce((acc, r) => acc + (r.costUsd || 0), 0) + normAgg.costUsd).toFixed(5))
|
|
215
|
+
const candidates = (referenceOutputs || []).map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd }))
|
|
216
|
+
return { totalTokens, totalCostUsd, candidates, aggregator: normAgg }
|
|
217
|
+
}
|