@goodandready/dsh-moa 0.2.3 → 0.2.5
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 +21 -0
- package/docs/README.ru.md +21 -0
- package/lib/client.js +226 -124
- package/lib/file-workspace.js +94 -5
- package/lib/history.js +191 -0
- package/lib/index.js +221 -188
- package/lib/moa-runner.js +216 -87
- package/lib/pricing.js +194 -0
- package/package.json +2 -2
package/lib/moa-runner.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { estimateTokenCost as calculateTokenCost, resolveModelRates, refreshCatalogInBackground, DIRECT_VENDOR_RATES, FALLBACK_RATES } from './pricing.js'
|
|
1
2
|
/**
|
|
2
|
-
* Mixture of Agents (MoA) execution engine with Interactive Questioning
|
|
3
|
-
*
|
|
3
|
+
* Mixture of Agents (MoA) execution engine with Interactive Questioning,
|
|
4
|
+
* Isolated Multi-Candidate File Execution, Native Cost Tracking,
|
|
5
|
+
* Refinement Mode, and Multi-Candidate Scalability.
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
import {
|
|
@@ -8,13 +10,19 @@ import {
|
|
|
8
10
|
writeCandidateWorkspace,
|
|
9
11
|
promoteCandidateWorkspace,
|
|
10
12
|
cleanMoaWorkspaces,
|
|
13
|
+
collectProjectContext,
|
|
14
|
+
isRefinementTask,
|
|
11
15
|
} from './file-workspace.js'
|
|
12
16
|
|
|
17
|
+
import { recordMoaRun } from './history.js'
|
|
18
|
+
|
|
13
19
|
export {
|
|
14
20
|
extractFileBlocks,
|
|
15
21
|
writeCandidateWorkspace,
|
|
16
22
|
promoteCandidateWorkspace,
|
|
17
23
|
cleanMoaWorkspaces,
|
|
24
|
+
collectProjectContext,
|
|
25
|
+
isRefinementTask,
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
export const REFERENCE_SYSTEM_PROMPT = `You are an expert candidate engineer model in a Mixture of Agents (MoA) architecture.
|
|
@@ -34,12 +42,30 @@ RULES:
|
|
|
34
42
|
4. If the user request is broad, make opinionated, high-quality technical decisions and deliver a complete working project.
|
|
35
43
|
5. Never leave placeholders like "// TODO" or "... rest of code". Deliver exhaustive, working code.`
|
|
36
44
|
|
|
45
|
+
export const REFINEMENT_SYSTEM_PROMPT = `You are an expert software engineer performing an iterative modification / refinement on an existing project.
|
|
46
|
+
You are provided with the current codebase files and the user's delta request.
|
|
47
|
+
|
|
48
|
+
RULES:
|
|
49
|
+
1. Modify the existing files or create new files to fulfill the user's change request.
|
|
50
|
+
2. Deliver complete, production-ready updated code for every modified file.
|
|
51
|
+
3. Format each updated file in markdown code blocks with explicit file attribute:
|
|
52
|
+
\`\`\`javascript file="src/app.js"
|
|
53
|
+
...
|
|
54
|
+
\`\`\`
|
|
55
|
+
4. Keep the existing architecture, styling conventions and dependencies consistent.`
|
|
56
|
+
|
|
37
57
|
export const ADVISOR_QUESTION_PROMPT = `You are a senior technical advisor in a Mixture of Agents (MoA) architecture.
|
|
38
58
|
The user has provided a prompt that may have multiple design choices, architectural paths, or underspecified requirements.
|
|
39
59
|
|
|
40
60
|
Review the user prompt and identify 1 to 3 critical, high-impact clarifying questions or architectural options that would define the implementation (e.g. framework/vanilla, features, design style, target environment).
|
|
41
61
|
Keep questions very clear, structured, and actionable. Avoid trivial questions. Respond directly in Russian.`
|
|
42
62
|
|
|
63
|
+
export { resolveModelRates, refreshCatalogInBackground, DIRECT_VENDOR_RATES, FALLBACK_RATES }
|
|
64
|
+
|
|
65
|
+
export function estimateTokenCost(slot, usage = {}, customPrices = {}) {
|
|
66
|
+
return calculateTokenCost(slot, usage, customPrices)
|
|
67
|
+
}
|
|
68
|
+
|
|
43
69
|
export function slotLabel(slot) {
|
|
44
70
|
if (!slot || typeof slot !== 'object') return 'unknown'
|
|
45
71
|
const prov = String(slot.provider || '').trim()
|
|
@@ -79,44 +105,38 @@ export function cleanAdvisoryMessages(messages = [], maxCharBudget = 4000) {
|
|
|
79
105
|
|
|
80
106
|
trimmed.push({ role, content: text })
|
|
81
107
|
}
|
|
82
|
-
|
|
83
108
|
return trimmed
|
|
84
109
|
}
|
|
85
110
|
|
|
86
|
-
/**
|
|
87
|
-
* Checks if the prompt should trigger a clarifying questions phase
|
|
88
|
-
* instead of immediate execution.
|
|
89
|
-
*/
|
|
90
111
|
export function isBroadPromptRequiringQuestions(userPrompt = '', messages = []) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
lastAssistantText.includes('Уточнение требований') ||
|
|
105
|
-
lastAssistantText.includes('Уточните, пожалуйста') ||
|
|
106
|
-
lastAssistantText.includes('опросник')
|
|
107
|
-
|
|
108
|
-
if (isAnsweringQuestions) {
|
|
112
|
+
if (!userPrompt || typeof userPrompt !== 'string') return false
|
|
113
|
+
const p = userPrompt.trim()
|
|
114
|
+
const wordCount = p.split(/\s+/).length
|
|
115
|
+
|
|
116
|
+
if (/^(да|нет|1|2|3|4|ок|погнали|давай|yes|no)\b/i.test(p) && wordCount <= 5) {
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const hasRecentQuestion = messages.some((m) => {
|
|
121
|
+
const text = typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '')
|
|
122
|
+
return text.includes('Уточнение требований') || text.includes('опросник') || text.includes('Вариант 1')
|
|
123
|
+
})
|
|
124
|
+
if (hasRecentQuestion) {
|
|
109
125
|
return false
|
|
110
126
|
}
|
|
111
127
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
128
|
+
const creationTriggers = [
|
|
129
|
+
'сделай', 'создай', 'напиши', 'разработай', 'придумай', 'реализуй',
|
|
130
|
+
'make', 'build', 'create', 'generate', 'develop',
|
|
131
|
+
]
|
|
132
|
+
const startsWithCreation = creationTriggers.some((t) => p.toLowerCase().startsWith(t))
|
|
133
|
+
|
|
134
|
+
if (startsWithCreation && wordCount <= 18) {
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
116
137
|
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
const wordCount = prompt.split(/\s+/).filter(Boolean).length
|
|
138
|
+
const vagueNouns = ['приложение', 'игру', 'сервис', 'сайт', 'лендинг', 'калькулятор', 'виджет', 'дашборд', 'app', 'game', 'tool', 'website']
|
|
139
|
+
if (vagueNouns.some((n) => p.toLowerCase().includes(n))) {
|
|
120
140
|
if (wordCount <= 12) return true
|
|
121
141
|
}
|
|
122
142
|
|
|
@@ -140,21 +160,30 @@ ${joined}
|
|
|
140
160
|
В конце добавь примечание, что пользователь может ответить кратко (например: "1, 2, темная тема") или довериться выбору по умолчанию.`
|
|
141
161
|
}
|
|
142
162
|
|
|
143
|
-
export function buildSynthesisPrompt(userPrompt, referenceOutputs = []) {
|
|
163
|
+
export function buildSynthesisPrompt(userPrompt, referenceOutputs = [], judgeCriteria = '') {
|
|
144
164
|
const joined = referenceOutputs
|
|
145
165
|
.map((r, i) => {
|
|
146
166
|
const fileSummary = (r.files && r.files.length > 0)
|
|
147
167
|
? ` [Созданные файлы: ${r.files.map((f) => f.relativePath).join(', ')}]`
|
|
148
168
|
: ''
|
|
149
|
-
|
|
169
|
+
// If 3+ candidates, summarize text to prevent judge context overflow (#13)
|
|
170
|
+
let textContent = r.text
|
|
171
|
+
if (referenceOutputs.length >= 3 && textContent.length > 3000) {
|
|
172
|
+
textContent = stripOrSummarizeCode(textContent)
|
|
173
|
+
}
|
|
174
|
+
return `Reference ${i + 1} — ${r.label}:${fileSummary}\n${textContent}`
|
|
150
175
|
})
|
|
151
176
|
.join('\n\n')
|
|
152
177
|
|
|
178
|
+
const criteriaBlock = judgeCriteria && judgeCriteria.trim()
|
|
179
|
+
? `\n### 🎯 Дополнительные критерии оценки от пользователя:\n${judgeCriteria.trim()}\n`
|
|
180
|
+
: ''
|
|
181
|
+
|
|
153
182
|
return `You are the expert aggregator/judge in a Mixture of Agents (MoA) process. You evaluate solutions from multiple candidate models, judge which one is best (or how to combine their best parts), and deliver the final authoritative verdict and solution.
|
|
154
183
|
|
|
155
184
|
Original user prompt:
|
|
156
185
|
${userPrompt}
|
|
157
|
-
|
|
186
|
+
${criteriaBlock}
|
|
158
187
|
Reference responses from candidate models:
|
|
159
188
|
${joined}
|
|
160
189
|
|
|
@@ -181,9 +210,10 @@ export function parseWinnerIndex(judgeText, defaultIndex = 1) {
|
|
|
181
210
|
const idx = parseInt(match[1], 10)
|
|
182
211
|
if (!isNaN(idx) && idx >= 1) return idx
|
|
183
212
|
}
|
|
184
|
-
|
|
185
|
-
if (
|
|
186
|
-
|
|
213
|
+
const candMatch = /(?:Кандидат|Reference)\s*(\d+)\b/i.exec(judgeText)
|
|
214
|
+
if (candMatch) {
|
|
215
|
+
const idx = parseInt(candMatch[1], 10)
|
|
216
|
+
if (!isNaN(idx) && idx >= 1) return idx
|
|
187
217
|
}
|
|
188
218
|
return defaultIndex
|
|
189
219
|
}
|
|
@@ -263,6 +293,13 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
263
293
|
const text = (typeof res === 'string' ? res : (res?.content || res?.text || '')).trim()
|
|
264
294
|
const files = extractFileBlocks(text)
|
|
265
295
|
|
|
296
|
+
// Estimate tokens & cost
|
|
297
|
+
const rawUsage = res?.usage || {
|
|
298
|
+
inputTokens: Math.round(JSON.stringify(fullMessages).length / 4),
|
|
299
|
+
outputTokens: Math.round(text.length / 4),
|
|
300
|
+
}
|
|
301
|
+
const costInfo = estimateTokenCost(slot, rawUsage, options.prices)
|
|
302
|
+
|
|
266
303
|
if (typeof onProgress === 'function') {
|
|
267
304
|
const fileMsg = files.length > 0 ? ` (создано файлов: ${files.length})` : ''
|
|
268
305
|
onProgress(`✅ *Кандидат ${i + 1} (${label}) завершил ответ${fileMsg}.*\n`)
|
|
@@ -275,6 +312,8 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
275
312
|
model: slot.model,
|
|
276
313
|
text: text || '(empty response)',
|
|
277
314
|
files,
|
|
315
|
+
usage: costInfo,
|
|
316
|
+
costUsd: costInfo.costUsd,
|
|
278
317
|
ok: true,
|
|
279
318
|
}
|
|
280
319
|
} catch (err) {
|
|
@@ -289,6 +328,8 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
289
328
|
model: slot.model,
|
|
290
329
|
text: `[failed: ${err?.message || String(err)}]`,
|
|
291
330
|
files: [],
|
|
331
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
332
|
+
costUsd: 0,
|
|
292
333
|
ok: false,
|
|
293
334
|
}
|
|
294
335
|
}
|
|
@@ -299,10 +340,10 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
299
340
|
|
|
300
341
|
/**
|
|
301
342
|
* Runs the full Mixture of Agents pipeline:
|
|
302
|
-
* 1. Checks if questions
|
|
343
|
+
* 1. Checks if questions or refinement needed
|
|
303
344
|
* 2. Parallel candidate proposers write to .moa/candidate-X/
|
|
304
|
-
* 3. Aggregator judges and picks winner
|
|
305
|
-
* 4. Promotes winner files and
|
|
345
|
+
* 3. Aggregator judges and picks winner (or bypassed in Fast Mode)
|
|
346
|
+
* 4. Promotes winner files, records history and calculates costs
|
|
306
347
|
*/
|
|
307
348
|
export async function runMoAPipeline({
|
|
308
349
|
userPrompt,
|
|
@@ -312,20 +353,37 @@ export async function runMoAPipeline({
|
|
|
312
353
|
cwd,
|
|
313
354
|
onProgress,
|
|
314
355
|
skipQuestions = false,
|
|
356
|
+
prices = {},
|
|
315
357
|
}) {
|
|
316
358
|
if (typeof callLlm !== 'function') {
|
|
317
359
|
throw new Error('callLlm function is required for runMoAPipeline')
|
|
318
360
|
}
|
|
319
361
|
|
|
362
|
+
const startTime = Date.now()
|
|
320
363
|
const referenceModels = preset?.reference_models || preset?.referenceModels || []
|
|
321
364
|
const aggregator = preset?.aggregator || { provider: 'default', model: 'default' }
|
|
322
365
|
const refTemp = preset?.reference_temperature ?? preset?.referenceTemperature ?? 0.6
|
|
323
366
|
const aggTemp = preset?.aggregator_temperature ?? preset?.aggregatorTemperature ?? 0.4
|
|
324
367
|
const maxTokens = preset?.max_tokens ?? preset?.maxTokens ?? 4096
|
|
368
|
+
const judgeCriteria = preset?.judge_criteria || preset?.judgeCriteria || ''
|
|
325
369
|
const workDir = cwd || process.cwd()
|
|
326
370
|
|
|
371
|
+
// Collect project context (#6) and check refinement task (#4)
|
|
372
|
+
const projectCtx = await collectProjectContext(workDir, 16000)
|
|
373
|
+
const isRefinement = isRefinementTask(userPrompt, projectCtx.files)
|
|
374
|
+
|
|
375
|
+
// System prompt selection
|
|
376
|
+
let candidateSystemPrompt = REFERENCE_SYSTEM_PROMPT
|
|
377
|
+
if (isRefinement && projectCtx.files.length > 0) {
|
|
378
|
+
const fileList = projectCtx.files.map((f) => `### File: ${f.relativePath}\n\`\`\`\n${f.content}\n\`\`\``).join('\n\n')
|
|
379
|
+
candidateSystemPrompt = `${REFINEMENT_SYSTEM_PROMPT}\n\n## Existing Project Files:\n${fileList}`
|
|
380
|
+
if (typeof onProgress === 'function') {
|
|
381
|
+
onProgress(`🔄 *[Refinement Mode]: Обнаружен существующий проект (${projectCtx.files.length} файлов). Кандидаты вносят точечные изменения...*\n\n`)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
327
385
|
// Phase 1: Check if clarifying questions should be asked
|
|
328
|
-
const needsQuestions = !skipQuestions && isBroadPromptRequiringQuestions(userPrompt, messages)
|
|
386
|
+
const needsQuestions = !skipQuestions && !isRefinement && isBroadPromptRequiringQuestions(userPrompt, messages)
|
|
329
387
|
if (needsQuestions) {
|
|
330
388
|
if (typeof onProgress === 'function') {
|
|
331
389
|
onProgress('🔍 *Задача общего характера. Советники формируют ключевые развилки...*\n\n')
|
|
@@ -371,15 +429,19 @@ export async function runMoAPipeline({
|
|
|
371
429
|
}
|
|
372
430
|
}
|
|
373
431
|
|
|
432
|
+
// FAST MODE: single candidate model bypasses aggregator (#14)
|
|
433
|
+
const isFastMode = referenceModels.length === 1
|
|
434
|
+
|
|
374
435
|
// Phase 2: Parallel execution & file generation
|
|
375
436
|
if (typeof onProgress === 'function') {
|
|
376
|
-
|
|
437
|
+
const modeLabel = isFastMode ? '⚡ Fast Mode' : `советниками (${referenceModels.length})`
|
|
438
|
+
onProgress(`🚀 *Запускаю реализацию ${modeLabel}...*\n\n`)
|
|
377
439
|
}
|
|
378
440
|
|
|
379
441
|
const referenceOutputs = await runReferencesParallel(
|
|
380
442
|
referenceModels,
|
|
381
443
|
[...messages, { role: 'user', content: userPrompt }],
|
|
382
|
-
{ referenceTemperature: refTemp, maxTokens },
|
|
444
|
+
{ systemPrompt: candidateSystemPrompt, referenceTemperature: refTemp, maxTokens },
|
|
383
445
|
callLlm,
|
|
384
446
|
onProgress,
|
|
385
447
|
)
|
|
@@ -399,35 +461,51 @@ export async function runMoAPipeline({
|
|
|
399
461
|
}
|
|
400
462
|
}
|
|
401
463
|
|
|
402
|
-
|
|
464
|
+
let synthesizedText = ''
|
|
465
|
+
let winningIndex = 1
|
|
466
|
+
let aggUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
|
|
403
467
|
const aggLabel = slotLabel(aggregator)
|
|
404
|
-
if (typeof onProgress === 'function') {
|
|
405
|
-
onProgress(`\n⚖️ *Все кандидаты завершили генерацию. Судья (${aggLabel}) оценивает код и файлы...*\n\n`)
|
|
406
|
-
}
|
|
407
468
|
|
|
408
|
-
|
|
469
|
+
if (isFastMode) {
|
|
470
|
+
// Fast mode: promote candidate 1 directly without judge
|
|
471
|
+
synthesizedText = referenceOutputs[0]?.text || '(empty fast response)'
|
|
472
|
+
winningIndex = 1
|
|
473
|
+
} else {
|
|
474
|
+
// Phase 3: Aggregator evaluation
|
|
475
|
+
if (typeof onProgress === 'function') {
|
|
476
|
+
onProgress(`\n⚖️ *Все кандидаты завершили генерацию. Судья (${aggLabel}) оценивает код и файлы...*\n\n`)
|
|
477
|
+
}
|
|
409
478
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
479
|
+
const synthPrompt = buildSynthesisPrompt(userPrompt, referenceOutputs, judgeCriteria)
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
const aggPromise = callLlm({
|
|
483
|
+
provider: aggregator.provider,
|
|
484
|
+
model: aggregator.model,
|
|
485
|
+
messages: [{ role: 'user', content: synthPrompt }],
|
|
486
|
+
temperature: aggTemp,
|
|
487
|
+
maxTokens,
|
|
488
|
+
})
|
|
489
|
+
const aggTimeout = new Promise((_, reject) =>
|
|
490
|
+
setTimeout(() => reject(new Error(`Timeout after 90s waiting for aggregator ${aggLabel}`)), 90000).unref()
|
|
491
|
+
)
|
|
492
|
+
const res = await Promise.race([aggPromise, aggTimeout])
|
|
493
|
+
synthesizedText = (typeof res === 'string' ? res : (res?.content || res?.text || '')).trim()
|
|
494
|
+
|
|
495
|
+
const rawAggUsage = res?.usage || {
|
|
496
|
+
inputTokens: Math.round(synthPrompt.length / 4),
|
|
497
|
+
outputTokens: Math.round(synthesizedText.length / 4),
|
|
498
|
+
}
|
|
499
|
+
aggUsage = estimateTokenCost(aggregator, rawAggUsage, prices)
|
|
500
|
+
} catch (err) {
|
|
501
|
+
synthesizedText = `[Aggregator error: ${err?.message || String(err)}]\n\nFallback candidate outputs:\n\n` +
|
|
502
|
+
referenceOutputs.map((r, i) => `### ${r.label}\n${r.text}`).join('\n\n')
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
winningIndex = parseWinnerIndex(synthesizedText, 1)
|
|
427
506
|
}
|
|
428
507
|
|
|
429
508
|
// Phase 4: Promote winner files and cleanup
|
|
430
|
-
const winningIndex = parseWinnerIndex(synthesizedText, 1)
|
|
431
509
|
let promotedFiles = []
|
|
432
510
|
try {
|
|
433
511
|
promotedFiles = await promoteCandidateWorkspace(workDir, winningIndex)
|
|
@@ -448,7 +526,8 @@ export async function runMoAPipeline({
|
|
|
448
526
|
const lcRes = await fetch(`http://127.0.0.1:${port}/dsh-live-canvas/api/open-file`, {
|
|
449
527
|
method: 'POST',
|
|
450
528
|
headers: { 'Content-Type': 'application/json' },
|
|
451
|
-
body: JSON.stringify({ filePath: previewCandidate })
|
|
529
|
+
body: JSON.stringify({ filePath: previewCandidate }),
|
|
530
|
+
signal: AbortSignal.timeout(500),
|
|
452
531
|
})
|
|
453
532
|
if (lcRes.ok) {
|
|
454
533
|
const lcData = await lcRes.json()
|
|
@@ -470,21 +549,55 @@ export async function runMoAPipeline({
|
|
|
470
549
|
}
|
|
471
550
|
}
|
|
472
551
|
|
|
552
|
+
// Calculate totals
|
|
553
|
+
const totalTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0) + aggUsage.totalTokens
|
|
554
|
+
const totalCostUsd = Number((referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + aggUsage.costUsd).toFixed(5))
|
|
555
|
+
const durationMs = Date.now() - startTime
|
|
556
|
+
|
|
557
|
+
const winningRef = referenceOutputs[winningIndex - 1]
|
|
558
|
+
const winnerModel = winningRef?.label || slotLabel(referenceModels[0])
|
|
559
|
+
|
|
560
|
+
// Record run to history (#9, #10, #11)
|
|
561
|
+
try {
|
|
562
|
+
recordMoaRun({
|
|
563
|
+
prompt: userPrompt,
|
|
564
|
+
preset: preset?.name || 'default',
|
|
565
|
+
isRefinement,
|
|
566
|
+
candidates: referenceOutputs,
|
|
567
|
+
aggregator: isFastMode ? null : { provider: aggregator.provider, model: aggregator.model, usage: aggUsage, costUsd: aggUsage.costUsd },
|
|
568
|
+
winnerIndex: winningIndex,
|
|
569
|
+
winnerModel,
|
|
570
|
+
promotedFiles,
|
|
571
|
+
totalTokens,
|
|
572
|
+
totalCostUsd,
|
|
573
|
+
durationMs,
|
|
574
|
+
})
|
|
575
|
+
} catch (histErr) {
|
|
576
|
+
console.warn('[dsh-moa] Failed to record run in history:', histErr)
|
|
577
|
+
}
|
|
578
|
+
|
|
473
579
|
return {
|
|
474
580
|
kind: 'synthesis',
|
|
475
581
|
content: synthesizedText,
|
|
476
|
-
aggregator: aggLabel,
|
|
582
|
+
aggregator: isFastMode ? 'Fast Mode (Direct)' : aggLabel,
|
|
477
583
|
references: referenceOutputs,
|
|
478
584
|
presetName: preset?.name || 'default',
|
|
585
|
+
isRefinement,
|
|
586
|
+
isFastMode,
|
|
479
587
|
winningIndex,
|
|
588
|
+
winnerModel,
|
|
480
589
|
promotedFiles,
|
|
481
590
|
liveCanvas,
|
|
591
|
+
usage: {
|
|
592
|
+
totalTokens,
|
|
593
|
+
totalCostUsd,
|
|
594
|
+
candidates: referenceOutputs.map(r => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
|
|
595
|
+
aggregator: aggUsage,
|
|
596
|
+
},
|
|
597
|
+
durationMs,
|
|
482
598
|
}
|
|
483
599
|
}
|
|
484
600
|
|
|
485
|
-
/**
|
|
486
|
-
* Formats full MoA output showing candidate outputs, judge synthesis, and promoted files.
|
|
487
|
-
*/
|
|
488
601
|
/**
|
|
489
602
|
* Replaces large code blocks with concise file/code summaries to avoid token waste and huge chat dumps.
|
|
490
603
|
*/
|
|
@@ -492,7 +605,6 @@ export function stripOrSummarizeCode(text) {
|
|
|
492
605
|
if (!text || typeof text !== 'string') return ''
|
|
493
606
|
return text.replace(/```([a-zA-Z0-9_\-\.\/]*)\s*([\w\.\/\-]+\.[a-zA-Z0-9]+)?\n([\s\S]*?)```/g, (match, lang, fileTag, code) => {
|
|
494
607
|
const lines = code.trim().split('\n')
|
|
495
|
-
// Keep very short commands (<= 3 lines) that are not HTML/JSX/source files
|
|
496
608
|
if (lines.length <= 3 && !/html|jsx|tsx|vue|svelte|css|js|ts/i.test(lang)) {
|
|
497
609
|
return match
|
|
498
610
|
}
|
|
@@ -515,9 +627,14 @@ export function formatMoAResponse({ moaResult, presetName }) {
|
|
|
515
627
|
return parts.join('\n')
|
|
516
628
|
}
|
|
517
629
|
|
|
518
|
-
|
|
630
|
+
const modeBadge = moaResult?.isFastMode ? '⚡ Fast Mode' : `Судья: ${judge}`
|
|
631
|
+
parts.push(`## 🧠 Mixture of Agents (Пресет: ${pName} | ${modeBadge})`)
|
|
519
632
|
parts.push('')
|
|
520
633
|
|
|
634
|
+
if (moaResult?.isRefinement) {
|
|
635
|
+
parts.push('> 🔄 **Режим**: Итеративная доработка проекта (Refinement)')
|
|
636
|
+
}
|
|
637
|
+
|
|
521
638
|
const hasPromoted = moaResult?.promotedFiles && moaResult.promotedFiles.length > 0
|
|
522
639
|
if (hasPromoted) {
|
|
523
640
|
parts.push(`> 📦 **Созданы файлы в проекте**: \`${moaResult.promotedFiles.join('`, `')}\``)
|
|
@@ -527,25 +644,35 @@ export function formatMoAResponse({ moaResult, presetName }) {
|
|
|
527
644
|
parts.push(`> 🎨 **Live Canvas**: [🚀 Открыть ${moaResult.liveCanvas.title || 'превью'} в Live Canvas](${moaResult.liveCanvas.previewUrl}) | [↗ Открыть в новой вкладке](${moaResult.liveCanvas.previewUrl})`)
|
|
528
645
|
}
|
|
529
646
|
|
|
530
|
-
|
|
531
|
-
|
|
647
|
+
// Cost tracking card (#10)
|
|
648
|
+
if (moaResult?.usage) {
|
|
649
|
+
const u = moaResult.usage
|
|
650
|
+
const costStr = u.totalCostUsd > 0 ? `~\$${u.totalCostUsd.toFixed(4)}` : 'Бесплатно'
|
|
651
|
+
const tokStr = u.totalTokens >= 1000 ? `${(u.totalTokens / 1000).toFixed(1)}k` : `${u.totalTokens}`
|
|
652
|
+
parts.push(`> 💰 **Стоимость запуска**: ${costStr} (всего ${tokStr} токенов)`)
|
|
532
653
|
}
|
|
533
654
|
|
|
534
|
-
parts.push(`### ⚖️ Вердикт судьи и итоговое решение (Синтез: ${judge})`)
|
|
535
|
-
parts.push('')
|
|
536
|
-
const cleanJudgeContent = hasPromoted
|
|
537
|
-
? stripOrSummarizeCode(moaResult?.content || '')
|
|
538
|
-
: (moaResult?.content || '(нет ответа)')
|
|
539
|
-
parts.push(cleanJudgeContent)
|
|
540
655
|
parts.push('')
|
|
541
656
|
|
|
657
|
+
if (!moaResult?.isFastMode) {
|
|
658
|
+
parts.push(`### ⚖️ Вердикт судьи и итоговое решение (Синтез: ${judge})`)
|
|
659
|
+
parts.push('')
|
|
660
|
+
const cleanJudgeContent = hasPromoted
|
|
661
|
+
? stripOrSummarizeCode(moaResult?.content || '')
|
|
662
|
+
: (moaResult?.content || '(нет ответа)')
|
|
663
|
+
parts.push(cleanJudgeContent)
|
|
664
|
+
parts.push('')
|
|
665
|
+
}
|
|
666
|
+
|
|
542
667
|
if (Array.isArray(refs) && refs.length > 0) {
|
|
543
|
-
|
|
668
|
+
const title = moaResult?.isFastMode ? '### 🚀 Результат генерации кандидата:' : `### 👥 Ответы моделей-советников (${refs.length}):`
|
|
669
|
+
parts.push(title)
|
|
544
670
|
parts.push('')
|
|
545
671
|
refs.forEach((ref, i) => {
|
|
546
672
|
const statusIcon = ref.ok ? '✅' : '⚠️'
|
|
547
673
|
const fileBadge = ref.files?.length ? ` (${ref.files.length} файл(ов))` : ''
|
|
548
|
-
|
|
674
|
+
const costBadge = ref.costUsd > 0 ? ` [~\$${ref.costUsd.toFixed(4)}]` : ''
|
|
675
|
+
parts.push(`#### ${statusIcon} Модель ${i + 1}: ${ref.label}${fileBadge}${costBadge}`)
|
|
549
676
|
parts.push('')
|
|
550
677
|
parts.push(stripOrSummarizeCode(ref.text))
|
|
551
678
|
parts.push('')
|
|
@@ -583,7 +710,9 @@ export class MoaRunnerAdapter {
|
|
|
583
710
|
return { provider, id: model, name: 'MoA Ensemble' }
|
|
584
711
|
}
|
|
585
712
|
|
|
586
|
-
async prepareCall(
|
|
713
|
+
async prepareCall(providerOrConfig, modelOrSignal, _signal) {
|
|
714
|
+
const provider = typeof providerOrConfig === 'object' && providerOrConfig !== null ? providerOrConfig.provider : providerOrConfig
|
|
715
|
+
const model = typeof providerOrConfig === 'object' && providerOrConfig !== null ? providerOrConfig.model : modelOrSignal
|
|
587
716
|
return {
|
|
588
717
|
model: { provider, id: model, name: 'MoA Ensemble' },
|
|
589
718
|
stream: (options) => this.stream(options),
|
|
@@ -618,7 +747,7 @@ export class MoaRunnerAdapter {
|
|
|
618
747
|
const startTime = Date.now()
|
|
619
748
|
let lastYieldTime = Date.now()
|
|
620
749
|
|
|
621
|
-
//
|
|
750
|
+
// Yield progressive updates while pipeline runs
|
|
622
751
|
while (true) {
|
|
623
752
|
if (signal?.aborted) return
|
|
624
753
|
|
|
@@ -659,7 +788,7 @@ export class MoaRunnerAdapter {
|
|
|
659
788
|
|
|
660
789
|
yield { type: 'text-delta', index: 0, text: '\n---\n\n' + formatted }
|
|
661
790
|
yield { type: 'block-end', index: 0, block: { type: 'text', text: formatted } }
|
|
662
|
-
yield { type: 'usage', usage: { inputTokens: 100, outputTokens: formatted.length } }
|
|
791
|
+
yield { type: 'usage', usage: { inputTokens: moaResult?.usage?.totalTokens || 100, outputTokens: formatted.length } }
|
|
663
792
|
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
664
793
|
} catch (err) {
|
|
665
794
|
if (signal?.aborted) return
|