@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.
@@ -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
- export const DEFAULT_PROMPT = 'Please propose an optimal, well-structured, production-ready solution with full code and explanations.'
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
- export async function callWithTransientRetry(callLlmFn, callArgs, maxRetries = 0, retryDelayMs = 1200) {
27
- let attempt = 0
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
  }