@goodandready/dsh-moa 0.2.2 → 0.2.4
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 +14 -0
- package/docs/README.ru.md +14 -0
- package/lib/client.js +55 -3
- package/lib/file-workspace.js +94 -5
- package/lib/history.js +191 -0
- package/lib/index.js +185 -164
- package/lib/moa-runner.js +237 -86
- package/package.json +1 -1
package/lib/moa-runner.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Mixture of Agents (MoA) execution engine with Interactive Questioning
|
|
3
|
-
*
|
|
2
|
+
* Mixture of Agents (MoA) execution engine with Interactive Questioning,
|
|
3
|
+
* Isolated Multi-Candidate File Execution, Native Cost Tracking,
|
|
4
|
+
* Refinement Mode, and Multi-Candidate Scalability.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import {
|
|
@@ -8,13 +9,19 @@ import {
|
|
|
8
9
|
writeCandidateWorkspace,
|
|
9
10
|
promoteCandidateWorkspace,
|
|
10
11
|
cleanMoaWorkspaces,
|
|
12
|
+
collectProjectContext,
|
|
13
|
+
isRefinementTask,
|
|
11
14
|
} from './file-workspace.js'
|
|
12
15
|
|
|
16
|
+
import { recordMoaRun } from './history.js'
|
|
17
|
+
|
|
13
18
|
export {
|
|
14
19
|
extractFileBlocks,
|
|
15
20
|
writeCandidateWorkspace,
|
|
16
21
|
promoteCandidateWorkspace,
|
|
17
22
|
cleanMoaWorkspaces,
|
|
23
|
+
collectProjectContext,
|
|
24
|
+
isRefinementTask,
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
export const REFERENCE_SYSTEM_PROMPT = `You are an expert candidate engineer model in a Mixture of Agents (MoA) architecture.
|
|
@@ -34,12 +41,56 @@ RULES:
|
|
|
34
41
|
4. If the user request is broad, make opinionated, high-quality technical decisions and deliver a complete working project.
|
|
35
42
|
5. Never leave placeholders like "// TODO" or "... rest of code". Deliver exhaustive, working code.`
|
|
36
43
|
|
|
44
|
+
export const REFINEMENT_SYSTEM_PROMPT = `You are an expert software engineer performing an iterative modification / refinement on an existing project.
|
|
45
|
+
You are provided with the current codebase files and the user's delta request.
|
|
46
|
+
|
|
47
|
+
RULES:
|
|
48
|
+
1. Modify the existing files or create new files to fulfill the user's change request.
|
|
49
|
+
2. Deliver complete, production-ready updated code for every modified file.
|
|
50
|
+
3. Format each updated file in markdown code blocks with explicit file attribute:
|
|
51
|
+
\`\`\`javascript file="src/app.js"
|
|
52
|
+
...
|
|
53
|
+
\`\`\`
|
|
54
|
+
4. Keep the existing architecture, styling conventions and dependencies consistent.`
|
|
55
|
+
|
|
37
56
|
export const ADVISOR_QUESTION_PROMPT = `You are a senior technical advisor in a Mixture of Agents (MoA) architecture.
|
|
38
57
|
The user has provided a prompt that may have multiple design choices, architectural paths, or underspecified requirements.
|
|
39
58
|
|
|
40
59
|
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
60
|
Keep questions very clear, structured, and actionable. Avoid trivial questions. Respond directly in Russian.`
|
|
42
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Built-in pricing table for major LLM model families ($ per 1M tokens) (#10).
|
|
64
|
+
*/
|
|
65
|
+
export const MODEL_PRICING_REGISTRY = [
|
|
66
|
+
{ pattern: /deepseek.*flash|v4.*flash/i, inputPerM: 0.14, outputPerM: 0.28 },
|
|
67
|
+
{ pattern: /deepseek.*reasoner|r1/i, inputPerM: 0.55, outputPerM: 2.19 },
|
|
68
|
+
{ pattern: /deepseek/i, inputPerM: 0.27, outputPerM: 1.10 },
|
|
69
|
+
{ pattern: /gpt-4o-mini|gpt-5.*mini/i, inputPerM: 0.15, outputPerM: 0.60 },
|
|
70
|
+
{ pattern: /gpt-5|gpt-4o|sol/i, inputPerM: 2.50, outputPerM: 10.00 },
|
|
71
|
+
{ pattern: /grok/i, inputPerM: 2.00, outputPerM: 10.00 },
|
|
72
|
+
{ pattern: /claude-3-5-sonnet|claude-3-7-sonnet/i, inputPerM: 3.00, outputPerM: 15.00 },
|
|
73
|
+
{ pattern: /claude-3-5-haiku/i, inputPerM: 0.80, outputPerM: 4.00 },
|
|
74
|
+
{ pattern: /claude/i, inputPerM: 3.00, outputPerM: 15.00 },
|
|
75
|
+
{ pattern: /qwen/i, inputPerM: 0.40, outputPerM: 1.20 },
|
|
76
|
+
{ pattern: /commandcode/i, inputPerM: 0.50, outputPerM: 1.50 },
|
|
77
|
+
{ pattern: /.*/, inputPerM: 0.50, outputPerM: 1.50 },
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
export function estimateTokenCost(slot, usage = {}) {
|
|
81
|
+
const modelStr = `${slot?.provider || ''} ${slot?.model || ''}`.trim()
|
|
82
|
+
const match = MODEL_PRICING_REGISTRY.find((p) => p.pattern.test(modelStr)) || MODEL_PRICING_REGISTRY[MODEL_PRICING_REGISTRY.length - 1]
|
|
83
|
+
const inTokens = usage.inputTokens || usage.promptTokens || usage.input_tokens || 0
|
|
84
|
+
const outTokens = usage.outputTokens || usage.completionTokens || usage.output_tokens || 0
|
|
85
|
+
const cost = (inTokens * match.inputPerM / 1_000_000) + (outTokens * match.outputPerM / 1_000_000)
|
|
86
|
+
return {
|
|
87
|
+
inputTokens: inTokens,
|
|
88
|
+
outputTokens: outTokens,
|
|
89
|
+
totalTokens: inTokens + outTokens,
|
|
90
|
+
costUsd: Number(cost.toFixed(5)),
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
43
94
|
export function slotLabel(slot) {
|
|
44
95
|
if (!slot || typeof slot !== 'object') return 'unknown'
|
|
45
96
|
const prov = String(slot.provider || '').trim()
|
|
@@ -79,44 +130,38 @@ export function cleanAdvisoryMessages(messages = [], maxCharBudget = 4000) {
|
|
|
79
130
|
|
|
80
131
|
trimmed.push({ role, content: text })
|
|
81
132
|
}
|
|
82
|
-
|
|
83
133
|
return trimmed
|
|
84
134
|
}
|
|
85
135
|
|
|
86
|
-
/**
|
|
87
|
-
* Checks if the prompt should trigger a clarifying questions phase
|
|
88
|
-
* instead of immediate execution.
|
|
89
|
-
*/
|
|
90
136
|
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) {
|
|
137
|
+
if (!userPrompt || typeof userPrompt !== 'string') return false
|
|
138
|
+
const p = userPrompt.trim()
|
|
139
|
+
const wordCount = p.split(/\s+/).length
|
|
140
|
+
|
|
141
|
+
if (/^(да|нет|1|2|3|4|ок|погнали|давай|yes|no)\b/i.test(p) && wordCount <= 5) {
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const hasRecentQuestion = messages.some((m) => {
|
|
146
|
+
const text = typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '')
|
|
147
|
+
return text.includes('Уточнение требований') || text.includes('опросник') || text.includes('Вариант 1')
|
|
148
|
+
})
|
|
149
|
+
if (hasRecentQuestion) {
|
|
109
150
|
return false
|
|
110
151
|
}
|
|
111
152
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
153
|
+
const creationTriggers = [
|
|
154
|
+
'сделай', 'создай', 'напиши', 'разработай', 'придумай', 'реализуй',
|
|
155
|
+
'make', 'build', 'create', 'generate', 'develop',
|
|
156
|
+
]
|
|
157
|
+
const startsWithCreation = creationTriggers.some((t) => p.toLowerCase().startsWith(t))
|
|
158
|
+
|
|
159
|
+
if (startsWithCreation && wordCount <= 18) {
|
|
160
|
+
return true
|
|
161
|
+
}
|
|
116
162
|
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
const wordCount = prompt.split(/\s+/).filter(Boolean).length
|
|
163
|
+
const vagueNouns = ['приложение', 'игру', 'сервис', 'сайт', 'лендинг', 'калькулятор', 'виджет', 'дашборд', 'app', 'game', 'tool', 'website']
|
|
164
|
+
if (vagueNouns.some((n) => p.toLowerCase().includes(n))) {
|
|
120
165
|
if (wordCount <= 12) return true
|
|
121
166
|
}
|
|
122
167
|
|
|
@@ -140,21 +185,30 @@ ${joined}
|
|
|
140
185
|
В конце добавь примечание, что пользователь может ответить кратко (например: "1, 2, темная тема") или довериться выбору по умолчанию.`
|
|
141
186
|
}
|
|
142
187
|
|
|
143
|
-
export function buildSynthesisPrompt(userPrompt, referenceOutputs = []) {
|
|
188
|
+
export function buildSynthesisPrompt(userPrompt, referenceOutputs = [], judgeCriteria = '') {
|
|
144
189
|
const joined = referenceOutputs
|
|
145
190
|
.map((r, i) => {
|
|
146
191
|
const fileSummary = (r.files && r.files.length > 0)
|
|
147
192
|
? ` [Созданные файлы: ${r.files.map((f) => f.relativePath).join(', ')}]`
|
|
148
193
|
: ''
|
|
149
|
-
|
|
194
|
+
// If 3+ candidates, summarize text to prevent judge context overflow (#13)
|
|
195
|
+
let textContent = r.text
|
|
196
|
+
if (referenceOutputs.length >= 3 && textContent.length > 3000) {
|
|
197
|
+
textContent = stripOrSummarizeCode(textContent)
|
|
198
|
+
}
|
|
199
|
+
return `Reference ${i + 1} — ${r.label}:${fileSummary}\n${textContent}`
|
|
150
200
|
})
|
|
151
201
|
.join('\n\n')
|
|
152
202
|
|
|
203
|
+
const criteriaBlock = judgeCriteria && judgeCriteria.trim()
|
|
204
|
+
? `\n### 🎯 Дополнительные критерии оценки от пользователя:\n${judgeCriteria.trim()}\n`
|
|
205
|
+
: ''
|
|
206
|
+
|
|
153
207
|
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
208
|
|
|
155
209
|
Original user prompt:
|
|
156
210
|
${userPrompt}
|
|
157
|
-
|
|
211
|
+
${criteriaBlock}
|
|
158
212
|
Reference responses from candidate models:
|
|
159
213
|
${joined}
|
|
160
214
|
|
|
@@ -181,9 +235,10 @@ export function parseWinnerIndex(judgeText, defaultIndex = 1) {
|
|
|
181
235
|
const idx = parseInt(match[1], 10)
|
|
182
236
|
if (!isNaN(idx) && idx >= 1) return idx
|
|
183
237
|
}
|
|
184
|
-
|
|
185
|
-
if (
|
|
186
|
-
|
|
238
|
+
const candMatch = /(?:Кандидат|Reference)\s*(\d+)\b/i.exec(judgeText)
|
|
239
|
+
if (candMatch) {
|
|
240
|
+
const idx = parseInt(candMatch[1], 10)
|
|
241
|
+
if (!isNaN(idx) && idx >= 1) return idx
|
|
187
242
|
}
|
|
188
243
|
return defaultIndex
|
|
189
244
|
}
|
|
@@ -263,6 +318,13 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
263
318
|
const text = (typeof res === 'string' ? res : (res?.content || res?.text || '')).trim()
|
|
264
319
|
const files = extractFileBlocks(text)
|
|
265
320
|
|
|
321
|
+
// Estimate tokens & cost
|
|
322
|
+
const rawUsage = res?.usage || {
|
|
323
|
+
inputTokens: Math.round(JSON.stringify(fullMessages).length / 4),
|
|
324
|
+
outputTokens: Math.round(text.length / 4),
|
|
325
|
+
}
|
|
326
|
+
const costInfo = estimateTokenCost(slot, rawUsage)
|
|
327
|
+
|
|
266
328
|
if (typeof onProgress === 'function') {
|
|
267
329
|
const fileMsg = files.length > 0 ? ` (создано файлов: ${files.length})` : ''
|
|
268
330
|
onProgress(`✅ *Кандидат ${i + 1} (${label}) завершил ответ${fileMsg}.*\n`)
|
|
@@ -275,6 +337,8 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
275
337
|
model: slot.model,
|
|
276
338
|
text: text || '(empty response)',
|
|
277
339
|
files,
|
|
340
|
+
usage: costInfo,
|
|
341
|
+
costUsd: costInfo.costUsd,
|
|
278
342
|
ok: true,
|
|
279
343
|
}
|
|
280
344
|
} catch (err) {
|
|
@@ -289,6 +353,8 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
289
353
|
model: slot.model,
|
|
290
354
|
text: `[failed: ${err?.message || String(err)}]`,
|
|
291
355
|
files: [],
|
|
356
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
357
|
+
costUsd: 0,
|
|
292
358
|
ok: false,
|
|
293
359
|
}
|
|
294
360
|
}
|
|
@@ -299,10 +365,10 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
299
365
|
|
|
300
366
|
/**
|
|
301
367
|
* Runs the full Mixture of Agents pipeline:
|
|
302
|
-
* 1. Checks if questions
|
|
368
|
+
* 1. Checks if questions or refinement needed
|
|
303
369
|
* 2. Parallel candidate proposers write to .moa/candidate-X/
|
|
304
|
-
* 3. Aggregator judges and picks winner
|
|
305
|
-
* 4. Promotes winner files and
|
|
370
|
+
* 3. Aggregator judges and picks winner (or bypassed in Fast Mode)
|
|
371
|
+
* 4. Promotes winner files, records history and calculates costs
|
|
306
372
|
*/
|
|
307
373
|
export async function runMoAPipeline({
|
|
308
374
|
userPrompt,
|
|
@@ -317,15 +383,31 @@ export async function runMoAPipeline({
|
|
|
317
383
|
throw new Error('callLlm function is required for runMoAPipeline')
|
|
318
384
|
}
|
|
319
385
|
|
|
386
|
+
const startTime = Date.now()
|
|
320
387
|
const referenceModels = preset?.reference_models || preset?.referenceModels || []
|
|
321
388
|
const aggregator = preset?.aggregator || { provider: 'default', model: 'default' }
|
|
322
389
|
const refTemp = preset?.reference_temperature ?? preset?.referenceTemperature ?? 0.6
|
|
323
390
|
const aggTemp = preset?.aggregator_temperature ?? preset?.aggregatorTemperature ?? 0.4
|
|
324
391
|
const maxTokens = preset?.max_tokens ?? preset?.maxTokens ?? 4096
|
|
392
|
+
const judgeCriteria = preset?.judge_criteria || preset?.judgeCriteria || ''
|
|
325
393
|
const workDir = cwd || process.cwd()
|
|
326
394
|
|
|
395
|
+
// Collect project context (#6) and check refinement task (#4)
|
|
396
|
+
const projectCtx = await collectProjectContext(workDir, 16000)
|
|
397
|
+
const isRefinement = isRefinementTask(userPrompt, projectCtx.files)
|
|
398
|
+
|
|
399
|
+
// System prompt selection
|
|
400
|
+
let candidateSystemPrompt = REFERENCE_SYSTEM_PROMPT
|
|
401
|
+
if (isRefinement && projectCtx.files.length > 0) {
|
|
402
|
+
const fileList = projectCtx.files.map((f) => `### File: ${f.relativePath}\n\`\`\`\n${f.content}\n\`\`\``).join('\n\n')
|
|
403
|
+
candidateSystemPrompt = `${REFINEMENT_SYSTEM_PROMPT}\n\n## Existing Project Files:\n${fileList}`
|
|
404
|
+
if (typeof onProgress === 'function') {
|
|
405
|
+
onProgress(`🔄 *[Refinement Mode]: Обнаружен существующий проект (${projectCtx.files.length} файлов). Кандидаты вносят точечные изменения...*\n\n`)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
327
409
|
// Phase 1: Check if clarifying questions should be asked
|
|
328
|
-
const needsQuestions = !skipQuestions && isBroadPromptRequiringQuestions(userPrompt, messages)
|
|
410
|
+
const needsQuestions = !skipQuestions && !isRefinement && isBroadPromptRequiringQuestions(userPrompt, messages)
|
|
329
411
|
if (needsQuestions) {
|
|
330
412
|
if (typeof onProgress === 'function') {
|
|
331
413
|
onProgress('🔍 *Задача общего характера. Советники формируют ключевые развилки...*\n\n')
|
|
@@ -371,15 +453,19 @@ export async function runMoAPipeline({
|
|
|
371
453
|
}
|
|
372
454
|
}
|
|
373
455
|
|
|
456
|
+
// FAST MODE: single candidate model bypasses aggregator (#14)
|
|
457
|
+
const isFastMode = referenceModels.length === 1
|
|
458
|
+
|
|
374
459
|
// Phase 2: Parallel execution & file generation
|
|
375
460
|
if (typeof onProgress === 'function') {
|
|
376
|
-
|
|
461
|
+
const modeLabel = isFastMode ? '⚡ Fast Mode' : `советниками (${referenceModels.length})`
|
|
462
|
+
onProgress(`🚀 *Запускаю реализацию ${modeLabel}...*\n\n`)
|
|
377
463
|
}
|
|
378
464
|
|
|
379
465
|
const referenceOutputs = await runReferencesParallel(
|
|
380
466
|
referenceModels,
|
|
381
467
|
[...messages, { role: 'user', content: userPrompt }],
|
|
382
|
-
{ referenceTemperature: refTemp, maxTokens },
|
|
468
|
+
{ systemPrompt: candidateSystemPrompt, referenceTemperature: refTemp, maxTokens },
|
|
383
469
|
callLlm,
|
|
384
470
|
onProgress,
|
|
385
471
|
)
|
|
@@ -399,35 +485,51 @@ export async function runMoAPipeline({
|
|
|
399
485
|
}
|
|
400
486
|
}
|
|
401
487
|
|
|
402
|
-
|
|
488
|
+
let synthesizedText = ''
|
|
489
|
+
let winningIndex = 1
|
|
490
|
+
let aggUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
|
|
403
491
|
const aggLabel = slotLabel(aggregator)
|
|
404
|
-
if (typeof onProgress === 'function') {
|
|
405
|
-
onProgress(`\n⚖️ *Все кандидаты завершили генерацию. Судья (${aggLabel}) оценивает код и файлы...*\n\n`)
|
|
406
|
-
}
|
|
407
492
|
|
|
408
|
-
|
|
493
|
+
if (isFastMode) {
|
|
494
|
+
// Fast mode: promote candidate 1 directly without judge
|
|
495
|
+
synthesizedText = referenceOutputs[0]?.text || '(empty fast response)'
|
|
496
|
+
winningIndex = 1
|
|
497
|
+
} else {
|
|
498
|
+
// Phase 3: Aggregator evaluation
|
|
499
|
+
if (typeof onProgress === 'function') {
|
|
500
|
+
onProgress(`\n⚖️ *Все кандидаты завершили генерацию. Судья (${aggLabel}) оценивает код и файлы...*\n\n`)
|
|
501
|
+
}
|
|
409
502
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
503
|
+
const synthPrompt = buildSynthesisPrompt(userPrompt, referenceOutputs, judgeCriteria)
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
const aggPromise = callLlm({
|
|
507
|
+
provider: aggregator.provider,
|
|
508
|
+
model: aggregator.model,
|
|
509
|
+
messages: [{ role: 'user', content: synthPrompt }],
|
|
510
|
+
temperature: aggTemp,
|
|
511
|
+
maxTokens,
|
|
512
|
+
})
|
|
513
|
+
const aggTimeout = new Promise((_, reject) =>
|
|
514
|
+
setTimeout(() => reject(new Error(`Timeout after 90s waiting for aggregator ${aggLabel}`)), 90000).unref()
|
|
515
|
+
)
|
|
516
|
+
const res = await Promise.race([aggPromise, aggTimeout])
|
|
517
|
+
synthesizedText = (typeof res === 'string' ? res : (res?.content || res?.text || '')).trim()
|
|
518
|
+
|
|
519
|
+
const rawAggUsage = res?.usage || {
|
|
520
|
+
inputTokens: Math.round(synthPrompt.length / 4),
|
|
521
|
+
outputTokens: Math.round(synthesizedText.length / 4),
|
|
522
|
+
}
|
|
523
|
+
aggUsage = estimateTokenCost(aggregator, rawAggUsage)
|
|
524
|
+
} catch (err) {
|
|
525
|
+
synthesizedText = `[Aggregator error: ${err?.message || String(err)}]\n\nFallback candidate outputs:\n\n` +
|
|
526
|
+
referenceOutputs.map((r, i) => `### ${r.label}\n${r.text}`).join('\n\n')
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
winningIndex = parseWinnerIndex(synthesizedText, 1)
|
|
427
530
|
}
|
|
428
531
|
|
|
429
532
|
// Phase 4: Promote winner files and cleanup
|
|
430
|
-
const winningIndex = parseWinnerIndex(synthesizedText, 1)
|
|
431
533
|
let promotedFiles = []
|
|
432
534
|
try {
|
|
433
535
|
promotedFiles = await promoteCandidateWorkspace(workDir, winningIndex)
|
|
@@ -448,7 +550,8 @@ export async function runMoAPipeline({
|
|
|
448
550
|
const lcRes = await fetch(`http://127.0.0.1:${port}/dsh-live-canvas/api/open-file`, {
|
|
449
551
|
method: 'POST',
|
|
450
552
|
headers: { 'Content-Type': 'application/json' },
|
|
451
|
-
body: JSON.stringify({ filePath: previewCandidate })
|
|
553
|
+
body: JSON.stringify({ filePath: previewCandidate }),
|
|
554
|
+
signal: AbortSignal.timeout(500),
|
|
452
555
|
})
|
|
453
556
|
if (lcRes.ok) {
|
|
454
557
|
const lcData = await lcRes.json()
|
|
@@ -470,21 +573,55 @@ export async function runMoAPipeline({
|
|
|
470
573
|
}
|
|
471
574
|
}
|
|
472
575
|
|
|
576
|
+
// Calculate totals
|
|
577
|
+
const totalTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0) + aggUsage.totalTokens
|
|
578
|
+
const totalCostUsd = Number((referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + aggUsage.costUsd).toFixed(5))
|
|
579
|
+
const durationMs = Date.now() - startTime
|
|
580
|
+
|
|
581
|
+
const winningRef = referenceOutputs[winningIndex - 1]
|
|
582
|
+
const winnerModel = winningRef?.label || slotLabel(referenceModels[0])
|
|
583
|
+
|
|
584
|
+
// Record run to history (#9, #10, #11)
|
|
585
|
+
try {
|
|
586
|
+
recordMoaRun({
|
|
587
|
+
prompt: userPrompt,
|
|
588
|
+
preset: preset?.name || 'default',
|
|
589
|
+
isRefinement,
|
|
590
|
+
candidates: referenceOutputs,
|
|
591
|
+
aggregator: isFastMode ? null : { provider: aggregator.provider, model: aggregator.model, usage: aggUsage, costUsd: aggUsage.costUsd },
|
|
592
|
+
winnerIndex: winningIndex,
|
|
593
|
+
winnerModel,
|
|
594
|
+
promotedFiles,
|
|
595
|
+
totalTokens,
|
|
596
|
+
totalCostUsd,
|
|
597
|
+
durationMs,
|
|
598
|
+
})
|
|
599
|
+
} catch (histErr) {
|
|
600
|
+
console.warn('[dsh-moa] Failed to record run in history:', histErr)
|
|
601
|
+
}
|
|
602
|
+
|
|
473
603
|
return {
|
|
474
604
|
kind: 'synthesis',
|
|
475
605
|
content: synthesizedText,
|
|
476
|
-
aggregator: aggLabel,
|
|
606
|
+
aggregator: isFastMode ? 'Fast Mode (Direct)' : aggLabel,
|
|
477
607
|
references: referenceOutputs,
|
|
478
608
|
presetName: preset?.name || 'default',
|
|
609
|
+
isRefinement,
|
|
610
|
+
isFastMode,
|
|
479
611
|
winningIndex,
|
|
612
|
+
winnerModel,
|
|
480
613
|
promotedFiles,
|
|
481
614
|
liveCanvas,
|
|
615
|
+
usage: {
|
|
616
|
+
totalTokens,
|
|
617
|
+
totalCostUsd,
|
|
618
|
+
candidates: referenceOutputs.map(r => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
|
|
619
|
+
aggregator: aggUsage,
|
|
620
|
+
},
|
|
621
|
+
durationMs,
|
|
482
622
|
}
|
|
483
623
|
}
|
|
484
624
|
|
|
485
|
-
/**
|
|
486
|
-
* Formats full MoA output showing candidate outputs, judge synthesis, and promoted files.
|
|
487
|
-
*/
|
|
488
625
|
/**
|
|
489
626
|
* Replaces large code blocks with concise file/code summaries to avoid token waste and huge chat dumps.
|
|
490
627
|
*/
|
|
@@ -492,7 +629,6 @@ export function stripOrSummarizeCode(text) {
|
|
|
492
629
|
if (!text || typeof text !== 'string') return ''
|
|
493
630
|
return text.replace(/```([a-zA-Z0-9_\-\.\/]*)\s*([\w\.\/\-]+\.[a-zA-Z0-9]+)?\n([\s\S]*?)```/g, (match, lang, fileTag, code) => {
|
|
494
631
|
const lines = code.trim().split('\n')
|
|
495
|
-
// Keep very short commands (<= 3 lines) that are not HTML/JSX/source files
|
|
496
632
|
if (lines.length <= 3 && !/html|jsx|tsx|vue|svelte|css|js|ts/i.test(lang)) {
|
|
497
633
|
return match
|
|
498
634
|
}
|
|
@@ -515,9 +651,14 @@ export function formatMoAResponse({ moaResult, presetName }) {
|
|
|
515
651
|
return parts.join('\n')
|
|
516
652
|
}
|
|
517
653
|
|
|
518
|
-
|
|
654
|
+
const modeBadge = moaResult?.isFastMode ? '⚡ Fast Mode' : `Судья: ${judge}`
|
|
655
|
+
parts.push(`## 🧠 Mixture of Agents (Пресет: ${pName} | ${modeBadge})`)
|
|
519
656
|
parts.push('')
|
|
520
657
|
|
|
658
|
+
if (moaResult?.isRefinement) {
|
|
659
|
+
parts.push('> 🔄 **Режим**: Итеративная доработка проекта (Refinement)')
|
|
660
|
+
}
|
|
661
|
+
|
|
521
662
|
const hasPromoted = moaResult?.promotedFiles && moaResult.promotedFiles.length > 0
|
|
522
663
|
if (hasPromoted) {
|
|
523
664
|
parts.push(`> 📦 **Созданы файлы в проекте**: \`${moaResult.promotedFiles.join('`, `')}\``)
|
|
@@ -527,25 +668,35 @@ export function formatMoAResponse({ moaResult, presetName }) {
|
|
|
527
668
|
parts.push(`> 🎨 **Live Canvas**: [🚀 Открыть ${moaResult.liveCanvas.title || 'превью'} в Live Canvas](${moaResult.liveCanvas.previewUrl}) | [↗ Открыть в новой вкладке](${moaResult.liveCanvas.previewUrl})`)
|
|
528
669
|
}
|
|
529
670
|
|
|
530
|
-
|
|
531
|
-
|
|
671
|
+
// Cost tracking card (#10)
|
|
672
|
+
if (moaResult?.usage) {
|
|
673
|
+
const u = moaResult.usage
|
|
674
|
+
const costStr = u.totalCostUsd > 0 ? `~\$${u.totalCostUsd.toFixed(4)}` : 'Бесплатно'
|
|
675
|
+
const tokStr = u.totalTokens >= 1000 ? `${(u.totalTokens / 1000).toFixed(1)}k` : `${u.totalTokens}`
|
|
676
|
+
parts.push(`> 💰 **Стоимость запуска**: ${costStr} (всего ${tokStr} токенов)`)
|
|
532
677
|
}
|
|
533
678
|
|
|
534
|
-
parts.push(`### ⚖️ Вердикт судьи и итоговое решение (Синтез: ${judge})`)
|
|
535
|
-
parts.push('')
|
|
536
|
-
const cleanJudgeContent = hasPromoted
|
|
537
|
-
? stripOrSummarizeCode(moaResult?.content || '')
|
|
538
|
-
: (moaResult?.content || '(нет ответа)')
|
|
539
|
-
parts.push(cleanJudgeContent)
|
|
540
679
|
parts.push('')
|
|
541
680
|
|
|
681
|
+
if (!moaResult?.isFastMode) {
|
|
682
|
+
parts.push(`### ⚖️ Вердикт судьи и итоговое решение (Синтез: ${judge})`)
|
|
683
|
+
parts.push('')
|
|
684
|
+
const cleanJudgeContent = hasPromoted
|
|
685
|
+
? stripOrSummarizeCode(moaResult?.content || '')
|
|
686
|
+
: (moaResult?.content || '(нет ответа)')
|
|
687
|
+
parts.push(cleanJudgeContent)
|
|
688
|
+
parts.push('')
|
|
689
|
+
}
|
|
690
|
+
|
|
542
691
|
if (Array.isArray(refs) && refs.length > 0) {
|
|
543
|
-
|
|
692
|
+
const title = moaResult?.isFastMode ? '### 🚀 Результат генерации кандидата:' : `### 👥 Ответы моделей-советников (${refs.length}):`
|
|
693
|
+
parts.push(title)
|
|
544
694
|
parts.push('')
|
|
545
695
|
refs.forEach((ref, i) => {
|
|
546
696
|
const statusIcon = ref.ok ? '✅' : '⚠️'
|
|
547
697
|
const fileBadge = ref.files?.length ? ` (${ref.files.length} файл(ов))` : ''
|
|
548
|
-
|
|
698
|
+
const costBadge = ref.costUsd > 0 ? ` [~\$${ref.costUsd.toFixed(4)}]` : ''
|
|
699
|
+
parts.push(`#### ${statusIcon} Модель ${i + 1}: ${ref.label}${fileBadge}${costBadge}`)
|
|
549
700
|
parts.push('')
|
|
550
701
|
parts.push(stripOrSummarizeCode(ref.text))
|
|
551
702
|
parts.push('')
|
|
@@ -618,7 +769,7 @@ export class MoaRunnerAdapter {
|
|
|
618
769
|
const startTime = Date.now()
|
|
619
770
|
let lastYieldTime = Date.now()
|
|
620
771
|
|
|
621
|
-
//
|
|
772
|
+
// Yield progressive updates while pipeline runs
|
|
622
773
|
while (true) {
|
|
623
774
|
if (signal?.aborted) return
|
|
624
775
|
|
|
@@ -659,7 +810,7 @@ export class MoaRunnerAdapter {
|
|
|
659
810
|
|
|
660
811
|
yield { type: 'text-delta', index: 0, text: '\n---\n\n' + formatted }
|
|
661
812
|
yield { type: 'block-end', index: 0, block: { type: 'text', text: formatted } }
|
|
662
|
-
yield { type: 'usage', usage: { inputTokens: 100, outputTokens: formatted.length } }
|
|
813
|
+
yield { type: 'usage', usage: { inputTokens: moaResult?.usage?.totalTokens || 100, outputTokens: formatted.length } }
|
|
663
814
|
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
664
815
|
} catch (err) {
|
|
665
816
|
if (signal?.aborted) return
|