@goodandready/dsh-moa 0.2.9 → 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 +14 -6
- package/docs/plans/47-client-js-split-plan.md +57 -0
- package/lib/client.js +260 -85
- 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 +528 -177
- package/package.json +1 -1
package/lib/moa-runner.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Mixture of Agents (MoA)
|
|
2
|
+
* DeepSeek Harness Mixture of Agents (MoA) — Runner Engine
|
|
3
3
|
*
|
|
4
|
-
* Implements:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* 7. Real-time cost tracking, token metrics and history logging
|
|
12
|
-
* 8. Zero-latency async push-queue streaming for llm/stream turns
|
|
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
|
|
13
11
|
*/
|
|
14
12
|
|
|
13
|
+
import path from 'node:path'
|
|
15
14
|
import {
|
|
16
15
|
extractFileBlocks,
|
|
17
16
|
collectProjectContext,
|
|
@@ -20,7 +19,6 @@ import {
|
|
|
20
19
|
promoteCandidateWorkspace,
|
|
21
20
|
cleanMoaWorkspaces,
|
|
22
21
|
} from './file-workspace.js'
|
|
23
|
-
|
|
24
22
|
import { estimateTokenCost } from './pricing.js'
|
|
25
23
|
import { recordMoaRun } from './history.js'
|
|
26
24
|
|
|
@@ -39,6 +37,25 @@ When generating files for a project, explicitly specify file paths using fenced
|
|
|
39
37
|
\`\`\`css file="style.css"
|
|
40
38
|
\`\`\``
|
|
41
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.`
|
|
58
|
+
|
|
42
59
|
export function slotLabel(slot) {
|
|
43
60
|
if (!slot) return 'unknown'
|
|
44
61
|
if (typeof slot === 'string') return slot
|
|
@@ -128,28 +145,81 @@ export function isBroadPromptRequiringQuestions(userPrompt = '', messages = [])
|
|
|
128
145
|
|
|
129
146
|
export function buildQuestionSynthesisPrompt(userPrompt, referenceOutputs = []) {
|
|
130
147
|
const joined = referenceOutputs
|
|
131
|
-
.map((r, i) =>
|
|
148
|
+
.map((r, i) => `Advisor ${i + 1} (${r.label}):\n${r.text}`)
|
|
132
149
|
.join('\n\n')
|
|
133
150
|
|
|
134
|
-
return
|
|
135
|
-
|
|
151
|
+
return `You are the lead architect and judge in a Mixture of Agents (MoA) ensemble.
|
|
152
|
+
The user gave the task:
|
|
136
153
|
"${userPrompt}"
|
|
137
154
|
|
|
138
|
-
|
|
155
|
+
The advisors proposed the following decision points and clarifications:
|
|
156
|
+
${joined}
|
|
157
|
+
|
|
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
|
+
}
|
|
162
|
+
|
|
163
|
+
export function buildCuratorSynthesisPrompt(userPrompt, referenceOutputs = [], judgeCriteria = '') {
|
|
164
|
+
const joined = referenceOutputs
|
|
165
|
+
.map((r, i) => {
|
|
166
|
+
const fileSummary = (r.files && r.files.length > 0)
|
|
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:
|
|
139
188
|
${joined}
|
|
140
189
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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.`
|
|
144
211
|
}
|
|
145
212
|
|
|
146
|
-
export function buildSynthesisPrompt(userPrompt, referenceOutputs = [], judgeCriteria = '') {
|
|
213
|
+
export function buildSynthesisPrompt(userPrompt, referenceOutputs = [], judgeCriteria = '', options = {}) {
|
|
214
|
+
if (options.curatorSynthesis) {
|
|
215
|
+
return buildCuratorSynthesisPrompt(userPrompt, referenceOutputs, judgeCriteria)
|
|
216
|
+
}
|
|
217
|
+
|
|
147
218
|
const joined = referenceOutputs
|
|
148
219
|
.map((r, i) => {
|
|
149
220
|
const fileSummary = (r.files && r.files.length > 0)
|
|
150
|
-
? ` [
|
|
221
|
+
? ` [Files created: ${r.files.map((f) => f.relativePath).join(', ')}]`
|
|
151
222
|
: ''
|
|
152
|
-
// If 3+ candidates, summarize text to prevent judge context overflow (#13)
|
|
153
223
|
let textContent = r.text
|
|
154
224
|
if (referenceOutputs.length >= 3 && textContent.length > 3000) {
|
|
155
225
|
textContent = stripOrSummarizeCode(textContent)
|
|
@@ -159,7 +229,7 @@ export function buildSynthesisPrompt(userPrompt, referenceOutputs = [], judgeCri
|
|
|
159
229
|
.join('\n\n')
|
|
160
230
|
|
|
161
231
|
const criteriaBlock = judgeCriteria && judgeCriteria.trim()
|
|
162
|
-
? `\n### 🎯
|
|
232
|
+
? `\n### 🎯 Additional evaluation criteria from the user:\n${judgeCriteria.trim()}\n`
|
|
163
233
|
: ''
|
|
164
234
|
|
|
165
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.
|
|
@@ -170,37 +240,53 @@ ${criteriaBlock}
|
|
|
170
240
|
Reference responses from candidate models:
|
|
171
241
|
${joined}
|
|
172
242
|
|
|
243
|
+
${ANTIPATTERNS_RUBRIC}
|
|
244
|
+
|
|
173
245
|
Instructions:
|
|
174
246
|
Your response MUST be structured into three clear parts (respond in the same language as the user's prompt, e.g. Russian):
|
|
175
247
|
|
|
176
|
-
### 1. ⚖️
|
|
177
|
-
-
|
|
178
|
-
-
|
|
179
|
-
WINNER_CANDIDATE_INDEX:
|
|
180
|
-
-
|
|
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.
|
|
181
253
|
|
|
182
|
-
### 2. 📁
|
|
183
|
-
-
|
|
254
|
+
### 2. 📁 Project files created
|
|
255
|
+
- List the winner files promoted to the project root and their purpose.
|
|
184
256
|
|
|
185
|
-
### 3. 🚀
|
|
186
|
-
-
|
|
257
|
+
### 3. 🚀 How to run and use
|
|
258
|
+
- Describe how to open and run the created project.`
|
|
187
259
|
}
|
|
188
260
|
|
|
189
|
-
export function parseWinnerIndex(judgeText, defaultIndex = 1) {
|
|
261
|
+
export function parseWinnerIndex(judgeText, defaultIndex = 1, candidateCount = Infinity) {
|
|
190
262
|
if (!judgeText || typeof judgeText !== 'string') return defaultIndex
|
|
263
|
+
const inRange = (idx) => !isNaN(idx) && idx >= 1 && idx <= candidateCount
|
|
191
264
|
const match = /WINNER_CANDIDATE_INDEX:\s*(\d+)/i.exec(judgeText)
|
|
192
265
|
if (match) {
|
|
193
266
|
const idx = parseInt(match[1], 10)
|
|
194
|
-
if (
|
|
267
|
+
if (inRange(idx)) return idx
|
|
195
268
|
}
|
|
196
|
-
const candMatch = /(?:Кандидат|Reference)\s*(\d+)\b/i.exec(judgeText)
|
|
269
|
+
const candMatch = /(?:Кандидат|Candidate|Reference)\s*(\d+)\b/i.exec(judgeText)
|
|
197
270
|
if (candMatch) {
|
|
198
271
|
const idx = parseInt(candMatch[1], 10)
|
|
199
|
-
if (
|
|
272
|
+
if (inRange(idx)) return idx
|
|
200
273
|
}
|
|
201
274
|
return defaultIndex
|
|
202
275
|
}
|
|
203
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
|
+
|
|
204
290
|
export function parseMoACommand(text, presets = []) {
|
|
205
291
|
if (typeof text !== 'string' || !text.startsWith('/moa')) {
|
|
206
292
|
return null
|
|
@@ -241,7 +327,28 @@ export function parseMoACommand(text, presets = []) {
|
|
|
241
327
|
}
|
|
242
328
|
|
|
243
329
|
/**
|
|
244
|
-
*
|
|
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.
|
|
245
352
|
*/
|
|
246
353
|
export async function runReferencesParallel(references, messages, options = {}, callLlm, onProgress) {
|
|
247
354
|
if (!Array.isArray(references) || references.length === 0) {
|
|
@@ -250,76 +357,198 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
250
357
|
|
|
251
358
|
const advisoryMessages = cleanAdvisoryMessages(messages)
|
|
252
359
|
const systemPrompt = options.systemPrompt || REFERENCE_SYSTEM_PROMPT
|
|
360
|
+
// Deterministic prefix for optimal prompt caching hit rate (#2.3)
|
|
253
361
|
const fullMessages = [{ role: 'system', content: systemPrompt }, ...advisoryMessages]
|
|
254
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
|
+
}
|
|
255
376
|
|
|
256
|
-
|
|
377
|
+
if (typeof onProgress === 'function') {
|
|
378
|
+
onProgress(`⚡ *Launching ${total} candidate models in parallel...*\n`)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
references.forEach((slot, i) => {
|
|
257
382
|
const label = slotLabel(slot)
|
|
258
|
-
if (typeof onProgress === 'function') {
|
|
259
|
-
onProgress(`⚡ *Кандидат ${i + 1} (${label}) начал генерацию...*\n`)
|
|
260
|
-
}
|
|
261
383
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
+
})
|
|
403
|
+
|
|
404
|
+
const res = await Promise.race([callPromise, timeoutPromise])
|
|
405
|
+
const text = typeof res === 'string' ? res : (res?.content || res?.text || '')
|
|
406
|
+
|
|
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()
|
|
282
447
|
}
|
|
283
|
-
|
|
448
|
+
}
|
|
284
449
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
onProgress(`✅ *Кандидат ${i + 1} (${label}) завершил ответ${costStr}*\n`)
|
|
288
|
-
}
|
|
450
|
+
runOne()
|
|
451
|
+
})
|
|
289
452
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
+
}
|
|
304
483
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
+
}
|
|
314
503
|
}
|
|
315
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()
|
|
316
515
|
})
|
|
317
516
|
|
|
318
|
-
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
|
+
}
|
|
319
548
|
}
|
|
320
549
|
|
|
321
550
|
/**
|
|
322
|
-
* Main MoA pipeline runner.
|
|
551
|
+
* Main MoA pipeline runner with Curator Synthesis, Fallback Chains, and Live Token Streaming.
|
|
323
552
|
*/
|
|
324
553
|
export async function runMoAPipeline({
|
|
325
554
|
userPrompt,
|
|
@@ -328,28 +557,39 @@ export async function runMoAPipeline({
|
|
|
328
557
|
callLlm,
|
|
329
558
|
cwd = process.cwd(),
|
|
330
559
|
onProgress,
|
|
560
|
+
onStreamDelta,
|
|
561
|
+
prices = {},
|
|
562
|
+
historyFilePath,
|
|
563
|
+
liveCanvas = null,
|
|
331
564
|
}) {
|
|
332
565
|
const startTime = Date.now()
|
|
333
566
|
const referenceModels = preset?.reference_models || [
|
|
334
567
|
{ provider: 'opencode-go', model: 'deepseek-v4-flash' },
|
|
335
568
|
{ provider: 'grok', model: 'grok-build-0.1' },
|
|
336
569
|
]
|
|
337
|
-
const
|
|
338
|
-
const
|
|
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
|
+
|
|
339
574
|
const refTemp = preset?.reference_temperature ?? 0.6
|
|
340
575
|
const aggTemp = preset?.aggregator_temperature ?? 0.4
|
|
341
576
|
const maxTokens = preset?.max_tokens ?? 4096
|
|
342
577
|
const judgeCriteria = preset?.judge_criteria ?? ''
|
|
343
|
-
const isFastMode = referenceModels.length === 1
|
|
344
|
-
|
|
345
|
-
|
|
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
|
|
346
586
|
if (typeof onProgress === 'function') {
|
|
347
|
-
onProgress('🔍
|
|
587
|
+
onProgress('🔍 *Scanning project context...*\n')
|
|
348
588
|
}
|
|
349
589
|
const projectContext = await collectProjectContext(cwd)
|
|
350
590
|
const isRefinement = isRefinementTask(userPrompt, projectContext.files)
|
|
351
591
|
|
|
352
|
-
// 2. Check if broad prompt requires user questionnaire
|
|
592
|
+
// 2. Check if broad prompt requires user questionnaire
|
|
353
593
|
const askQuestions = preset?.ask_clarifying_questions !== false
|
|
354
594
|
const needsQuestions = !isFastMode && askQuestions && !isRefinement && isBroadPromptRequiringQuestions(userPrompt, messages)
|
|
355
595
|
|
|
@@ -363,32 +603,36 @@ export async function runMoAPipeline({
|
|
|
363
603
|
|
|
364
604
|
let promptForCandidates = userPrompt
|
|
365
605
|
if (needsQuestions) {
|
|
366
|
-
promptForCandidates +=
|
|
606
|
+
promptForCandidates += "\n(Note: the task is broad. Propose the key architectural and functional decision points to clarify the user's requirements.)"
|
|
367
607
|
}
|
|
368
608
|
|
|
369
609
|
const enrichedMessages = [...messages, { role: 'user', content: promptForCandidates }]
|
|
370
610
|
|
|
371
|
-
// 4. Parallel fan-out to candidate models
|
|
372
|
-
if (typeof onProgress === 'function') {
|
|
373
|
-
onProgress(`🚀 *Запуск ${referenceModels.length} моделей-кандидатов параллельно...*\n`)
|
|
374
|
-
}
|
|
375
|
-
|
|
611
|
+
// 4. Parallel fan-out to candidate models with quorum & transient retry
|
|
376
612
|
const referenceOutputs = await runReferencesParallel(
|
|
377
613
|
referenceModels,
|
|
378
614
|
enrichedMessages,
|
|
379
|
-
{
|
|
615
|
+
{
|
|
616
|
+
systemPrompt: candidateSystemPrompt,
|
|
617
|
+
temperature: refTemp,
|
|
618
|
+
maxTokens,
|
|
619
|
+
prices,
|
|
620
|
+
quorumEnabled: isQuorumEnabled,
|
|
621
|
+
gracePeriodSec,
|
|
622
|
+
maxRetries: candidateRetries,
|
|
623
|
+
},
|
|
380
624
|
callLlm,
|
|
381
625
|
onProgress
|
|
382
626
|
)
|
|
383
627
|
|
|
384
|
-
// Fail fast if all candidates failed
|
|
628
|
+
// Fail fast if all candidates failed
|
|
385
629
|
const successfulRefs = referenceOutputs.filter((r) => r.ok)
|
|
386
630
|
if (successfulRefs.length === 0) {
|
|
387
631
|
const reasons = referenceOutputs.map((r) => `${r.label}: ${r.error || 'unknown error'}`).join('; ')
|
|
388
632
|
return {
|
|
389
633
|
kind: 'failure',
|
|
390
|
-
content: `⚠️
|
|
391
|
-
aggregator:
|
|
634
|
+
content: `⚠️ All advisor models (${referenceOutputs.length}) failed: ${reasons}`,
|
|
635
|
+
aggregator: slotLabel(primaryJudge),
|
|
392
636
|
references: referenceOutputs,
|
|
393
637
|
presetName: preset?.name || 'default',
|
|
394
638
|
isRefinement,
|
|
@@ -406,7 +650,7 @@ export async function runMoAPipeline({
|
|
|
406
650
|
}
|
|
407
651
|
}
|
|
408
652
|
|
|
409
|
-
// 5. Extract file blocks & write candidate workspaces
|
|
653
|
+
// 5. Extract file blocks & write candidate workspaces
|
|
410
654
|
for (let i = 0; i < referenceOutputs.length; i++) {
|
|
411
655
|
const ref = referenceOutputs[i]
|
|
412
656
|
if (!ref.ok) continue
|
|
@@ -417,24 +661,53 @@ export async function runMoAPipeline({
|
|
|
417
661
|
}
|
|
418
662
|
}
|
|
419
663
|
|
|
420
|
-
// 6. Questionnaire synthesis branch
|
|
664
|
+
// 6. Questionnaire synthesis branch
|
|
421
665
|
if (needsQuestions) {
|
|
422
666
|
if (typeof onProgress === 'function') {
|
|
423
|
-
onProgress('📋
|
|
667
|
+
onProgress('📋 *Judge synthesizes the clarification questionnaire...*\n')
|
|
424
668
|
}
|
|
425
669
|
const questionPrompt = buildQuestionSynthesisPrompt(userPrompt, referenceOutputs)
|
|
426
670
|
let questionsContent = ''
|
|
671
|
+
let qUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
|
|
427
672
|
try {
|
|
428
|
-
const qRes = await callLlm
|
|
429
|
-
provider:
|
|
430
|
-
model:
|
|
673
|
+
const qRes = await callWithTransientRetry(callLlm, {
|
|
674
|
+
provider: primaryJudge.provider,
|
|
675
|
+
model: primaryJudge.model,
|
|
431
676
|
messages: [{ role: 'user', content: questionPrompt }],
|
|
432
677
|
temperature: 0.3,
|
|
433
678
|
maxTokens: 2048,
|
|
434
|
-
})
|
|
679
|
+
}, 1)
|
|
435
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)),
|
|
684
|
+
}
|
|
685
|
+
qUsage = estimateTokenCost(primaryJudge, qFallback, prices)
|
|
436
686
|
} catch {
|
|
437
|
-
questionsContent = '###
|
|
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)
|
|
438
711
|
}
|
|
439
712
|
|
|
440
713
|
return {
|
|
@@ -459,6 +732,26 @@ export async function runMoAPipeline({
|
|
|
459
732
|
const totalTokens = single.usage?.totalTokens || 0
|
|
460
733
|
const totalCostUsd = single.costUsd || 0
|
|
461
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
|
+
}
|
|
462
755
|
|
|
463
756
|
return {
|
|
464
757
|
kind: 'synthesis',
|
|
@@ -471,6 +764,7 @@ export async function runMoAPipeline({
|
|
|
471
764
|
winningIndex: 1,
|
|
472
765
|
winnerModel: single.label,
|
|
473
766
|
promotedFiles,
|
|
767
|
+
...(livePreview ? { liveCanvas: livePreview } : {}),
|
|
474
768
|
usage: {
|
|
475
769
|
totalTokens,
|
|
476
770
|
totalCostUsd,
|
|
@@ -481,39 +775,84 @@ export async function runMoAPipeline({
|
|
|
481
775
|
}
|
|
482
776
|
}
|
|
483
777
|
|
|
484
|
-
// 8. Aggregator / Judge synthesis (#
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
+
)
|
|
488
785
|
|
|
489
|
-
const synthesisPrompt = buildSynthesisPrompt(userPrompt, referenceOutputs, judgeCriteria)
|
|
490
786
|
let synthesizedText = ''
|
|
491
787
|
let aggUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
|
|
788
|
+
let chosenJudge = primaryJudge
|
|
789
|
+
let judgeSuccess = false
|
|
790
|
+
let lastJudgeError = null
|
|
492
791
|
|
|
493
|
-
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
792
|
+
for (let jIdx = 0; jIdx < judgesChain.length; jIdx++) {
|
|
793
|
+
const currentJudge = judgesChain[jIdx]
|
|
794
|
+
const currentLabel = slotLabel(currentJudge)
|
|
795
|
+
|
|
796
|
+
if (typeof onProgress === 'function') {
|
|
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`)
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
try {
|
|
803
|
+
const aggRes = await callWithTransientRetry(callLlm, {
|
|
804
|
+
provider: currentJudge.provider,
|
|
805
|
+
model: currentJudge.model,
|
|
806
|
+
messages: [{ role: 'user', content: synthesisPrompt }],
|
|
807
|
+
temperature: aggTemp,
|
|
808
|
+
maxTokens,
|
|
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)),
|
|
820
|
+
}
|
|
821
|
+
aggUsage = estimateTokenCost(currentJudge, aggFallbackUsage, prices)
|
|
822
|
+
chosenJudge = currentJudge
|
|
823
|
+
judgeSuccess = true
|
|
824
|
+
break
|
|
825
|
+
} catch (err) {
|
|
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
|
+
}
|
|
505
833
|
}
|
|
506
|
-
aggUsage = estimateTokenCost(aggregator, aggFallbackUsage)
|
|
507
|
-
} catch (err) {
|
|
508
|
-
console.warn(`[dsh-moa] Aggregator ${aggLabel} failed:`, err)
|
|
509
|
-
synthesizedText = `⚠️ *[Aggregator error: ${err?.message || err}. Fallback candidate outputs:]*\n\n` +
|
|
510
|
-
successfulRefs.map((r, i) => `### Кандидат ${i + 1} (${r.label})\n${r.text}`).join('\n\n')
|
|
511
834
|
}
|
|
512
835
|
|
|
513
|
-
|
|
514
|
-
|
|
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')
|
|
840
|
+
}
|
|
841
|
+
|
|
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)
|
|
515
850
|
let promotedFiles = []
|
|
516
|
-
|
|
851
|
+
|
|
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) {
|
|
517
856
|
promotedFiles = await promoteCandidateWorkspace(cwd, winningIndex)
|
|
518
857
|
} else if (successfulRefs[0]?.files?.length > 0) {
|
|
519
858
|
const fallbackIdx = successfulRefs[0].index
|
|
@@ -522,16 +861,7 @@ export async function runMoAPipeline({
|
|
|
522
861
|
await cleanMoaWorkspaces(cwd)
|
|
523
862
|
}
|
|
524
863
|
|
|
525
|
-
|
|
526
|
-
let liveCanvas = null
|
|
527
|
-
const htmlFile = promotedFiles.find((f) => f.endsWith('.html') || f.endsWith('index.html'))
|
|
528
|
-
if (htmlFile) {
|
|
529
|
-
liveCanvas = {
|
|
530
|
-
previewUrl: `http://localhost:3000/preview/${encodeURIComponent(htmlFile)}`,
|
|
531
|
-
file: htmlFile,
|
|
532
|
-
title: 'Interactive Web Preview',
|
|
533
|
-
}
|
|
534
|
-
}
|
|
864
|
+
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs)
|
|
535
865
|
|
|
536
866
|
const totalTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0) + aggUsage.totalTokens
|
|
537
867
|
const totalCostUsd = Number((referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + aggUsage.costUsd).toFixed(5))
|
|
@@ -539,22 +869,23 @@ export async function runMoAPipeline({
|
|
|
539
869
|
|
|
540
870
|
const winningRef = referenceOutputs[winningIndex - 1]
|
|
541
871
|
const winnerModel = winningRef?.label || slotLabel(referenceModels[0])
|
|
872
|
+
const finalAggLabel = slotLabel(chosenJudge)
|
|
542
873
|
|
|
543
|
-
// Record run to history
|
|
874
|
+
// Record run to history
|
|
544
875
|
try {
|
|
545
876
|
recordMoaRun({
|
|
546
877
|
prompt: userPrompt,
|
|
547
878
|
preset: preset?.name || 'default',
|
|
548
879
|
isRefinement,
|
|
549
|
-
candidates: referenceOutputs,
|
|
550
|
-
aggregator:
|
|
880
|
+
candidates: candidatesForHistory(referenceOutputs),
|
|
881
|
+
aggregator: { provider: chosenJudge.provider, model: chosenJudge.model, usage: aggUsage, costUsd: aggUsage.costUsd },
|
|
551
882
|
winnerIndex: winningIndex,
|
|
552
883
|
winnerModel,
|
|
553
884
|
promotedFiles,
|
|
554
885
|
totalTokens,
|
|
555
886
|
totalCostUsd,
|
|
556
887
|
durationMs,
|
|
557
|
-
})
|
|
888
|
+
}, historyFilePath)
|
|
558
889
|
} catch (histErr) {
|
|
559
890
|
console.warn('[dsh-moa] Failed to record run in history:', histErr)
|
|
560
891
|
}
|
|
@@ -562,19 +893,21 @@ export async function runMoAPipeline({
|
|
|
562
893
|
return {
|
|
563
894
|
kind: 'synthesis',
|
|
564
895
|
content: synthesizedText,
|
|
565
|
-
aggregator: isFastMode ? 'Fast Mode (Direct)' :
|
|
896
|
+
aggregator: isFastMode ? 'Fast Mode (Direct)' : finalAggLabel,
|
|
566
897
|
references: referenceOutputs,
|
|
567
898
|
presetName: preset?.name || 'default',
|
|
568
899
|
isRefinement,
|
|
569
900
|
isFastMode,
|
|
901
|
+
isCuratorSynthesis,
|
|
902
|
+
recommendedAssembler,
|
|
570
903
|
winningIndex,
|
|
571
904
|
winnerModel,
|
|
572
905
|
promotedFiles,
|
|
573
|
-
liveCanvas,
|
|
906
|
+
...(livePreview ? { liveCanvas: livePreview } : {}),
|
|
574
907
|
usage: {
|
|
575
908
|
totalTokens,
|
|
576
909
|
totalCostUsd,
|
|
577
|
-
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 })),
|
|
578
911
|
aggregator: aggUsage,
|
|
579
912
|
},
|
|
580
913
|
durationMs,
|
|
@@ -592,8 +925,8 @@ export function stripOrSummarizeCode(text) {
|
|
|
592
925
|
return match
|
|
593
926
|
}
|
|
594
927
|
const fileHint = fileTag || (code.match(/^\s*(?:\/\/|#|<!--|\/\*)\s*(?:file|filepath|path):\s*([^\s*]+)/im)?.[1])
|
|
595
|
-
const label = fileHint ?
|
|
596
|
-
return `\n> 📄 *[${label}
|
|
928
|
+
const label = fileHint ? `file \`${fileHint}\`` : (lang ? `code \`${lang}\`` : 'code')
|
|
929
|
+
return `\n> 📄 *[${label} - ${lines.length} lines saved to disk]*\n`
|
|
597
930
|
})
|
|
598
931
|
}
|
|
599
932
|
|
|
@@ -604,47 +937,53 @@ export function formatMoAResponse({ moaResult, presetName }) {
|
|
|
604
937
|
const refs = moaResult?.references || []
|
|
605
938
|
|
|
606
939
|
if (moaResult?.kind === 'questions') {
|
|
607
|
-
parts.push('## 🧠 Mixture of Agents —
|
|
608
|
-
parts.push(
|
|
940
|
+
parts.push('## 🧠 Mixture of Agents — Requirements Clarification')
|
|
941
|
+
parts.push(`*Judge (${judge}) and the advisors analyzed the task:*\n`)
|
|
609
942
|
parts.push(moaResult.content)
|
|
610
943
|
return parts.join('\n')
|
|
611
944
|
}
|
|
612
945
|
|
|
613
946
|
if (moaResult?.kind === 'failure') {
|
|
614
|
-
parts.push('## ⚠️ Mixture of Agents —
|
|
947
|
+
parts.push('## ⚠️ Mixture of Agents — Execution Failed')
|
|
615
948
|
parts.push(moaResult.content)
|
|
616
949
|
return parts.join('\n')
|
|
617
950
|
}
|
|
618
951
|
|
|
619
|
-
const modeBadge = moaResult?.isFastMode
|
|
620
|
-
|
|
952
|
+
const modeBadge = moaResult?.isFastMode
|
|
953
|
+
? '⚡ Fast Mode'
|
|
954
|
+
: (moaResult?.isCuratorSynthesis ? `🧠 Curator: ${judge}` : `Judge: ${judge}`)
|
|
955
|
+
parts.push(`## 🧠 Mixture of Agents (Preset: ${pName} | ${modeBadge})`)
|
|
621
956
|
parts.push('')
|
|
622
957
|
|
|
623
958
|
if (moaResult?.isRefinement) {
|
|
624
|
-
parts.push('> 🔄
|
|
959
|
+
parts.push('> 🔄 **Mode**: Iterative project refinement (Refinement)')
|
|
625
960
|
}
|
|
626
961
|
|
|
627
962
|
const hasPromoted = moaResult?.promotedFiles && moaResult.promotedFiles.length > 0
|
|
628
963
|
if (hasPromoted) {
|
|
629
|
-
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}\`)`)
|
|
630
969
|
}
|
|
631
970
|
|
|
632
971
|
if (moaResult?.liveCanvas?.previewUrl) {
|
|
633
972
|
parts.push(`> 🎨 **Live Canvas**: [🚀 Открыть ${moaResult.liveCanvas.title || 'превью'} в Live Canvas](${moaResult.liveCanvas.previewUrl}) | [↗ Открыть в новой вкладке](${moaResult.liveCanvas.previewUrl})`)
|
|
634
973
|
}
|
|
635
974
|
|
|
636
|
-
// Cost tracking card
|
|
975
|
+
// Cost tracking card
|
|
637
976
|
if (moaResult?.usage) {
|
|
638
977
|
const u = moaResult.usage
|
|
639
|
-
const costStr = u.totalCostUsd > 0 ? `~\$${u.totalCostUsd.toFixed(4)}` : '
|
|
978
|
+
const costStr = u.totalCostUsd > 0 ? `~\$${u.totalCostUsd.toFixed(4)}` : 'Free'
|
|
640
979
|
const tokStr = u.totalTokens >= 1000 ? `${(u.totalTokens / 1000).toFixed(1)}k` : `${u.totalTokens}`
|
|
641
|
-
parts.push(`> 💰
|
|
980
|
+
parts.push(`> 💰 **Run cost**: ${costStr} (${tokStr} tokens total)`)
|
|
642
981
|
}
|
|
643
982
|
|
|
644
983
|
parts.push('')
|
|
645
984
|
|
|
646
985
|
if (!moaResult?.isFastMode) {
|
|
647
|
-
parts.push(`### ⚖️
|
|
986
|
+
parts.push(`### ⚖️ Judge verdict and final synthesis (Synthesis: ${judge})`)
|
|
648
987
|
parts.push('')
|
|
649
988
|
const cleanJudgeContent = hasPromoted
|
|
650
989
|
? stripOrSummarizeCode(moaResult?.content || '')
|
|
@@ -654,14 +993,14 @@ export function formatMoAResponse({ moaResult, presetName }) {
|
|
|
654
993
|
}
|
|
655
994
|
|
|
656
995
|
if (Array.isArray(refs) && refs.length > 0) {
|
|
657
|
-
const title = moaResult?.isFastMode ? '### 🚀
|
|
996
|
+
const title = moaResult?.isFastMode ? '### 🚀 Candidate generation result:' : `### 👥 Advisor responses (${refs.length}):`
|
|
658
997
|
parts.push(title)
|
|
659
998
|
parts.push('')
|
|
660
999
|
refs.forEach((ref, i) => {
|
|
661
1000
|
const statusIcon = ref.ok ? '✅' : '⚠️'
|
|
662
1001
|
const fileBadge = ref.files?.length ? ` (${ref.files.length} файл(ов))` : ''
|
|
663
1002
|
const costBadge = ref.costUsd > 0 ? ` [~\$${ref.costUsd.toFixed(4)}]` : ''
|
|
664
|
-
parts.push(`#### ${statusIcon}
|
|
1003
|
+
parts.push(`#### ${statusIcon} Model ${i + 1}: ${ref.label}${fileBadge}${costBadge}`)
|
|
665
1004
|
parts.push('')
|
|
666
1005
|
parts.push(stripOrSummarizeCode(ref.text))
|
|
667
1006
|
parts.push('')
|
|
@@ -673,12 +1012,12 @@ export function formatMoAResponse({ moaResult, presetName }) {
|
|
|
673
1012
|
return parts.join('\n')
|
|
674
1013
|
}
|
|
675
1014
|
|
|
676
|
-
export async function* streamMoATurn({ targetPreset, userPrompt, messages, callLlm, cwd }, options = {}) {
|
|
1015
|
+
export async function* streamMoATurn({ targetPreset, userPrompt, messages, callLlm, cwd, prices, historyFilePath, liveCanvas }, options = {}) {
|
|
677
1016
|
const signal = options?.signal
|
|
678
1017
|
if (signal?.aborted) return
|
|
679
1018
|
|
|
680
1019
|
yield { type: 'block-start', index: 0, blockType: 'text' }
|
|
681
|
-
yield { type: 'text-delta', index: 0, text: '🧠 *Mixture of Agents
|
|
1020
|
+
yield { type: 'text-delta', index: 0, text: '🧠 *Mixture of Agents started...*\n\n' }
|
|
682
1021
|
|
|
683
1022
|
// Async push-queue for zero-latency live delta streaming
|
|
684
1023
|
const queue = []
|
|
@@ -700,6 +1039,12 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
|
|
|
700
1039
|
callLlm,
|
|
701
1040
|
cwd,
|
|
702
1041
|
onProgress: pushUpdate,
|
|
1042
|
+
onStreamDelta: (delta) => {
|
|
1043
|
+
pushUpdate(delta)
|
|
1044
|
+
},
|
|
1045
|
+
prices,
|
|
1046
|
+
historyFilePath,
|
|
1047
|
+
liveCanvas,
|
|
703
1048
|
})
|
|
704
1049
|
.catch((err) => ({ error: err }))
|
|
705
1050
|
.finally(() => {
|
|
@@ -736,7 +1081,7 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
|
|
|
736
1081
|
|
|
737
1082
|
const elapsedSec = Math.floor((Date.now() - startTime) / 1000)
|
|
738
1083
|
if (!done && Date.now() - lastYieldTime >= 3000) {
|
|
739
|
-
yield { type: 'text-delta', index: 0, text: `⏳ *[${elapsedSec}
|
|
1084
|
+
yield { type: 'text-delta', index: 0, text: `⏳ *[${elapsedSec}s] Still processing...*\n` }
|
|
740
1085
|
lastYieldTime = Date.now()
|
|
741
1086
|
}
|
|
742
1087
|
}
|
|
@@ -748,7 +1093,7 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
|
|
|
748
1093
|
}
|
|
749
1094
|
|
|
750
1095
|
if (result?.error) {
|
|
751
|
-
const errText = '\n\n⚠️
|
|
1096
|
+
const errText = '\n\n⚠️ **Mixture of Agents error**: ' + (result.error?.message || String(result.error))
|
|
752
1097
|
yield { type: 'text-delta', index: 0, text: errText }
|
|
753
1098
|
yield { type: 'block-end', index: 0, block: { type: 'text', text: errText } }
|
|
754
1099
|
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
@@ -762,7 +1107,13 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
|
|
|
762
1107
|
|
|
763
1108
|
yield { type: 'text-delta', index: 0, text: '\n---\n\n' + formatted }
|
|
764
1109
|
yield { type: 'block-end', index: 0, block: { type: 'text', text: formatted } }
|
|
765
|
-
yield {
|
|
1110
|
+
yield {
|
|
1111
|
+
type: 'usage',
|
|
1112
|
+
usage: {
|
|
1113
|
+
inputTokens: result?.usage?.totalTokens || 0,
|
|
1114
|
+
outputTokens: Math.round((formatted.length || 0) / 4),
|
|
1115
|
+
},
|
|
1116
|
+
}
|
|
766
1117
|
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
767
1118
|
} finally {
|
|
768
1119
|
if (signal?.aborted) {
|