@goodandready/dsh-moa 0.2.15 → 0.2.16
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 +7 -0
- package/lib/best-effort.js +38 -0
- package/lib/client.js +7 -4
- package/lib/file-workspace.js +48 -34
- package/lib/history.js +4 -4
- package/lib/index.js +17 -382
- package/lib/moa-candidates.js +225 -0
- package/lib/moa-runner.js +13 -222
- package/lib/routes.js +408 -0
- package/lib/updater.js +280 -0
- package/package.json +1 -2
- package/docs/README.ru.md +0 -287
- package/docs/README.zh.md +0 -243
- package/docs/design/DESIGN.md +0 -79
- package/docs/plans/47-client-js-split-plan.md +0 -57
- package/docs/plans/55-resilience-plan.md +0 -33
- package/docs/plans/59-power-pack-plan.md +0 -73
- package/docs/plans/63-stability-polish-plan.md +0 -15
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek Harness Mixture of Agents (MoA) — Candidate Fan-Out & Retry
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { bestEffort } from './best-effort.js'
|
|
6
|
+
import { slotLabel, cleanAdvisoryMessages, ROLE_PERSONA_PROMPTS, SYSTEM_ROLE_PROPOSER } from './moa-prompts.js'
|
|
7
|
+
import { estimateTokenCost } from './pricing.js'
|
|
8
|
+
|
|
9
|
+
export const REFERENCE_SYSTEM_PROMPT = SYSTEM_ROLE_PROPOSER
|
|
10
|
+
|
|
11
|
+
export async function callWithTransientRetry(callLlmFn, callArgs, maxRetries = 0, retryDelayMs = 1200) {
|
|
12
|
+
let attempt = 0
|
|
13
|
+
while (true) {
|
|
14
|
+
if (callArgs?.signal?.aborted) throw new Error('Aborted')
|
|
15
|
+
try {
|
|
16
|
+
return await callLlmFn(callArgs)
|
|
17
|
+
} catch (err) {
|
|
18
|
+
attempt++
|
|
19
|
+
const msg = err?.message || String(err)
|
|
20
|
+
const isTransient = /429|rate limit|502|503|504|econnreset|etimedout|socket hang up/i.test(msg)
|
|
21
|
+
if (attempt <= maxRetries && isTransient && !callArgs?.signal?.aborted) {
|
|
22
|
+
await new Promise((r) => setTimeout(r, retryDelayMs))
|
|
23
|
+
if (callArgs?.signal?.aborted) throw new Error('Aborted')
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
throw err
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Dispatches queries to all reference models in parallel with transient retry and quorum straggler mitigation.
|
|
33
|
+
*/
|
|
34
|
+
export async function runReferencesParallel(references, messages, options = {}, callLlm, onProgress) {
|
|
35
|
+
if (!Array.isArray(references) || references.length === 0) {
|
|
36
|
+
return []
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const advisoryMessages = cleanAdvisoryMessages(messages)
|
|
40
|
+
const systemPrompt = options.systemPrompt || REFERENCE_SYSTEM_PROMPT
|
|
41
|
+
const timeoutMs = options.timeoutMs ?? ((options.timeoutSec ?? 60) * 1000)
|
|
42
|
+
const maxRetries = options.maxRetries ?? 0
|
|
43
|
+
const quorumEnabled = Boolean(options.quorumEnabled)
|
|
44
|
+
const gracePeriodMs = (options.gracePeriodSec ?? 10) * 1000
|
|
45
|
+
|
|
46
|
+
const total = references.length
|
|
47
|
+
let finishedCount = 0
|
|
48
|
+
const results = new Array(total)
|
|
49
|
+
const abortControllers = references.map(() => new AbortController())
|
|
50
|
+
if (options.signal) {
|
|
51
|
+
if (options.signal.aborted) {
|
|
52
|
+
return references.map((r, i) => ({
|
|
53
|
+
index: i + 1,
|
|
54
|
+
provider: r.provider,
|
|
55
|
+
model: r.model,
|
|
56
|
+
label: slotLabel(r),
|
|
57
|
+
ok: false,
|
|
58
|
+
text: '(aborted)',
|
|
59
|
+
error: 'Turn aborted',
|
|
60
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
61
|
+
costUsd: 0,
|
|
62
|
+
}))
|
|
63
|
+
}
|
|
64
|
+
if (typeof options.signal.addEventListener === 'function') {
|
|
65
|
+
options.signal.addEventListener('abort', () => {
|
|
66
|
+
for (const ac of abortControllers) {
|
|
67
|
+
bestEffort('turn abort signal', () => ac.abort(new Error('Turn aborted')))
|
|
68
|
+
}
|
|
69
|
+
}, { once: true })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let onTaskFinished = null
|
|
74
|
+
const notifyFinished = () => {
|
|
75
|
+
if (typeof onTaskFinished === 'function') onTaskFinished()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (typeof onProgress === 'function') {
|
|
79
|
+
onProgress(`⚡ *Launching ${total} candidate models in parallel...*\n`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
references.forEach((slot, i) => {
|
|
83
|
+
const label = slotLabel(slot)
|
|
84
|
+
const persona = slot?.role_persona && ROLE_PERSONA_PROMPTS[slot.role_persona]
|
|
85
|
+
? `\n\n${ROLE_PERSONA_PROMPTS[slot.role_persona]}`
|
|
86
|
+
: ''
|
|
87
|
+
const slotMessages = [{ role: 'system', content: systemPrompt + persona }, ...advisoryMessages]
|
|
88
|
+
|
|
89
|
+
const runOne = async () => {
|
|
90
|
+
try {
|
|
91
|
+
const callPromise = callWithTransientRetry(
|
|
92
|
+
callLlm,
|
|
93
|
+
{
|
|
94
|
+
provider: slot.provider,
|
|
95
|
+
model: slot.model,
|
|
96
|
+
messages: slotMessages,
|
|
97
|
+
temperature: options.temperature ?? 0.6,
|
|
98
|
+
maxTokens: options.maxTokens ?? 4096,
|
|
99
|
+
timeoutMs,
|
|
100
|
+
signal: abortControllers[i].signal,
|
|
101
|
+
},
|
|
102
|
+
maxRetries
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
106
|
+
const timer = setTimeout(() => reject(new Error(`Timeout after ${Math.round(timeoutMs / 1000)}s`)), timeoutMs)
|
|
107
|
+
timer.unref?.()
|
|
108
|
+
abortControllers[i].signal.addEventListener('abort', () => clearTimeout(timer), { once: true })
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const res = await Promise.race([callPromise, timeoutPromise])
|
|
112
|
+
const text = typeof res === 'string' ? res : (res?.content || res?.text || '')
|
|
113
|
+
|
|
114
|
+
const fallbackUsage = (typeof res === 'object' && res?.usage) ? res.usage : {
|
|
115
|
+
inputTokens: Math.max(1, Math.round(slotMessages.map((m) => m.content).join('').length / 4)),
|
|
116
|
+
outputTokens: Math.max(1, Math.round(text.length / 4)),
|
|
117
|
+
}
|
|
118
|
+
const costInfo = estimateTokenCost(slot, fallbackUsage, options.prices)
|
|
119
|
+
|
|
120
|
+
finishedCount++
|
|
121
|
+
if (typeof onProgress === 'function') {
|
|
122
|
+
const costStr = costInfo.costUsd > 0 ? ` (~\$${costInfo.costUsd.toFixed(4)})` : ''
|
|
123
|
+
onProgress(`✅ *Candidate ${i + 1}/${total} (${label}) finished${costStr}*\n`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
results[i] = {
|
|
127
|
+
index: i + 1,
|
|
128
|
+
slot,
|
|
129
|
+
label,
|
|
130
|
+
text,
|
|
131
|
+
usage: costInfo,
|
|
132
|
+
costUsd: costInfo.costUsd,
|
|
133
|
+
ok: true,
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
finishedCount++
|
|
137
|
+
const errMsg = err?.message || String(err)
|
|
138
|
+
console.warn(`[dsh-moa] Reference ${label} failed:`, errMsg)
|
|
139
|
+
if (typeof onProgress === 'function') {
|
|
140
|
+
onProgress(`⚠️ *Candidate ${i + 1}/${total} (${label}) error: ${errMsg}*\n`)
|
|
141
|
+
}
|
|
142
|
+
results[i] = {
|
|
143
|
+
index: i + 1,
|
|
144
|
+
slot,
|
|
145
|
+
label,
|
|
146
|
+
text: `[Model ${label} error: ${errMsg}]`,
|
|
147
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
148
|
+
costUsd: 0,
|
|
149
|
+
ok: false,
|
|
150
|
+
error: errMsg,
|
|
151
|
+
}
|
|
152
|
+
} finally {
|
|
153
|
+
notifyFinished()
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
runOne()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// Straggler mitigation via Quorum + Grace Period
|
|
161
|
+
if (quorumEnabled && total >= 3) {
|
|
162
|
+
const quorumTarget = Math.max(2, Math.min(total - 1, Math.ceil(total * 0.6)))
|
|
163
|
+
let graceTimer = null
|
|
164
|
+
|
|
165
|
+
await new Promise((resolve) => {
|
|
166
|
+
const checkQuorum = () => {
|
|
167
|
+
if (finishedCount >= total) {
|
|
168
|
+
if (graceTimer) clearTimeout(graceTimer)
|
|
169
|
+
resolve()
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (finishedCount >= quorumTarget && !graceTimer) {
|
|
174
|
+
if (typeof onProgress === 'function') {
|
|
175
|
+
onProgress(`⏳ *Quorum reached (${finishedCount}/${total}). Starting ${Math.round(gracePeriodMs / 1000)}s grace period for stragglers...*\n`)
|
|
176
|
+
}
|
|
177
|
+
graceTimer = setTimeout(() => {
|
|
178
|
+
if (typeof onProgress === 'function' && finishedCount < total) {
|
|
179
|
+
onProgress(`⏩ *Grace period expired. Proceeding with ${finishedCount}/${total} ready candidates.*\n`)
|
|
180
|
+
}
|
|
181
|
+
for (let k = 0; k < total; k++) {
|
|
182
|
+
if (!results[k]) {
|
|
183
|
+
bestEffort('quorum grace period abort', () => abortControllers[k].abort(new Error('Quorum grace period timed out')))
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
resolve()
|
|
187
|
+
}, gracePeriodMs)
|
|
188
|
+
graceTimer.unref?.()
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
onTaskFinished = checkQuorum
|
|
193
|
+
checkQuorum()
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
for (let i = 0; i < total; i++) {
|
|
197
|
+
if (!results[i]) {
|
|
198
|
+
const slot = references[i]
|
|
199
|
+
const label = slotLabel(slot)
|
|
200
|
+
results[i] = {
|
|
201
|
+
index: i + 1,
|
|
202
|
+
slot,
|
|
203
|
+
label,
|
|
204
|
+
text: `[Model ${label} timed out (quorum grace period exceeded)]`,
|
|
205
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
206
|
+
costUsd: 0,
|
|
207
|
+
ok: false,
|
|
208
|
+
error: 'Quorum grace period timed out',
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return results
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Standard mode: wait for all tasks to complete
|
|
216
|
+
await new Promise((resolve) => {
|
|
217
|
+
const checkAll = () => {
|
|
218
|
+
if (finishedCount >= total) resolve()
|
|
219
|
+
}
|
|
220
|
+
onTaskFinished = checkAll
|
|
221
|
+
checkAll()
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
return results
|
|
225
|
+
}
|
package/lib/moa-runner.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bestEffort } from './best-effort.js'
|
|
1
2
|
/**
|
|
2
3
|
* DeepSeek Harness Mixture of Agents (MoA) — Runner Engine
|
|
3
4
|
* Parallel fan-out, Consilium Round 2 peer critique, aggregator synthesis & streaming.
|
|
@@ -17,229 +18,13 @@ export { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buil
|
|
|
17
18
|
export { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
|
|
18
19
|
export { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
export const REFERENCE_SYSTEM_PROMPT = SYSTEM_ROLE_PROPOSER
|
|
21
|
+
const DEFAULT_PROMPT = 'Please propose an optimal, well-structured, production-ready solution with full code and explanations.'
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Invokes LLM call with transient retry for recoverable network/rate-limit errors.
|
|
25
25
|
*/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
while (true) {
|
|
29
|
-
if (callArgs?.signal?.aborted) throw new Error('Aborted')
|
|
30
|
-
try {
|
|
31
|
-
return await callLlmFn(callArgs)
|
|
32
|
-
} catch (err) {
|
|
33
|
-
attempt++
|
|
34
|
-
const msg = err?.message || String(err)
|
|
35
|
-
const isTransient = /429|rate limit|502|503|504|econnreset|etimedout|socket hang up/i.test(msg)
|
|
36
|
-
if (attempt <= maxRetries && isTransient && !callArgs?.signal?.aborted) {
|
|
37
|
-
await new Promise((r) => setTimeout(r, retryDelayMs))
|
|
38
|
-
if (callArgs?.signal?.aborted) throw new Error('Aborted')
|
|
39
|
-
continue
|
|
40
|
-
}
|
|
41
|
-
throw err
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Dispatches queries to all reference models in parallel with transient retry and quorum straggler mitigation.
|
|
48
|
-
*/
|
|
49
|
-
export async function runReferencesParallel(references, messages, options = {}, callLlm, onProgress) {
|
|
50
|
-
if (!Array.isArray(references) || references.length === 0) {
|
|
51
|
-
return []
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const advisoryMessages = cleanAdvisoryMessages(messages)
|
|
55
|
-
const systemPrompt = options.systemPrompt || REFERENCE_SYSTEM_PROMPT
|
|
56
|
-
const timeoutMs = options.timeoutMs ?? ((options.timeoutSec ?? 60) * 1000)
|
|
57
|
-
const maxRetries = options.maxRetries ?? 0
|
|
58
|
-
const quorumEnabled = Boolean(options.quorumEnabled)
|
|
59
|
-
const gracePeriodMs = (options.gracePeriodSec ?? 10) * 1000
|
|
60
|
-
|
|
61
|
-
const total = references.length
|
|
62
|
-
let finishedCount = 0
|
|
63
|
-
const results = new Array(total)
|
|
64
|
-
const abortControllers = references.map(() => new AbortController())
|
|
65
|
-
if (options.signal) {
|
|
66
|
-
if (options.signal.aborted) {
|
|
67
|
-
return references.map((r, i) => ({
|
|
68
|
-
index: i + 1,
|
|
69
|
-
provider: r.provider,
|
|
70
|
-
model: r.model,
|
|
71
|
-
label: slotLabel(r),
|
|
72
|
-
ok: false,
|
|
73
|
-
text: '(aborted)',
|
|
74
|
-
error: 'Turn aborted',
|
|
75
|
-
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
76
|
-
costUsd: 0,
|
|
77
|
-
}))
|
|
78
|
-
}
|
|
79
|
-
if (typeof options.signal.addEventListener === 'function') {
|
|
80
|
-
options.signal.addEventListener('abort', () => {
|
|
81
|
-
for (const ac of abortControllers) {
|
|
82
|
-
try { ac.abort(new Error('Turn aborted')) } catch {}
|
|
83
|
-
}
|
|
84
|
-
}, { once: true })
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
let onTaskFinished = null
|
|
89
|
-
const notifyFinished = () => {
|
|
90
|
-
if (typeof onTaskFinished === 'function') onTaskFinished()
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (typeof onProgress === 'function') {
|
|
94
|
-
onProgress(`⚡ *Launching ${total} candidate models in parallel...*\n`)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
references.forEach((slot, i) => {
|
|
98
|
-
const label = slotLabel(slot)
|
|
99
|
-
const persona = slot?.role_persona && ROLE_PERSONA_PROMPTS[slot.role_persona]
|
|
100
|
-
? `\n\n${ROLE_PERSONA_PROMPTS[slot.role_persona]}`
|
|
101
|
-
: ''
|
|
102
|
-
const slotMessages = [{ role: 'system', content: systemPrompt + persona }, ...advisoryMessages]
|
|
103
|
-
|
|
104
|
-
const runOne = async () => {
|
|
105
|
-
try {
|
|
106
|
-
const callPromise = callWithTransientRetry(
|
|
107
|
-
callLlm,
|
|
108
|
-
{
|
|
109
|
-
provider: slot.provider,
|
|
110
|
-
model: slot.model,
|
|
111
|
-
messages: slotMessages,
|
|
112
|
-
temperature: options.temperature ?? 0.6,
|
|
113
|
-
maxTokens: options.maxTokens ?? 4096,
|
|
114
|
-
timeoutMs,
|
|
115
|
-
signal: abortControllers[i].signal,
|
|
116
|
-
},
|
|
117
|
-
maxRetries
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
121
|
-
const timer = setTimeout(() => reject(new Error(`Timeout after ${Math.round(timeoutMs / 1000)}s`)), timeoutMs)
|
|
122
|
-
timer.unref?.()
|
|
123
|
-
abortControllers[i].signal.addEventListener('abort', () => clearTimeout(timer), { once: true })
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
const res = await Promise.race([callPromise, timeoutPromise])
|
|
127
|
-
const text = typeof res === 'string' ? res : (res?.content || res?.text || '')
|
|
128
|
-
|
|
129
|
-
const fallbackUsage = (typeof res === 'object' && res?.usage) ? res.usage : {
|
|
130
|
-
inputTokens: Math.max(1, Math.round(slotMessages.map((m) => m.content).join('').length / 4)),
|
|
131
|
-
outputTokens: Math.max(1, Math.round(text.length / 4)),
|
|
132
|
-
}
|
|
133
|
-
const costInfo = estimateTokenCost(slot, fallbackUsage, options.prices)
|
|
134
|
-
|
|
135
|
-
finishedCount++
|
|
136
|
-
if (typeof onProgress === 'function') {
|
|
137
|
-
const costStr = costInfo.costUsd > 0 ? ` (~\$${costInfo.costUsd.toFixed(4)})` : ''
|
|
138
|
-
onProgress(`✅ *Candidate ${i + 1}/${total} (${label}) finished${costStr}*\n`)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
results[i] = {
|
|
142
|
-
index: i + 1,
|
|
143
|
-
slot,
|
|
144
|
-
label,
|
|
145
|
-
text,
|
|
146
|
-
usage: costInfo,
|
|
147
|
-
costUsd: costInfo.costUsd,
|
|
148
|
-
ok: true,
|
|
149
|
-
}
|
|
150
|
-
} catch (err) {
|
|
151
|
-
finishedCount++
|
|
152
|
-
const errMsg = err?.message || String(err)
|
|
153
|
-
console.warn(`[dsh-moa] Reference ${label} failed:`, errMsg)
|
|
154
|
-
if (typeof onProgress === 'function') {
|
|
155
|
-
onProgress(`⚠️ *Candidate ${i + 1}/${total} (${label}) error: ${errMsg}*\n`)
|
|
156
|
-
}
|
|
157
|
-
results[i] = {
|
|
158
|
-
index: i + 1,
|
|
159
|
-
slot,
|
|
160
|
-
label,
|
|
161
|
-
text: `[Model ${label} error: ${errMsg}]`,
|
|
162
|
-
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
163
|
-
costUsd: 0,
|
|
164
|
-
ok: false,
|
|
165
|
-
error: errMsg,
|
|
166
|
-
}
|
|
167
|
-
} finally {
|
|
168
|
-
notifyFinished()
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
runOne()
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
// Straggler mitigation via Quorum + Grace Period
|
|
176
|
-
if (quorumEnabled && total >= 3) {
|
|
177
|
-
const quorumTarget = Math.max(2, Math.min(total - 1, Math.ceil(total * 0.6)))
|
|
178
|
-
let graceTimer = null
|
|
179
|
-
|
|
180
|
-
await new Promise((resolve) => {
|
|
181
|
-
const checkQuorum = () => {
|
|
182
|
-
if (finishedCount >= total) {
|
|
183
|
-
if (graceTimer) clearTimeout(graceTimer)
|
|
184
|
-
resolve()
|
|
185
|
-
return
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (finishedCount >= quorumTarget && !graceTimer) {
|
|
189
|
-
if (typeof onProgress === 'function') {
|
|
190
|
-
onProgress(`⏳ *Quorum reached (${finishedCount}/${total}). Starting ${Math.round(gracePeriodMs / 1000)}s grace period for stragglers...*\n`)
|
|
191
|
-
}
|
|
192
|
-
graceTimer = setTimeout(() => {
|
|
193
|
-
if (typeof onProgress === 'function' && finishedCount < total) {
|
|
194
|
-
onProgress(`⏩ *Grace period expired. Proceeding with ${finishedCount}/${total} ready candidates.*\n`)
|
|
195
|
-
}
|
|
196
|
-
for (let k = 0; k < total; k++) {
|
|
197
|
-
if (!results[k]) {
|
|
198
|
-
try { abortControllers[k].abort(new Error('Quorum grace period timed out')) } catch {}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
resolve()
|
|
202
|
-
}, gracePeriodMs)
|
|
203
|
-
graceTimer.unref?.()
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
onTaskFinished = checkQuorum
|
|
208
|
-
checkQuorum()
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
for (let i = 0; i < total; i++) {
|
|
212
|
-
if (!results[i]) {
|
|
213
|
-
const slot = references[i]
|
|
214
|
-
const label = slotLabel(slot)
|
|
215
|
-
results[i] = {
|
|
216
|
-
index: i + 1,
|
|
217
|
-
slot,
|
|
218
|
-
label,
|
|
219
|
-
text: `[Model ${label} timed out (quorum grace period exceeded)]`,
|
|
220
|
-
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
221
|
-
costUsd: 0,
|
|
222
|
-
ok: false,
|
|
223
|
-
error: 'Quorum grace period timed out',
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return results
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Standard mode: wait for all tasks to complete
|
|
231
|
-
await new Promise((resolve) => {
|
|
232
|
-
const checkAll = () => {
|
|
233
|
-
if (finishedCount >= total) resolve()
|
|
234
|
-
}
|
|
235
|
-
onTaskFinished = checkAll
|
|
236
|
-
checkAll()
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
return results
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
26
|
+
import { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel } from './moa-candidates.js'
|
|
27
|
+
export { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel }
|
|
243
28
|
|
|
244
29
|
/**
|
|
245
30
|
* Executes the full Mixture of Agents pipeline.
|
|
@@ -421,7 +206,9 @@ export async function runMoAPipeline({
|
|
|
421
206
|
totalCostUsd,
|
|
422
207
|
durationMs,
|
|
423
208
|
}, historyFilePath || undefined)
|
|
424
|
-
} catch {
|
|
209
|
+
} catch (histErr) {
|
|
210
|
+
console.warn('[dsh-moa] Failed to record questionnaire run in history:', histErr?.message || histErr)
|
|
211
|
+
}
|
|
425
212
|
|
|
426
213
|
return {
|
|
427
214
|
kind: 'questions',
|
|
@@ -474,7 +261,9 @@ export async function runMoAPipeline({
|
|
|
474
261
|
totalCostUsd,
|
|
475
262
|
durationMs,
|
|
476
263
|
}, historyFilePath || undefined)
|
|
477
|
-
} catch {
|
|
264
|
+
} catch (histErr) {
|
|
265
|
+
console.warn('[dsh-moa] Failed to record fast synthesis run in history:', histErr?.message || histErr)
|
|
266
|
+
}
|
|
478
267
|
|
|
479
268
|
return {
|
|
480
269
|
kind: 'synthesis',
|
|
@@ -534,7 +323,9 @@ export async function runMoAPipeline({
|
|
|
534
323
|
const extraCost = estimateTokenCost(slot, res.usage, prices)
|
|
535
324
|
cand.costUsd = Number(((cand.costUsd || 0) + extraCost.costUsd).toFixed(5))
|
|
536
325
|
}
|
|
537
|
-
} catch {
|
|
326
|
+
} catch (r2CostErr) {
|
|
327
|
+
console.warn('[dsh-moa] Failed to estimate Round 2 cost:', r2CostErr?.message || r2CostErr)
|
|
328
|
+
}
|
|
538
329
|
})
|
|
539
330
|
await Promise.allSettled(r2Promises)
|
|
540
331
|
}
|