@goodandready/dsh-moa 0.2.8 → 0.2.10
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 +64 -37
- package/docs/README.ru.md +64 -37
- package/docs/README.zh.md +70 -27
- package/docs/design/DESIGN.md +43 -41
- package/docs/plans/47-client-js-split-plan.md +57 -0
- package/lib/client.js +666 -465
- package/lib/file-workspace.js +3 -1
- package/lib/history.js +4 -1
- package/lib/index.js +66 -12
- package/lib/live-canvas.js +34 -0
- package/lib/moa-runner.js +665 -351
- package/lib/pricing.js +17 -3
- package/package.json +1 -1
package/lib/moa-runner.js
CHANGED
|
@@ -1,84 +1,75 @@
|
|
|
1
|
-
import { estimateTokenCost as calculateTokenCost, resolveModelRates, refreshCatalogInBackground, DIRECT_VENDOR_RATES, FALLBACK_RATES } from './pricing.js'
|
|
2
1
|
/**
|
|
3
|
-
* Mixture of Agents (MoA)
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
9
|
+
* - Live token streaming for aggregator
|
|
10
|
+
* - File promotion & Live Canvas sandbox preview
|
|
6
11
|
*/
|
|
7
12
|
|
|
13
|
+
import path from 'node:path'
|
|
8
14
|
import {
|
|
9
15
|
extractFileBlocks,
|
|
10
|
-
writeCandidateWorkspace,
|
|
11
|
-
promoteCandidateWorkspace,
|
|
12
|
-
cleanMoaWorkspaces,
|
|
13
16
|
collectProjectContext,
|
|
14
17
|
isRefinementTask,
|
|
15
|
-
} from './file-workspace.js'
|
|
16
|
-
|
|
17
|
-
import { recordMoaRun } from './history.js'
|
|
18
|
-
|
|
19
|
-
export {
|
|
20
|
-
extractFileBlocks,
|
|
21
18
|
writeCandidateWorkspace,
|
|
22
19
|
promoteCandidateWorkspace,
|
|
23
20
|
cleanMoaWorkspaces,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
21
|
+
} from './file-workspace.js'
|
|
22
|
+
import { estimateTokenCost } from './pricing.js'
|
|
23
|
+
import { recordMoaRun } from './history.js'
|
|
27
24
|
|
|
28
|
-
export
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
Keep questions very clear, structured, and actionable. Avoid trivial questions. Respond directly in Russian.`
|
|
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
|
-
}
|
|
25
|
+
export { estimateTokenCost } from './pricing.js'
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_PROMPT = 'Please propose an optimal, well-structured, production-ready solution with full code and explanations.'
|
|
28
|
+
|
|
29
|
+
export const REFERENCE_SYSTEM_PROMPT = `You are an expert AI software architect and senior engineer acting as a candidate proposer in a Mixture of Agents (MoA) ensemble.
|
|
30
|
+
Your task is to provide the highest-quality, robust, complete, production-ready solution to the user request.
|
|
31
|
+
Write clean, modern, fully functional code without placeholders or shortcuts.
|
|
32
|
+
When generating files for a project, explicitly specify file paths using fenced code blocks with file annotations, e.g.:
|
|
33
|
+
\`\`\`html file="index.html"
|
|
34
|
+
\`\`\`
|
|
35
|
+
\`\`\`javascript file="script.js"
|
|
36
|
+
\`\`\`
|
|
37
|
+
\`\`\`css file="style.css"
|
|
38
|
+
\`\`\``
|
|
39
|
+
|
|
40
|
+
export const ANTIPATTERNS_RUBRIC = `### 🚫 Strict Antipatterns Evaluation Checklist
|
|
41
|
+
Penalize and strictly downgrade candidates exhibiting any of the following flaws:
|
|
42
|
+
1. 🚫 Lazy Code & Placeholders:
|
|
43
|
+
- Phrases like "// ... rest of code unchanged", "/* TODO: implement */", incomplete functions or stubs returning null/mock without notice.
|
|
44
|
+
2. 🚫 Blind Mocking:
|
|
45
|
+
- Hardcoded dummy arrays instead of real dynamic logic, user input handling, or real API integration.
|
|
46
|
+
3. 🚫 Silent Failures & Missing Error Handling:
|
|
47
|
+
- Missing try/catch around async calls, fetch, JSON.parse; lack of user-facing fallback or retry states.
|
|
48
|
+
4. 🚫 AI Slop UI & Poor Ergonomics:
|
|
49
|
+
- Generic purple/cyan gradients on pure black backgrounds, blurry drop-shadows without borders, lack of typographic hierarchy, low contrast (e.g. light gray text on white).
|
|
50
|
+
5. 🚫 Missing UI States:
|
|
51
|
+
- Lack of loading state (spinner/skeleton), error display with retry action, or empty state when no data exists.
|
|
52
|
+
6. 🚫 Broken Layout & Mobile Incompatibility:
|
|
53
|
+
- Fixed pixel widths (e.g. width: 800px) overflowing small viewports; unscrollable modal dialogs.
|
|
54
|
+
7. 🚫 Monolithic God Objects & Overengineering:
|
|
55
|
+
- Dumping 1000+ lines into a single unmaintainable file, or building 10+ abstraction layers for a 2-function task.
|
|
56
|
+
8. 🚫 Context Amnesia & Regressions:
|
|
57
|
+
- Dropping or breaking previously functioning project features while adding new code.`
|
|
68
58
|
|
|
69
59
|
export function slotLabel(slot) {
|
|
70
|
-
if (!slot
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return prov || model || 'unknown'
|
|
60
|
+
if (!slot) return 'unknown'
|
|
61
|
+
if (typeof slot === 'string') return slot
|
|
62
|
+
if (slot.provider && slot.model) return `${slot.provider}:${slot.model}`
|
|
63
|
+
return slot.model || slot.provider || 'unknown'
|
|
75
64
|
}
|
|
76
65
|
|
|
77
66
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
67
|
+
* Extracts and sanitizes conversation history for candidate models.
|
|
68
|
+
* Robust against complex DSH message content types (strings, part arrays, objects, tool calls).
|
|
80
69
|
*/
|
|
81
|
-
export function cleanAdvisoryMessages(messages = [], maxCharBudget =
|
|
70
|
+
export function cleanAdvisoryMessages(messages = [], maxCharBudget = 24000) {
|
|
71
|
+
if (!Array.isArray(messages)) return []
|
|
72
|
+
|
|
82
73
|
const trimmed = []
|
|
83
74
|
for (const msg of messages) {
|
|
84
75
|
if (!msg || typeof msg !== 'object') continue
|
|
@@ -90,12 +81,21 @@ export function cleanAdvisoryMessages(messages = [], maxCharBudget = 4000) {
|
|
|
90
81
|
text = msg.content
|
|
91
82
|
} else if (Array.isArray(msg.content)) {
|
|
92
83
|
text = msg.content
|
|
93
|
-
.filter((part) => part && part.type === 'text' && typeof part.text === 'string')
|
|
84
|
+
.filter((part) => part && typeof part === 'object' && part.type === 'text' && typeof part.text === 'string')
|
|
94
85
|
.map((part) => part.text)
|
|
95
86
|
.join('\n')
|
|
87
|
+
} else if (msg.content && typeof msg.content === 'object') {
|
|
88
|
+
if (typeof msg.content.text === 'string') {
|
|
89
|
+
text = msg.content.text
|
|
90
|
+
} else if (typeof msg.content.content === 'string') {
|
|
91
|
+
text = msg.content.content
|
|
92
|
+
}
|
|
93
|
+
} else if (typeof msg.text === 'string') {
|
|
94
|
+
text = msg.text
|
|
96
95
|
}
|
|
97
96
|
|
|
98
|
-
|
|
97
|
+
text = text.trim()
|
|
98
|
+
if (!text) continue
|
|
99
99
|
|
|
100
100
|
if (text.length > maxCharBudget) {
|
|
101
101
|
const head = text.slice(0, Math.floor(maxCharBudget * 0.7))
|
|
@@ -118,7 +118,7 @@ export function isBroadPromptRequiringQuestions(userPrompt = '', messages = [])
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
const hasRecentQuestion = messages.some((m) => {
|
|
121
|
-
const text = typeof m
|
|
121
|
+
const text = typeof m?.content === 'string' ? m.content : JSON.stringify(m?.content || '')
|
|
122
122
|
return text.includes('Уточнение требований') || text.includes('опросник') || text.includes('Вариант 1')
|
|
123
123
|
})
|
|
124
124
|
if (hasRecentQuestion) {
|
|
@@ -145,28 +145,81 @@ export function isBroadPromptRequiringQuestions(userPrompt = '', messages = [])
|
|
|
145
145
|
|
|
146
146
|
export function buildQuestionSynthesisPrompt(userPrompt, referenceOutputs = []) {
|
|
147
147
|
const joined = referenceOutputs
|
|
148
|
-
.map((r, i) =>
|
|
148
|
+
.map((r, i) => `Advisor ${i + 1} (${r.label}):\n${r.text}`)
|
|
149
149
|
.join('\n\n')
|
|
150
150
|
|
|
151
|
-
return
|
|
152
|
-
|
|
151
|
+
return `You are the lead architect and judge in a Mixture of Agents (MoA) ensemble.
|
|
152
|
+
The user gave the task:
|
|
153
153
|
"${userPrompt}"
|
|
154
154
|
|
|
155
|
-
|
|
155
|
+
The advisors proposed the following decision points and clarifications:
|
|
156
156
|
${joined}
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
Your task is to synthesize a single, compact, friendly and structured questionnaire (2-4 questions) in the same language as the user's prompt.
|
|
159
|
+
Each question must offer 2-3 concrete recommended answer options (e.g.: 1. Format: single-file HTML/JS or React? 2. Style: minimalism, iOS or neubrutalism?).
|
|
160
|
+
At the end, add a note that the user can answer briefly (e.g.: "1, 2, dark theme") or trust the defaults.`
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
export function
|
|
163
|
+
export function buildCuratorSynthesisPrompt(userPrompt, referenceOutputs = [], judgeCriteria = '') {
|
|
164
164
|
const joined = referenceOutputs
|
|
165
165
|
.map((r, i) => {
|
|
166
166
|
const fileSummary = (r.files && r.files.length > 0)
|
|
167
|
-
? ` [
|
|
167
|
+
? ` [Files created: ${r.files.map((f) => f.relativePath).join(', ')}]`
|
|
168
|
+
: ''
|
|
169
|
+
let textContent = r.text
|
|
170
|
+
if (referenceOutputs.length >= 3 && textContent.length > 3000) {
|
|
171
|
+
textContent = stripOrSummarizeCode(textContent)
|
|
172
|
+
}
|
|
173
|
+
return `Candidate ${i + 1} — ${r.label}:${fileSummary}\n${textContent}`
|
|
174
|
+
})
|
|
175
|
+
.join('\n\n')
|
|
176
|
+
|
|
177
|
+
const criteriaBlock = judgeCriteria && judgeCriteria.trim()
|
|
178
|
+
? `\n### 🎯 Additional evaluation criteria:\n${judgeCriteria.trim()}\n`
|
|
179
|
+
: ''
|
|
180
|
+
|
|
181
|
+
return `You are the expert Lead Technical Curator and Solution Architect in a Mixture of Agents (MoA) ensemble.
|
|
182
|
+
Your mission is not merely to select one candidate, but to synthesize the optimal solution by extracting the finest components from each candidate's response, identifying potential flaws using the strict antipatterns rubric, and selecting/advising which single agent model is best suited to assemble the final unified deliverable.
|
|
183
|
+
|
|
184
|
+
User Request:
|
|
185
|
+
${userPrompt}
|
|
186
|
+
${criteriaBlock}
|
|
187
|
+
Candidate proposals:
|
|
188
|
+
${joined}
|
|
189
|
+
|
|
190
|
+
${ANTIPATTERNS_RUBRIC}
|
|
191
|
+
|
|
192
|
+
Instructions:
|
|
193
|
+
Your response MUST be structured into three clear parts (respond in the same language as the user's prompt, e.g. Russian):
|
|
194
|
+
|
|
195
|
+
### 1. 🔍 Curator Analysis & Component Breakdown
|
|
196
|
+
- For EACH candidate, provide:
|
|
197
|
+
- ⭐ **Strongest aspects** (e.g. robust architecture, superior UI/CSS design, clean data validation).
|
|
198
|
+
- ⚠️ **Defects or Antipatterns** found from the checklist above.
|
|
199
|
+
- Highlight which candidate provides the best foundation for each component (e.g. Candidate 1 for core logic, Candidate 2 for visual UI).
|
|
200
|
+
|
|
201
|
+
### 2. 🧩 Assembly Recipe & Recommended Master Assembler
|
|
202
|
+
- Recommend the best single agent model to assemble and finalize the solution:
|
|
203
|
+
RECOMMENDED_ASSEMBLER: <number from 1 to N> (<provider:model>)
|
|
204
|
+
- State the machine winner index marker for file promotion:
|
|
205
|
+
WINNER_CANDIDATE_INDEX: <number from 1 to N>
|
|
206
|
+
- Provide the exact blueprint / instructions for combining the best pieces into a unified deliverable.
|
|
207
|
+
|
|
208
|
+
### 3. 🚀 Unified Solution & Execution Guide
|
|
209
|
+
- Present the final synthesized code or complete instructions combining the best candidate features.
|
|
210
|
+
- How to run, verify, and use the deliverable.`
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function buildSynthesisPrompt(userPrompt, referenceOutputs = [], judgeCriteria = '', options = {}) {
|
|
214
|
+
if (options.curatorSynthesis) {
|
|
215
|
+
return buildCuratorSynthesisPrompt(userPrompt, referenceOutputs, judgeCriteria)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const joined = referenceOutputs
|
|
219
|
+
.map((r, i) => {
|
|
220
|
+
const fileSummary = (r.files && r.files.length > 0)
|
|
221
|
+
? ` [Files created: ${r.files.map((f) => f.relativePath).join(', ')}]`
|
|
168
222
|
: ''
|
|
169
|
-
// If 3+ candidates, summarize text to prevent judge context overflow (#13)
|
|
170
223
|
let textContent = r.text
|
|
171
224
|
if (referenceOutputs.length >= 3 && textContent.length > 3000) {
|
|
172
225
|
textContent = stripOrSummarizeCode(textContent)
|
|
@@ -176,7 +229,7 @@ export function buildSynthesisPrompt(userPrompt, referenceOutputs = [], judgeCri
|
|
|
176
229
|
.join('\n\n')
|
|
177
230
|
|
|
178
231
|
const criteriaBlock = judgeCriteria && judgeCriteria.trim()
|
|
179
|
-
? `\n### 🎯
|
|
232
|
+
? `\n### 🎯 Additional evaluation criteria from the user:\n${judgeCriteria.trim()}\n`
|
|
180
233
|
: ''
|
|
181
234
|
|
|
182
235
|
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.
|
|
@@ -187,37 +240,53 @@ ${criteriaBlock}
|
|
|
187
240
|
Reference responses from candidate models:
|
|
188
241
|
${joined}
|
|
189
242
|
|
|
243
|
+
${ANTIPATTERNS_RUBRIC}
|
|
244
|
+
|
|
190
245
|
Instructions:
|
|
191
246
|
Your response MUST be structured into three clear parts (respond in the same language as the user's prompt, e.g. Russian):
|
|
192
247
|
|
|
193
|
-
### 1. ⚖️
|
|
194
|
-
-
|
|
195
|
-
-
|
|
196
|
-
WINNER_CANDIDATE_INDEX:
|
|
197
|
-
-
|
|
248
|
+
### 1. ⚖️ Judge verdict and comparative analysis
|
|
249
|
+
- **Winner**: clearly name the model and candidate number (e.g. "Winner: Candidate 1 (opencode-go:deepseek-v4-flash)" or "Reference 1 (label) is chosen").
|
|
250
|
+
- Always add the machine winner-selection marker:
|
|
251
|
+
WINNER_CANDIDATE_INDEX: <number from 1 to N>
|
|
252
|
+
- **Why this choice**: compare code, architecture, strengths, weaknesses and reliability of all candidates in detail.
|
|
198
253
|
|
|
199
|
-
### 2. 📁
|
|
200
|
-
-
|
|
254
|
+
### 2. 📁 Project files created
|
|
255
|
+
- List the winner files promoted to the project root and their purpose.
|
|
201
256
|
|
|
202
|
-
### 3. 🚀
|
|
203
|
-
-
|
|
257
|
+
### 3. 🚀 How to run and use
|
|
258
|
+
- Describe how to open and run the created project.`
|
|
204
259
|
}
|
|
205
260
|
|
|
206
|
-
export function parseWinnerIndex(judgeText, defaultIndex = 1) {
|
|
261
|
+
export function parseWinnerIndex(judgeText, defaultIndex = 1, candidateCount = Infinity) {
|
|
207
262
|
if (!judgeText || typeof judgeText !== 'string') return defaultIndex
|
|
263
|
+
const inRange = (idx) => !isNaN(idx) && idx >= 1 && idx <= candidateCount
|
|
208
264
|
const match = /WINNER_CANDIDATE_INDEX:\s*(\d+)/i.exec(judgeText)
|
|
209
265
|
if (match) {
|
|
210
266
|
const idx = parseInt(match[1], 10)
|
|
211
|
-
if (
|
|
267
|
+
if (inRange(idx)) return idx
|
|
212
268
|
}
|
|
213
|
-
const candMatch = /(?:Кандидат|Reference)\s*(\d+)\b/i.exec(judgeText)
|
|
269
|
+
const candMatch = /(?:Кандидат|Candidate|Reference)\s*(\d+)\b/i.exec(judgeText)
|
|
214
270
|
if (candMatch) {
|
|
215
271
|
const idx = parseInt(candMatch[1], 10)
|
|
216
|
-
if (
|
|
272
|
+
if (inRange(idx)) return idx
|
|
217
273
|
}
|
|
218
274
|
return defaultIndex
|
|
219
275
|
}
|
|
220
276
|
|
|
277
|
+
export function parseRecommendedAssembler(judgeText, defaultIndex = 1, candidateCount = Infinity) {
|
|
278
|
+
if (!judgeText || typeof judgeText !== 'string') return { index: defaultIndex, label: '' }
|
|
279
|
+
const inRange = (idx) => !isNaN(idx) && idx >= 1 && idx <= candidateCount
|
|
280
|
+
const match = /RECOMMENDED_ASSEMBLER:\s*(\d+)(?:\s*\(([^)]+)\))?/i.exec(judgeText)
|
|
281
|
+
if (match) {
|
|
282
|
+
const idx = parseInt(match[1], 10)
|
|
283
|
+
if (inRange(idx)) {
|
|
284
|
+
return { index: idx, label: match[2]?.trim() || '' }
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return { index: defaultIndex, label: '' }
|
|
288
|
+
}
|
|
289
|
+
|
|
221
290
|
export function parseMoACommand(text, presets = []) {
|
|
222
291
|
if (typeof text !== 'string' || !text.startsWith('/moa')) {
|
|
223
292
|
return null
|
|
@@ -258,7 +327,28 @@ export function parseMoACommand(text, presets = []) {
|
|
|
258
327
|
}
|
|
259
328
|
|
|
260
329
|
/**
|
|
261
|
-
*
|
|
330
|
+
* Invokes LLM call with transient retry for recoverable network/rate-limit errors.
|
|
331
|
+
*/
|
|
332
|
+
export async function callWithTransientRetry(callLlmFn, callArgs, maxRetries = 0, retryDelayMs = 1200) {
|
|
333
|
+
let attempt = 0
|
|
334
|
+
while (true) {
|
|
335
|
+
try {
|
|
336
|
+
return await callLlmFn(callArgs)
|
|
337
|
+
} catch (err) {
|
|
338
|
+
attempt++
|
|
339
|
+
const msg = err?.message || String(err)
|
|
340
|
+
const isTransient = /429|rate limit|502|503|504|econnreset|etimedout|socket hang up/i.test(msg)
|
|
341
|
+
if (attempt <= maxRetries && isTransient) {
|
|
342
|
+
await new Promise((r) => setTimeout(r, retryDelayMs))
|
|
343
|
+
continue
|
|
344
|
+
}
|
|
345
|
+
throw err
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Dispatches queries to all reference models in parallel with transient retry and quorum straggler mitigation.
|
|
262
352
|
*/
|
|
263
353
|
export async function runReferencesParallel(references, messages, options = {}, callLlm, onProgress) {
|
|
264
354
|
if (!Array.isArray(references) || references.length === 0) {
|
|
@@ -267,338 +357,535 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
267
357
|
|
|
268
358
|
const advisoryMessages = cleanAdvisoryMessages(messages)
|
|
269
359
|
const systemPrompt = options.systemPrompt || REFERENCE_SYSTEM_PROMPT
|
|
360
|
+
// Deterministic prefix for optimal prompt caching hit rate (#2.3)
|
|
270
361
|
const fullMessages = [{ role: 'system', content: systemPrompt }, ...advisoryMessages]
|
|
271
362
|
const timeoutMs = options.timeoutMs ?? 75000
|
|
363
|
+
const maxRetries = options.maxRetries ?? 0
|
|
364
|
+
const quorumEnabled = Boolean(options.quorumEnabled)
|
|
365
|
+
const gracePeriodMs = (options.gracePeriodSec ?? 10) * 1000
|
|
366
|
+
|
|
367
|
+
const total = references.length
|
|
368
|
+
let finishedCount = 0
|
|
369
|
+
const results = new Array(total)
|
|
370
|
+
const abortControllers = references.map(() => new AbortController())
|
|
371
|
+
|
|
372
|
+
let onTaskFinished = null
|
|
373
|
+
const notifyFinished = () => {
|
|
374
|
+
if (typeof onTaskFinished === 'function') onTaskFinished()
|
|
375
|
+
}
|
|
272
376
|
|
|
273
|
-
|
|
377
|
+
if (typeof onProgress === 'function') {
|
|
378
|
+
onProgress(`⚡ *Launching ${total} candidate models in parallel...*\n`)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
references.forEach((slot, i) => {
|
|
274
382
|
const label = slotLabel(slot)
|
|
275
|
-
if (typeof onProgress === 'function') {
|
|
276
|
-
onProgress(`⚡ *Кандидат ${i + 1} (${label}) начал генерацию...*\n`)
|
|
277
|
-
}
|
|
278
383
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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)
|
|
384
|
+
const runOne = async () => {
|
|
385
|
+
try {
|
|
386
|
+
const callPromise = callWithTransientRetry(
|
|
387
|
+
callLlm,
|
|
388
|
+
{
|
|
389
|
+
provider: slot.provider,
|
|
390
|
+
model: slot.model,
|
|
391
|
+
messages: fullMessages,
|
|
392
|
+
temperature: options.temperature ?? 0.6,
|
|
393
|
+
maxTokens: options.maxTokens ?? 4096,
|
|
394
|
+
signal: abortControllers[i].signal,
|
|
395
|
+
},
|
|
396
|
+
maxRetries
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
400
|
+
const timer = setTimeout(() => reject(new Error(`Timeout after ${timeoutMs / 1000}s`)), timeoutMs)
|
|
401
|
+
timer.unref?.()
|
|
402
|
+
})
|
|
302
403
|
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
onProgress(`✅ *Кандидат ${i + 1} (${label}) завершил ответ${fileMsg}.*\n`)
|
|
306
|
-
}
|
|
404
|
+
const res = await Promise.race([callPromise, timeoutPromise])
|
|
405
|
+
const text = typeof res === 'string' ? res : (res?.content || res?.text || '')
|
|
307
406
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
407
|
+
const fallbackUsage = (typeof res === 'object' && res?.usage) ? res.usage : {
|
|
408
|
+
inputTokens: Math.max(1, Math.round(fullMessages.map((m) => m.content).join('').length / 4)),
|
|
409
|
+
outputTokens: Math.max(1, Math.round(text.length / 4)),
|
|
410
|
+
}
|
|
411
|
+
const costInfo = estimateTokenCost(slot, fallbackUsage, options.prices)
|
|
412
|
+
|
|
413
|
+
finishedCount++
|
|
414
|
+
if (typeof onProgress === 'function') {
|
|
415
|
+
const costStr = costInfo.costUsd > 0 ? ` (~\${costInfo.costUsd.toFixed(4)})` : ''
|
|
416
|
+
onProgress(`✅ *Candidate ${i + 1}/${total} (${label}) finished${costStr}*\n`)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
results[i] = {
|
|
420
|
+
index: i + 1,
|
|
421
|
+
slot,
|
|
422
|
+
label,
|
|
423
|
+
text,
|
|
424
|
+
usage: costInfo,
|
|
425
|
+
costUsd: costInfo.costUsd,
|
|
426
|
+
ok: true,
|
|
427
|
+
}
|
|
428
|
+
} catch (err) {
|
|
429
|
+
finishedCount++
|
|
430
|
+
const errMsg = err?.message || String(err)
|
|
431
|
+
console.warn(`[dsh-moa] Reference ${label} failed:`, errMsg)
|
|
432
|
+
if (typeof onProgress === 'function') {
|
|
433
|
+
onProgress(`⚠️ *Candidate ${i + 1}/${total} (${label}) error: ${errMsg}*\n`)
|
|
434
|
+
}
|
|
435
|
+
results[i] = {
|
|
436
|
+
index: i + 1,
|
|
437
|
+
slot,
|
|
438
|
+
label,
|
|
439
|
+
text: `[Model ${label} error: ${errMsg}]`,
|
|
440
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
441
|
+
costUsd: 0,
|
|
442
|
+
ok: false,
|
|
443
|
+
error: errMsg,
|
|
444
|
+
}
|
|
445
|
+
} finally {
|
|
446
|
+
notifyFinished()
|
|
318
447
|
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
runOne()
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
// Quorum straggler mitigation: exit as soon as grace period expires
|
|
454
|
+
if (quorumEnabled && total >= 3) {
|
|
455
|
+
const quorumTarget = Math.max(2, Math.min(total - 1, Math.ceil(total * 0.6)))
|
|
456
|
+
let graceTimer = null
|
|
457
|
+
let quorumTriggered = false
|
|
458
|
+
|
|
459
|
+
await new Promise((resolve) => {
|
|
460
|
+
const checkQuorum = () => {
|
|
461
|
+
if (finishedCount >= total) {
|
|
462
|
+
if (graceTimer) clearTimeout(graceTimer)
|
|
463
|
+
return resolve()
|
|
464
|
+
}
|
|
465
|
+
if (finishedCount >= quorumTarget && !quorumTriggered) {
|
|
466
|
+
quorumTriggered = true
|
|
467
|
+
if (typeof onProgress === 'function') {
|
|
468
|
+
onProgress(`⏳ *Quorum reached (${finishedCount}/${total}). Grace period ${gracePeriodMs / 1000}s for remaining models...*\n`)
|
|
469
|
+
}
|
|
470
|
+
graceTimer = setTimeout(() => {
|
|
471
|
+
if (typeof onProgress === 'function' && finishedCount < total) {
|
|
472
|
+
onProgress(`⏩ *Grace period expired. Proceeding with ${finishedCount}/${total} ready candidates.*\n`)
|
|
473
|
+
}
|
|
474
|
+
for (let k = 0; k < total; k++) {
|
|
475
|
+
if (!results[k]) {
|
|
476
|
+
try { abortControllers[k].abort(new Error('Quorum grace period timed out')) } catch {}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
resolve()
|
|
480
|
+
}, gracePeriodMs)
|
|
481
|
+
// active timer keeps event loop alive
|
|
482
|
+
}
|
|
323
483
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
484
|
+
|
|
485
|
+
onTaskFinished = checkQuorum
|
|
486
|
+
checkQuorum()
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
for (let i = 0; i < total; i++) {
|
|
490
|
+
if (!results[i]) {
|
|
491
|
+
const slot = references[i]
|
|
492
|
+
const label = slotLabel(slot)
|
|
493
|
+
results[i] = {
|
|
494
|
+
index: i + 1,
|
|
495
|
+
slot,
|
|
496
|
+
label,
|
|
497
|
+
text: `[Model ${label} timed out (quorum grace period exceeded)]`,
|
|
498
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
499
|
+
costUsd: 0,
|
|
500
|
+
ok: false,
|
|
501
|
+
error: 'Quorum grace period timed out',
|
|
502
|
+
}
|
|
334
503
|
}
|
|
335
504
|
}
|
|
505
|
+
return results
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Standard mode: wait for all tasks to complete
|
|
509
|
+
await new Promise((resolve) => {
|
|
510
|
+
const checkAll = () => {
|
|
511
|
+
if (finishedCount >= total) resolve()
|
|
512
|
+
}
|
|
513
|
+
onTaskFinished = checkAll
|
|
514
|
+
checkAll()
|
|
336
515
|
})
|
|
337
516
|
|
|
338
|
-
return
|
|
517
|
+
return results
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function candidatesForHistory(referenceOutputs) {
|
|
521
|
+
return (referenceOutputs || []).map((r) => ({
|
|
522
|
+
provider: r.slot?.provider || '',
|
|
523
|
+
model: r.slot?.model || '',
|
|
524
|
+
files: (r.files || []).map((f) => f.relativePath),
|
|
525
|
+
usage: r.usage || { inputTokens: 0, outputTokens: 0 },
|
|
526
|
+
costUsd: r.costUsd || 0,
|
|
527
|
+
}))
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async function createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs) {
|
|
531
|
+
if (!liveCanvas || typeof liveCanvas.createPreviewFromContent !== 'function') return null
|
|
532
|
+
const htmlRel = (promotedFiles || []).find((f) => f.endsWith('.html') || f.endsWith('.htm'))
|
|
533
|
+
if (!htmlRel) return null
|
|
534
|
+
let content = null
|
|
535
|
+
for (const r of referenceOutputs || []) {
|
|
536
|
+
const block = (r?.files || []).find((f) => f.relativePath === htmlRel)
|
|
537
|
+
if (block?.content) {
|
|
538
|
+
content = block.content
|
|
539
|
+
break
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (!content) return null
|
|
543
|
+
try {
|
|
544
|
+
return await liveCanvas.createPreviewFromContent({ content, title: htmlRel, filePath: path.join(cwd, htmlRel) })
|
|
545
|
+
} catch {
|
|
546
|
+
return null
|
|
547
|
+
}
|
|
339
548
|
}
|
|
340
549
|
|
|
341
550
|
/**
|
|
342
|
-
*
|
|
343
|
-
* 1. Checks if questions or refinement needed
|
|
344
|
-
* 2. Parallel candidate proposers write to .moa/candidate-X/
|
|
345
|
-
* 3. Aggregator judges and picks winner (or bypassed in Fast Mode)
|
|
346
|
-
* 4. Promotes winner files, records history and calculates costs
|
|
551
|
+
* Main MoA pipeline runner with Curator Synthesis, Fallback Chains, and Live Token Streaming.
|
|
347
552
|
*/
|
|
348
553
|
export async function runMoAPipeline({
|
|
349
554
|
userPrompt,
|
|
350
555
|
messages = [],
|
|
351
556
|
preset,
|
|
352
557
|
callLlm,
|
|
353
|
-
cwd,
|
|
558
|
+
cwd = process.cwd(),
|
|
354
559
|
onProgress,
|
|
355
|
-
|
|
560
|
+
onStreamDelta,
|
|
356
561
|
prices = {},
|
|
562
|
+
historyFilePath,
|
|
563
|
+
liveCanvas = null,
|
|
357
564
|
}) {
|
|
358
|
-
|
|
359
|
-
|
|
565
|
+
const startTime = Date.now()
|
|
566
|
+
const referenceModels = preset?.reference_models || [
|
|
567
|
+
{ provider: 'opencode-go', model: 'deepseek-v4-flash' },
|
|
568
|
+
{ provider: 'grok', model: 'grok-build-0.1' },
|
|
569
|
+
]
|
|
570
|
+
const primaryJudge = preset?.aggregator || { provider: 'codex', model: 'gpt-5.6-sol' }
|
|
571
|
+
const fallbackJudges = Array.isArray(preset?.aggregator_fallbacks) ? preset.aggregator_fallbacks : []
|
|
572
|
+
const judgesChain = [primaryJudge, ...fallbackJudges]
|
|
573
|
+
|
|
574
|
+
const refTemp = preset?.reference_temperature ?? 0.6
|
|
575
|
+
const aggTemp = preset?.aggregator_temperature ?? 0.4
|
|
576
|
+
const maxTokens = preset?.max_tokens ?? 4096
|
|
577
|
+
const judgeCriteria = preset?.judge_criteria ?? ''
|
|
578
|
+
const isFastMode = referenceModels.length === 1
|
|
579
|
+
const isCuratorSynthesis = Boolean(preset?.curator_synthesis)
|
|
580
|
+
const isStreamAggregator = preset?.stream_aggregator !== false
|
|
581
|
+
const isQuorumEnabled = Boolean(preset?.quorum_enabled)
|
|
582
|
+
const gracePeriodSec = preset?.grace_period_sec ?? 10
|
|
583
|
+
const candidateRetries = preset?.retry_count ?? 1
|
|
584
|
+
|
|
585
|
+
// 1. Collect current project workspace context
|
|
586
|
+
if (typeof onProgress === 'function') {
|
|
587
|
+
onProgress('🔍 *Scanning project context...*\n')
|
|
360
588
|
}
|
|
589
|
+
const projectContext = await collectProjectContext(cwd)
|
|
590
|
+
const isRefinement = isRefinementTask(userPrompt, projectContext.files)
|
|
361
591
|
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const maxTokens = preset?.max_tokens ?? preset?.maxTokens ?? 4096
|
|
368
|
-
const judgeCriteria = preset?.judge_criteria || preset?.judgeCriteria || ''
|
|
369
|
-
const workDir = cwd || process.cwd()
|
|
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
|
|
592
|
+
// 2. Check if broad prompt requires user questionnaire
|
|
593
|
+
const askQuestions = preset?.ask_clarifying_questions !== false
|
|
594
|
+
const needsQuestions = !isFastMode && askQuestions && !isRefinement && isBroadPromptRequiringQuestions(userPrompt, messages)
|
|
595
|
+
|
|
596
|
+
// 3. Build enriched prompt for candidates
|
|
376
597
|
let candidateSystemPrompt = REFERENCE_SYSTEM_PROMPT
|
|
377
|
-
if (isRefinement &&
|
|
378
|
-
const fileList =
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
onProgress(`🔄 *[Refinement Mode]: Обнаружен существующий проект (${projectCtx.files.length} файлов). Кандидаты вносят точечные изменения...*\n\n`)
|
|
382
|
-
}
|
|
598
|
+
if (isRefinement && projectContext.files.length > 0) {
|
|
599
|
+
const fileList = projectContext.files.map((f) => `- \`${f.relativePath}\` (${f.content.length} chars)`).join('\n')
|
|
600
|
+
const fileContents = projectContext.files.map((f) => `### File: ${f.relativePath}\n\`\`\`\n${f.content}\n\`\`\``).join('\n\n')
|
|
601
|
+
candidateSystemPrompt += `\n\nExisting project structure:\n${fileList}\n\nProject files:\n${fileContents}\n\nYou are modifying an existing project. Output modified or new files with explicit file paths.`
|
|
383
602
|
}
|
|
384
603
|
|
|
385
|
-
|
|
386
|
-
const allowQuestions = preset?.ask_clarifying_questions !== false && preset?.askClarifyingQuestions !== false
|
|
387
|
-
const needsQuestions = allowQuestions && !skipQuestions && !isRefinement && isBroadPromptRequiringQuestions(userPrompt, messages)
|
|
604
|
+
let promptForCandidates = userPrompt
|
|
388
605
|
if (needsQuestions) {
|
|
389
|
-
|
|
390
|
-
onProgress('🔍 *Задача общего характера. Советники формируют ключевые развилки...*\n\n')
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const questionOutputs = await runReferencesParallel(
|
|
394
|
-
referenceModels,
|
|
395
|
-
[...messages, { role: 'user', content: userPrompt }],
|
|
396
|
-
{ systemPrompt: ADVISOR_QUESTION_PROMPT, referenceTemperature: 0.5, maxTokens: 1024 },
|
|
397
|
-
callLlm,
|
|
398
|
-
onProgress,
|
|
399
|
-
)
|
|
400
|
-
|
|
401
|
-
if (typeof onProgress === 'function') {
|
|
402
|
-
onProgress(`\n⚖️ *Судья (${slotLabel(aggregator)}) синтезирует единый опросник для вас...*\n\n`)
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const qSynthPrompt = buildQuestionSynthesisPrompt(userPrompt, questionOutputs)
|
|
406
|
-
let questionsText = ''
|
|
407
|
-
try {
|
|
408
|
-
const qPromise = callLlm({
|
|
409
|
-
provider: aggregator.provider,
|
|
410
|
-
model: aggregator.model,
|
|
411
|
-
messages: [{ role: 'user', content: qSynthPrompt }],
|
|
412
|
-
temperature: 0.3,
|
|
413
|
-
maxTokens: 1500,
|
|
414
|
-
})
|
|
415
|
-
const qTimeout = new Promise((_, reject) =>
|
|
416
|
-
setTimeout(() => reject(new Error('Timeout waiting for judge questions synthesis')), 60000).unref()
|
|
417
|
-
)
|
|
418
|
-
const res = await Promise.race([qPromise, qTimeout])
|
|
419
|
-
questionsText = (typeof res === 'string' ? res : (res?.content || res?.text || '')).trim()
|
|
420
|
-
} catch (err) {
|
|
421
|
-
questionsText = `Не удалось сформировать вопросы судьи: ${err?.message || String(err)}`
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
return {
|
|
425
|
-
kind: 'questions',
|
|
426
|
-
content: questionsText,
|
|
427
|
-
aggregator: slotLabel(aggregator),
|
|
428
|
-
references: questionOutputs,
|
|
429
|
-
presetName: preset?.name || 'default',
|
|
430
|
-
}
|
|
606
|
+
promptForCandidates += "\n(Note: the task is broad. Propose the key architectural and functional decision points to clarify the user's requirements.)"
|
|
431
607
|
}
|
|
432
608
|
|
|
433
|
-
|
|
434
|
-
const isFastMode = referenceModels.length === 1
|
|
435
|
-
|
|
436
|
-
// Phase 2: Parallel execution & file generation
|
|
437
|
-
if (typeof onProgress === 'function') {
|
|
438
|
-
const modeLabel = isFastMode ? '⚡ Fast Mode' : `советниками (${referenceModels.length})`
|
|
439
|
-
onProgress(`🚀 *Запускаю реализацию ${modeLabel}...*\n\n`)
|
|
440
|
-
}
|
|
609
|
+
const enrichedMessages = [...messages, { role: 'user', content: promptForCandidates }]
|
|
441
610
|
|
|
611
|
+
// 4. Parallel fan-out to candidate models with quorum & transient retry
|
|
442
612
|
const referenceOutputs = await runReferencesParallel(
|
|
443
613
|
referenceModels,
|
|
444
|
-
|
|
445
|
-
{
|
|
614
|
+
enrichedMessages,
|
|
615
|
+
{
|
|
616
|
+
systemPrompt: candidateSystemPrompt,
|
|
617
|
+
temperature: refTemp,
|
|
618
|
+
maxTokens,
|
|
619
|
+
prices,
|
|
620
|
+
quorumEnabled: isQuorumEnabled,
|
|
621
|
+
gracePeriodSec,
|
|
622
|
+
maxRetries: candidateRetries,
|
|
623
|
+
},
|
|
446
624
|
callLlm,
|
|
447
|
-
onProgress
|
|
625
|
+
onProgress
|
|
448
626
|
)
|
|
449
627
|
|
|
450
|
-
//
|
|
451
|
-
const
|
|
452
|
-
if (
|
|
453
|
-
const
|
|
454
|
-
.map((r, i) => `• Кандидат ${i + 1} (${r.label}): ${r.text}`)
|
|
455
|
-
.join('\n')
|
|
456
|
-
const failMessage = `⚠️ **Все модели-советники (${referenceOutputs.length}) завершились с ошибкой**.\n\nСудья не вызывался для предотвращения бессмысленного расхода токенов.\n\n### Детали ошибок:\n${errorDetails}`
|
|
457
|
-
|
|
458
|
-
await cleanMoaWorkspaces(workDir)
|
|
459
|
-
|
|
628
|
+
// Fail fast if all candidates failed
|
|
629
|
+
const successfulRefs = referenceOutputs.filter((r) => r.ok)
|
|
630
|
+
if (successfulRefs.length === 0) {
|
|
631
|
+
const reasons = referenceOutputs.map((r) => `${r.label}: ${r.error || 'unknown error'}`).join('; ')
|
|
460
632
|
return {
|
|
461
633
|
kind: 'failure',
|
|
462
|
-
content:
|
|
463
|
-
aggregator: slotLabel(
|
|
634
|
+
content: `⚠️ All advisor models (${referenceOutputs.length}) failed: ${reasons}`,
|
|
635
|
+
aggregator: slotLabel(primaryJudge),
|
|
464
636
|
references: referenceOutputs,
|
|
465
637
|
presetName: preset?.name || 'default',
|
|
638
|
+
isRefinement,
|
|
639
|
+
isFastMode,
|
|
640
|
+
winningIndex: 0,
|
|
641
|
+
winnerModel: 'none',
|
|
466
642
|
promotedFiles: [],
|
|
467
643
|
usage: {
|
|
468
644
|
totalTokens: 0,
|
|
469
645
|
totalCostUsd: 0,
|
|
470
|
-
candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd:
|
|
646
|
+
candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
|
|
471
647
|
aggregator: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
472
648
|
},
|
|
473
649
|
durationMs: Date.now() - startTime,
|
|
474
650
|
}
|
|
475
651
|
}
|
|
476
|
-
|
|
652
|
+
|
|
653
|
+
// 5. Extract file blocks & write candidate workspaces
|
|
477
654
|
for (let i = 0; i < referenceOutputs.length; i++) {
|
|
478
655
|
const ref = referenceOutputs[i]
|
|
479
|
-
if (ref.ok
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
656
|
+
if (!ref.ok) continue
|
|
657
|
+
const files = extractFileBlocks(ref.text)
|
|
658
|
+
ref.files = files
|
|
659
|
+
if (files.length > 0) {
|
|
660
|
+
await writeCandidateWorkspace(cwd, i + 1, files)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// 6. Questionnaire synthesis branch
|
|
665
|
+
if (needsQuestions) {
|
|
666
|
+
if (typeof onProgress === 'function') {
|
|
667
|
+
onProgress('📋 *Judge synthesizes the clarification questionnaire...*\n')
|
|
668
|
+
}
|
|
669
|
+
const questionPrompt = buildQuestionSynthesisPrompt(userPrompt, referenceOutputs)
|
|
670
|
+
let questionsContent = ''
|
|
671
|
+
let qUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
|
|
672
|
+
try {
|
|
673
|
+
const qRes = await callWithTransientRetry(callLlm, {
|
|
674
|
+
provider: primaryJudge.provider,
|
|
675
|
+
model: primaryJudge.model,
|
|
676
|
+
messages: [{ role: 'user', content: questionPrompt }],
|
|
677
|
+
temperature: 0.3,
|
|
678
|
+
maxTokens: 2048,
|
|
679
|
+
}, 1)
|
|
680
|
+
questionsContent = typeof qRes === 'string' ? qRes : (qRes?.content || qRes?.text || '')
|
|
681
|
+
const qFallback = (typeof qRes === 'object' && qRes?.usage) ? qRes.usage : {
|
|
682
|
+
inputTokens: Math.max(1, Math.round(questionPrompt.length / 4)),
|
|
683
|
+
outputTokens: Math.max(1, Math.round(questionsContent.length / 4)),
|
|
487
684
|
}
|
|
685
|
+
qUsage = estimateTokenCost(primaryJudge, qFallback, prices)
|
|
686
|
+
} catch {
|
|
687
|
+
questionsContent = '### Project requirements clarification\nPlease specify the implementation details and the desired stack.'
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
await cleanMoaWorkspaces(cwd)
|
|
691
|
+
|
|
692
|
+
const qTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0) + qUsage.totalTokens
|
|
693
|
+
const qCostUsd = Number((referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + qUsage.costUsd).toFixed(5))
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
recordMoaRun({
|
|
697
|
+
prompt: userPrompt,
|
|
698
|
+
preset: preset?.name || 'default',
|
|
699
|
+
isRefinement,
|
|
700
|
+
candidates: candidatesForHistory(referenceOutputs),
|
|
701
|
+
aggregator: null,
|
|
702
|
+
winnerIndex: -1,
|
|
703
|
+
winnerModel: '',
|
|
704
|
+
promotedFiles: [],
|
|
705
|
+
totalTokens: qTokens,
|
|
706
|
+
totalCostUsd: qCostUsd,
|
|
707
|
+
durationMs: Date.now() - startTime,
|
|
708
|
+
}, historyFilePath)
|
|
709
|
+
} catch (histErr) {
|
|
710
|
+
console.warn('[dsh-moa] Failed to record questionnaire run in history:', histErr)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return {
|
|
714
|
+
kind: 'questions',
|
|
715
|
+
content: questionsContent,
|
|
716
|
+
references: referenceOutputs,
|
|
717
|
+
presetName: preset?.name || 'default',
|
|
718
|
+
isRefinement: false,
|
|
488
719
|
}
|
|
489
720
|
}
|
|
490
721
|
|
|
722
|
+
// 7. Fast mode bypass for single candidate
|
|
723
|
+
if (isFastMode) {
|
|
724
|
+
const single = referenceOutputs[0]
|
|
725
|
+
let promotedFiles = []
|
|
726
|
+
if (single.files && single.files.length > 0) {
|
|
727
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, 1)
|
|
728
|
+
} else {
|
|
729
|
+
await cleanMoaWorkspaces(cwd)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const totalTokens = single.usage?.totalTokens || 0
|
|
733
|
+
const totalCostUsd = single.costUsd || 0
|
|
734
|
+
const durationMs = Date.now() - startTime
|
|
735
|
+
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs)
|
|
736
|
+
|
|
737
|
+
try {
|
|
738
|
+
recordMoaRun({
|
|
739
|
+
prompt: userPrompt,
|
|
740
|
+
preset: preset?.name || 'default',
|
|
741
|
+
isRefinement,
|
|
742
|
+
isFastMode: true,
|
|
743
|
+
candidates: candidatesForHistory(referenceOutputs),
|
|
744
|
+
aggregator: null,
|
|
745
|
+
winnerIndex: 1,
|
|
746
|
+
winnerModel: single.label,
|
|
747
|
+
promotedFiles,
|
|
748
|
+
totalTokens,
|
|
749
|
+
totalCostUsd,
|
|
750
|
+
durationMs,
|
|
751
|
+
}, historyFilePath)
|
|
752
|
+
} catch (histErr) {
|
|
753
|
+
console.warn('[dsh-moa] Failed to record fast-mode run in history:', histErr)
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return {
|
|
757
|
+
kind: 'synthesis',
|
|
758
|
+
content: single.text,
|
|
759
|
+
aggregator: 'Fast Mode (Direct)',
|
|
760
|
+
references: referenceOutputs,
|
|
761
|
+
presetName: preset?.name || 'default',
|
|
762
|
+
isRefinement,
|
|
763
|
+
isFastMode: true,
|
|
764
|
+
winningIndex: 1,
|
|
765
|
+
winnerModel: single.label,
|
|
766
|
+
promotedFiles,
|
|
767
|
+
...(livePreview ? { liveCanvas: livePreview } : {}),
|
|
768
|
+
usage: {
|
|
769
|
+
totalTokens,
|
|
770
|
+
totalCostUsd,
|
|
771
|
+
candidates: [{ label: single.label, usage: single.usage, costUsd: single.costUsd }],
|
|
772
|
+
aggregator: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
773
|
+
},
|
|
774
|
+
durationMs,
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// 8. Aggregator / Judge synthesis with fallback chain (#3.3) & streaming (#2.1)
|
|
779
|
+
const synthesisPrompt = buildSynthesisPrompt(
|
|
780
|
+
userPrompt,
|
|
781
|
+
referenceOutputs,
|
|
782
|
+
judgeCriteria,
|
|
783
|
+
{ curatorSynthesis: isCuratorSynthesis }
|
|
784
|
+
)
|
|
785
|
+
|
|
491
786
|
let synthesizedText = ''
|
|
492
|
-
let winningIndex = 1
|
|
493
787
|
let aggUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
|
|
494
|
-
|
|
788
|
+
let chosenJudge = primaryJudge
|
|
789
|
+
let judgeSuccess = false
|
|
790
|
+
let lastJudgeError = null
|
|
791
|
+
|
|
792
|
+
for (let jIdx = 0; jIdx < judgesChain.length; jIdx++) {
|
|
793
|
+
const currentJudge = judgesChain[jIdx]
|
|
794
|
+
const currentLabel = slotLabel(currentJudge)
|
|
495
795
|
|
|
496
|
-
if (isFastMode) {
|
|
497
|
-
// Fast mode: promote candidate 1 directly without judge
|
|
498
|
-
synthesizedText = referenceOutputs[0]?.text || '(empty fast response)'
|
|
499
|
-
winningIndex = 1
|
|
500
|
-
} else {
|
|
501
|
-
// Phase 3: Aggregator evaluation
|
|
502
796
|
if (typeof onProgress === 'function') {
|
|
503
|
-
|
|
797
|
+
const judgeTitle = isCuratorSynthesis ? 'Lead Curator' : 'Judge'
|
|
798
|
+
const fallbackBadge = jIdx > 0 ? ` (Fallback #${jIdx})` : ''
|
|
799
|
+
onProgress(`⚖️ *${judgeTitle} (${currentLabel})${fallbackBadge} evaluates candidates and synthesizes the solution...*\n`)
|
|
504
800
|
}
|
|
505
801
|
|
|
506
|
-
const synthPrompt = buildSynthesisPrompt(userPrompt, referenceOutputs, judgeCriteria)
|
|
507
|
-
|
|
508
802
|
try {
|
|
509
|
-
const
|
|
510
|
-
provider:
|
|
511
|
-
model:
|
|
512
|
-
messages: [{ role: 'user', content:
|
|
803
|
+
const aggRes = await callWithTransientRetry(callLlm, {
|
|
804
|
+
provider: currentJudge.provider,
|
|
805
|
+
model: currentJudge.model,
|
|
806
|
+
messages: [{ role: 'user', content: synthesisPrompt }],
|
|
513
807
|
temperature: aggTemp,
|
|
514
808
|
maxTokens,
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
809
|
+
onStreamDelta: (delta) => {
|
|
810
|
+
if (isStreamAggregator && typeof onStreamDelta === 'function') {
|
|
811
|
+
onStreamDelta(delta)
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
}, 0)
|
|
815
|
+
|
|
816
|
+
synthesizedText = typeof aggRes === 'string' ? aggRes : (aggRes?.content || aggRes?.text || '')
|
|
817
|
+
const aggFallbackUsage = (typeof aggRes === 'object' && aggRes?.usage) ? aggRes.usage : {
|
|
818
|
+
inputTokens: Math.max(1, Math.round(synthesisPrompt.length / 4)),
|
|
819
|
+
outputTokens: Math.max(1, Math.round(synthesizedText.length / 4)),
|
|
525
820
|
}
|
|
526
|
-
aggUsage = estimateTokenCost(
|
|
821
|
+
aggUsage = estimateTokenCost(currentJudge, aggFallbackUsage, prices)
|
|
822
|
+
chosenJudge = currentJudge
|
|
823
|
+
judgeSuccess = true
|
|
824
|
+
break
|
|
527
825
|
} catch (err) {
|
|
528
|
-
|
|
529
|
-
|
|
826
|
+
lastJudgeError = err
|
|
827
|
+
console.warn(`[dsh-moa] Judge ${currentLabel} failed:`, err)
|
|
828
|
+
if (typeof onProgress === 'function') {
|
|
829
|
+
const nextJudge = judgesChain[jIdx + 1]
|
|
830
|
+
const nextHint = nextJudge ? ` Trying fallback ${slotLabel(nextJudge)}...` : ''
|
|
831
|
+
onProgress(`⚠️ *Judge ${currentLabel} failed: ${err?.message || err}.${nextHint}*\n`)
|
|
832
|
+
}
|
|
530
833
|
}
|
|
834
|
+
}
|
|
531
835
|
|
|
532
|
-
|
|
836
|
+
if (!judgeSuccess) {
|
|
837
|
+
const firstErr = lastJudgeError ? (lastJudgeError.message || String(lastJudgeError)) : 'unknown error'
|
|
838
|
+
synthesizedText = `⚠️ *[Aggregator error: ${firstErr}. Fallback candidate outputs:]*\n\n` +
|
|
839
|
+
successfulRefs.map((r, i) => `### Candidate ${i + 1} (${r.label})\n${r.text}`).join('\n\n')
|
|
533
840
|
}
|
|
534
841
|
|
|
535
|
-
//
|
|
842
|
+
// 9. Evaluate winner & promote files
|
|
843
|
+
const winningIndex = parseWinnerIndex(synthesizedText, 1, referenceOutputs.length)
|
|
844
|
+
const recommendedAssembler = isCuratorSynthesis
|
|
845
|
+
? parseRecommendedAssembler(synthesizedText, winningIndex, referenceOutputs.length)
|
|
846
|
+
: null
|
|
847
|
+
|
|
848
|
+
// Check if aggregator synthesized unified file blocks directly
|
|
849
|
+
const synthesizedFiles = extractFileBlocks(synthesizedText)
|
|
536
850
|
let promotedFiles = []
|
|
537
|
-
try {
|
|
538
|
-
promotedFiles = await promoteCandidateWorkspace(workDir, winningIndex)
|
|
539
|
-
if (typeof onProgress === 'function' && promotedFiles.length > 0) {
|
|
540
|
-
onProgress(`\n✅ *Файлы победителя (Кандидат ${winningIndex}) перенесены в проект: ${promotedFiles.join(', ')}*\n\n`)
|
|
541
|
-
}
|
|
542
|
-
} catch (promoteErr) {
|
|
543
|
-
console.warn('[dsh-moa] Error promoting candidate files:', promoteErr)
|
|
544
|
-
}
|
|
545
851
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
body: JSON.stringify({ filePath: previewCandidate }),
|
|
557
|
-
signal: AbortSignal.timeout(500),
|
|
558
|
-
})
|
|
559
|
-
if (lcRes.ok) {
|
|
560
|
-
const lcData = await lcRes.json()
|
|
561
|
-
if (lcData?.canvasId) {
|
|
562
|
-
liveCanvas = {
|
|
563
|
-
canvasId: lcData.canvasId,
|
|
564
|
-
title: lcData.title || previewCandidate,
|
|
565
|
-
filePath: previewCandidate,
|
|
566
|
-
previewUrl: lcData.previewUrl || `/dsh-live-canvas/sandbox/${lcData.canvasId}`
|
|
567
|
-
}
|
|
568
|
-
if (typeof onProgress === 'function') {
|
|
569
|
-
onProgress(`\n🎨 *[Live Canvas]: Файл ${previewCandidate} открыт для предпросмотра!*\n\n`)
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
} catch (err) {
|
|
574
|
-
// live-canvas plugin might not be installed or reachable, non-fatal
|
|
575
|
-
}
|
|
576
|
-
}
|
|
852
|
+
if (synthesizedFiles.length > 0) {
|
|
853
|
+
await writeCandidateWorkspace(cwd, 'curator-synthesis', synthesizedFiles)
|
|
854
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, 'curator-synthesis')
|
|
855
|
+
} else if (referenceOutputs[winningIndex - 1]?.files?.length > 0) {
|
|
856
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, winningIndex)
|
|
857
|
+
} else if (successfulRefs[0]?.files?.length > 0) {
|
|
858
|
+
const fallbackIdx = successfulRefs[0].index
|
|
859
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, fallbackIdx)
|
|
860
|
+
} else {
|
|
861
|
+
await cleanMoaWorkspaces(cwd)
|
|
577
862
|
}
|
|
578
863
|
|
|
579
|
-
|
|
864
|
+
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs)
|
|
865
|
+
|
|
580
866
|
const totalTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0) + aggUsage.totalTokens
|
|
581
867
|
const totalCostUsd = Number((referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + aggUsage.costUsd).toFixed(5))
|
|
582
868
|
const durationMs = Date.now() - startTime
|
|
583
869
|
|
|
584
870
|
const winningRef = referenceOutputs[winningIndex - 1]
|
|
585
871
|
const winnerModel = winningRef?.label || slotLabel(referenceModels[0])
|
|
872
|
+
const finalAggLabel = slotLabel(chosenJudge)
|
|
586
873
|
|
|
587
|
-
// Record run to history
|
|
874
|
+
// Record run to history
|
|
588
875
|
try {
|
|
589
876
|
recordMoaRun({
|
|
590
877
|
prompt: userPrompt,
|
|
591
878
|
preset: preset?.name || 'default',
|
|
592
879
|
isRefinement,
|
|
593
|
-
candidates: referenceOutputs,
|
|
594
|
-
aggregator:
|
|
880
|
+
candidates: candidatesForHistory(referenceOutputs),
|
|
881
|
+
aggregator: { provider: chosenJudge.provider, model: chosenJudge.model, usage: aggUsage, costUsd: aggUsage.costUsd },
|
|
595
882
|
winnerIndex: winningIndex,
|
|
596
883
|
winnerModel,
|
|
597
884
|
promotedFiles,
|
|
598
885
|
totalTokens,
|
|
599
886
|
totalCostUsd,
|
|
600
887
|
durationMs,
|
|
601
|
-
})
|
|
888
|
+
}, historyFilePath)
|
|
602
889
|
} catch (histErr) {
|
|
603
890
|
console.warn('[dsh-moa] Failed to record run in history:', histErr)
|
|
604
891
|
}
|
|
@@ -606,19 +893,21 @@ export async function runMoAPipeline({
|
|
|
606
893
|
return {
|
|
607
894
|
kind: 'synthesis',
|
|
608
895
|
content: synthesizedText,
|
|
609
|
-
aggregator: isFastMode ? 'Fast Mode (Direct)' :
|
|
896
|
+
aggregator: isFastMode ? 'Fast Mode (Direct)' : finalAggLabel,
|
|
610
897
|
references: referenceOutputs,
|
|
611
898
|
presetName: preset?.name || 'default',
|
|
612
899
|
isRefinement,
|
|
613
900
|
isFastMode,
|
|
901
|
+
isCuratorSynthesis,
|
|
902
|
+
recommendedAssembler,
|
|
614
903
|
winningIndex,
|
|
615
904
|
winnerModel,
|
|
616
905
|
promotedFiles,
|
|
617
|
-
liveCanvas,
|
|
906
|
+
...(livePreview ? { liveCanvas: livePreview } : {}),
|
|
618
907
|
usage: {
|
|
619
908
|
totalTokens,
|
|
620
909
|
totalCostUsd,
|
|
621
|
-
candidates: referenceOutputs.map(r => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
|
|
910
|
+
candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
|
|
622
911
|
aggregator: aggUsage,
|
|
623
912
|
},
|
|
624
913
|
durationMs,
|
|
@@ -636,8 +925,8 @@ export function stripOrSummarizeCode(text) {
|
|
|
636
925
|
return match
|
|
637
926
|
}
|
|
638
927
|
const fileHint = fileTag || (code.match(/^\s*(?:\/\/|#|<!--|\/\*)\s*(?:file|filepath|path):\s*([^\s*]+)/im)?.[1])
|
|
639
|
-
const label = fileHint ?
|
|
640
|
-
return `\n> 📄 *[${label}
|
|
928
|
+
const label = fileHint ? `file \`${fileHint}\`` : (lang ? `code \`${lang}\`` : 'code')
|
|
929
|
+
return `\n> 📄 *[${label} - ${lines.length} lines saved to disk]*\n`
|
|
641
930
|
})
|
|
642
931
|
}
|
|
643
932
|
|
|
@@ -648,41 +937,53 @@ export function formatMoAResponse({ moaResult, presetName }) {
|
|
|
648
937
|
const refs = moaResult?.references || []
|
|
649
938
|
|
|
650
939
|
if (moaResult?.kind === 'questions') {
|
|
651
|
-
parts.push('## 🧠 Mixture of Agents —
|
|
652
|
-
parts.push(
|
|
940
|
+
parts.push('## 🧠 Mixture of Agents — Requirements Clarification')
|
|
941
|
+
parts.push(`*Judge (${judge}) and the advisors analyzed the task:*\n`)
|
|
942
|
+
parts.push(moaResult.content)
|
|
943
|
+
return parts.join('\n')
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (moaResult?.kind === 'failure') {
|
|
947
|
+
parts.push('## ⚠️ Mixture of Agents — Execution Failed')
|
|
653
948
|
parts.push(moaResult.content)
|
|
654
949
|
return parts.join('\n')
|
|
655
950
|
}
|
|
656
951
|
|
|
657
|
-
const modeBadge = moaResult?.isFastMode
|
|
658
|
-
|
|
952
|
+
const modeBadge = moaResult?.isFastMode
|
|
953
|
+
? '⚡ Fast Mode'
|
|
954
|
+
: (moaResult?.isCuratorSynthesis ? `🧠 Curator: ${judge}` : `Judge: ${judge}`)
|
|
955
|
+
parts.push(`## 🧠 Mixture of Agents (Preset: ${pName} | ${modeBadge})`)
|
|
659
956
|
parts.push('')
|
|
660
957
|
|
|
661
958
|
if (moaResult?.isRefinement) {
|
|
662
|
-
parts.push('> 🔄
|
|
959
|
+
parts.push('> 🔄 **Mode**: Iterative project refinement (Refinement)')
|
|
663
960
|
}
|
|
664
961
|
|
|
665
962
|
const hasPromoted = moaResult?.promotedFiles && moaResult.promotedFiles.length > 0
|
|
666
963
|
if (hasPromoted) {
|
|
667
|
-
parts.push(`> 📦
|
|
964
|
+
parts.push(`> 📦 **Files created in the project**: \`${moaResult.promotedFiles.join('`, `')}\``)
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (moaResult?.recommendedAssembler?.label) {
|
|
968
|
+
parts.push(`> 🎯 **Recommended Master Assembler**: Candidate ${moaResult.recommendedAssembler.index} (\`${moaResult.recommendedAssembler.label}\`)`)
|
|
668
969
|
}
|
|
669
970
|
|
|
670
971
|
if (moaResult?.liveCanvas?.previewUrl) {
|
|
671
972
|
parts.push(`> 🎨 **Live Canvas**: [🚀 Открыть ${moaResult.liveCanvas.title || 'превью'} в Live Canvas](${moaResult.liveCanvas.previewUrl}) | [↗ Открыть в новой вкладке](${moaResult.liveCanvas.previewUrl})`)
|
|
672
973
|
}
|
|
673
974
|
|
|
674
|
-
// Cost tracking card
|
|
975
|
+
// Cost tracking card
|
|
675
976
|
if (moaResult?.usage) {
|
|
676
977
|
const u = moaResult.usage
|
|
677
|
-
const costStr = u.totalCostUsd > 0 ? `~\$${u.totalCostUsd.toFixed(4)}` : '
|
|
978
|
+
const costStr = u.totalCostUsd > 0 ? `~\$${u.totalCostUsd.toFixed(4)}` : 'Free'
|
|
678
979
|
const tokStr = u.totalTokens >= 1000 ? `${(u.totalTokens / 1000).toFixed(1)}k` : `${u.totalTokens}`
|
|
679
|
-
parts.push(`> 💰
|
|
980
|
+
parts.push(`> 💰 **Run cost**: ${costStr} (${tokStr} tokens total)`)
|
|
680
981
|
}
|
|
681
982
|
|
|
682
983
|
parts.push('')
|
|
683
984
|
|
|
684
985
|
if (!moaResult?.isFastMode) {
|
|
685
|
-
parts.push(`### ⚖️
|
|
986
|
+
parts.push(`### ⚖️ Judge verdict and final synthesis (Synthesis: ${judge})`)
|
|
686
987
|
parts.push('')
|
|
687
988
|
const cleanJudgeContent = hasPromoted
|
|
688
989
|
? stripOrSummarizeCode(moaResult?.content || '')
|
|
@@ -692,14 +993,14 @@ export function formatMoAResponse({ moaResult, presetName }) {
|
|
|
692
993
|
}
|
|
693
994
|
|
|
694
995
|
if (Array.isArray(refs) && refs.length > 0) {
|
|
695
|
-
const title = moaResult?.isFastMode ? '### 🚀
|
|
996
|
+
const title = moaResult?.isFastMode ? '### 🚀 Candidate generation result:' : `### 👥 Advisor responses (${refs.length}):`
|
|
696
997
|
parts.push(title)
|
|
697
998
|
parts.push('')
|
|
698
999
|
refs.forEach((ref, i) => {
|
|
699
1000
|
const statusIcon = ref.ok ? '✅' : '⚠️'
|
|
700
1001
|
const fileBadge = ref.files?.length ? ` (${ref.files.length} файл(ов))` : ''
|
|
701
1002
|
const costBadge = ref.costUsd > 0 ? ` [~\$${ref.costUsd.toFixed(4)}]` : ''
|
|
702
|
-
parts.push(`#### ${statusIcon}
|
|
1003
|
+
parts.push(`#### ${statusIcon} Model ${i + 1}: ${ref.label}${fileBadge}${costBadge}`)
|
|
703
1004
|
parts.push('')
|
|
704
1005
|
parts.push(stripOrSummarizeCode(ref.text))
|
|
705
1006
|
parts.push('')
|
|
@@ -711,12 +1012,12 @@ export function formatMoAResponse({ moaResult, presetName }) {
|
|
|
711
1012
|
return parts.join('\n')
|
|
712
1013
|
}
|
|
713
1014
|
|
|
714
|
-
export async function* streamMoATurn({ targetPreset, userPrompt, messages, callLlm, cwd }, options = {}) {
|
|
1015
|
+
export async function* streamMoATurn({ targetPreset, userPrompt, messages, callLlm, cwd, prices, historyFilePath, liveCanvas }, options = {}) {
|
|
715
1016
|
const signal = options?.signal
|
|
716
1017
|
if (signal?.aborted) return
|
|
717
1018
|
|
|
718
1019
|
yield { type: 'block-start', index: 0, blockType: 'text' }
|
|
719
|
-
yield { type: 'text-delta', index: 0, text: '🧠 *Mixture of Agents
|
|
1020
|
+
yield { type: 'text-delta', index: 0, text: '🧠 *Mixture of Agents started...*\n\n' }
|
|
720
1021
|
|
|
721
1022
|
// Async push-queue for zero-latency live delta streaming
|
|
722
1023
|
const queue = []
|
|
@@ -738,6 +1039,12 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
|
|
|
738
1039
|
callLlm,
|
|
739
1040
|
cwd,
|
|
740
1041
|
onProgress: pushUpdate,
|
|
1042
|
+
onStreamDelta: (delta) => {
|
|
1043
|
+
pushUpdate(delta)
|
|
1044
|
+
},
|
|
1045
|
+
prices,
|
|
1046
|
+
historyFilePath,
|
|
1047
|
+
liveCanvas,
|
|
741
1048
|
})
|
|
742
1049
|
.catch((err) => ({ error: err }))
|
|
743
1050
|
.finally(() => {
|
|
@@ -774,7 +1081,7 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
|
|
|
774
1081
|
|
|
775
1082
|
const elapsedSec = Math.floor((Date.now() - startTime) / 1000)
|
|
776
1083
|
if (!done && Date.now() - lastYieldTime >= 3000) {
|
|
777
|
-
yield { type: 'text-delta', index: 0, text: `⏳ *[${elapsedSec}
|
|
1084
|
+
yield { type: 'text-delta', index: 0, text: `⏳ *[${elapsedSec}s] Still processing...*\n` }
|
|
778
1085
|
lastYieldTime = Date.now()
|
|
779
1086
|
}
|
|
780
1087
|
}
|
|
@@ -786,7 +1093,7 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
|
|
|
786
1093
|
}
|
|
787
1094
|
|
|
788
1095
|
if (result?.error) {
|
|
789
|
-
const errText = '\n\n⚠️
|
|
1096
|
+
const errText = '\n\n⚠️ **Mixture of Agents error**: ' + (result.error?.message || String(result.error))
|
|
790
1097
|
yield { type: 'text-delta', index: 0, text: errText }
|
|
791
1098
|
yield { type: 'block-end', index: 0, block: { type: 'text', text: errText } }
|
|
792
1099
|
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
@@ -800,7 +1107,13 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
|
|
|
800
1107
|
|
|
801
1108
|
yield { type: 'text-delta', index: 0, text: '\n---\n\n' + formatted }
|
|
802
1109
|
yield { type: 'block-end', index: 0, block: { type: 'text', text: formatted } }
|
|
803
|
-
yield {
|
|
1110
|
+
yield {
|
|
1111
|
+
type: 'usage',
|
|
1112
|
+
usage: {
|
|
1113
|
+
inputTokens: result?.usage?.totalTokens || 0,
|
|
1114
|
+
outputTokens: Math.round((formatted.length || 0) / 4),
|
|
1115
|
+
},
|
|
1116
|
+
}
|
|
804
1117
|
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
805
1118
|
} finally {
|
|
806
1119
|
if (signal?.aborted) {
|
|
@@ -808,3 +1121,4 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
|
|
|
808
1121
|
}
|
|
809
1122
|
}
|
|
810
1123
|
}
|
|
1124
|
+
|