@goodandready/dsh-moa 0.2.63 → 0.2.65
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/lib/commands.js +83 -0
- package/lib/errors.js +176 -0
- package/lib/events.js +30 -0
- package/lib/index.js +38 -0
- package/lib/moa-candidates.js +36 -11
- package/lib/moa-service.js +155 -0
- package/lib/moa-stream.js +2 -1
- package/lib/pipeline/context.js +4 -2
- package/lib/pipeline/fast-mode.js +8 -0
- package/lib/pipeline/peer-critique.js +1 -1
- package/lib/pipeline/promotion.js +18 -0
- package/lib/pipeline/runner.js +38 -2
- package/lib/routes/pipeline.js +1 -0
- package/package.json +1 -1
package/lib/commands.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Official Slash Command Registration for /moa via dsh-commands service (#275)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { service } from './services.js'
|
|
6
|
+
|
|
7
|
+
export function registerMoaCommands(ctx, { live, moaService }) {
|
|
8
|
+
const registerWithService = (cmdCtx) => {
|
|
9
|
+
if (!cmdCtx) return
|
|
10
|
+
const commands = service(cmdCtx, 'commands')
|
|
11
|
+
if (!commands || typeof commands.register !== 'function') return
|
|
12
|
+
|
|
13
|
+
const formatPresetsList = (cfg) => {
|
|
14
|
+
const presets = Array.isArray(cfg?.presets) ? cfg.presets : []
|
|
15
|
+
if (presets.length === 0) return 'No presets configured in dsh-moa.'
|
|
16
|
+
return '### 🧠 Configured MoA Presets:\n' + presets.map((p, i) =>
|
|
17
|
+
`- **${p.name}** (${p.id || `preset-${i + 1}`}): ${p.description || 'Consensus preset'} | ${p.references?.length || p.reference_models?.length || 0} advisors`
|
|
18
|
+
).join('\n')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const commandHandler = async (invocation) => {
|
|
22
|
+
const rawText = typeof invocation === 'string' ? invocation : (invocation?.text || invocation?.raw || invocation?.args || '')
|
|
23
|
+
const trimmed = String(rawText).trim()
|
|
24
|
+
|
|
25
|
+
if (!trimmed || trimmed === 'help' || trimmed === '--help') {
|
|
26
|
+
return {
|
|
27
|
+
text: '🧠 **MoA Command Guide**:\n- `/moa presets` — list configured presets\n- `/moa history` — view recent runs\n- `/moa status` — check plugin status\n- `/moa [--preset <name>] <prompt>` — run MoA consensus in chat',
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (trimmed === 'presets' || trimmed === 'list') {
|
|
32
|
+
const cfg = typeof live === 'function' ? live() : {}
|
|
33
|
+
return { text: formatPresetsList(cfg) }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (trimmed === 'status') {
|
|
37
|
+
const cfg = typeof live === 'function' ? live() : {}
|
|
38
|
+
return {
|
|
39
|
+
text: `🧠 **MoA Plugin Status**: ${cfg?.enabled !== false ? 'Enabled' : 'Disabled'} | Default preset: \`${cfg?.default_preset || 'default'}\``,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (trimmed === 'history') {
|
|
44
|
+
const hist = moaService?.getHistory ? moaService.getHistory(5, 0) : { runs: [] }
|
|
45
|
+
if (!hist?.runs?.length) return { text: 'No MoA runs recorded in history.' }
|
|
46
|
+
return {
|
|
47
|
+
text: '### 📜 Recent MoA Runs:\n' + hist.runs.slice(0, 5).map((r) =>
|
|
48
|
+
`- \`${r.id?.slice(0, 8)}\` (${r.preset || 'default'}): "${r.prompt?.slice(0, 40)}..." (Winner: ${r.winnerModel || 'none'}, $${(r.totalCostUsd || 0).toFixed(4)})`
|
|
49
|
+
).join('\n'),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
text: `🧠 MoA turn dispatched: "${trimmed}". The turn will execute with live consensus streaming.`,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const unregister = commands.register({
|
|
59
|
+
definitionId: '@goodandready/dsh-moa',
|
|
60
|
+
name: 'moa',
|
|
61
|
+
description: 'Run Mixture of Agents consensus or inspect presets (/moa [presets|history|status|<prompt>])',
|
|
62
|
+
input: { hint: '[presets|history|status|<prompt>]' },
|
|
63
|
+
handler: commandHandler,
|
|
64
|
+
execute: async (rawArgs, invocation = null) => {
|
|
65
|
+
const res = await commandHandler(invocation || rawArgs)
|
|
66
|
+
return res?.text || String(res || '')
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const eff = typeof cmdCtx.effect === 'function' ? cmdCtx.effect : (fn) => fn()
|
|
71
|
+
eff(() => () => {
|
|
72
|
+
try {
|
|
73
|
+
if (typeof unregister === 'function') unregister()
|
|
74
|
+
} catch (_e) {}
|
|
75
|
+
}, 'dsh-moa: slash command registration')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (service(ctx, 'commands')?.register) {
|
|
79
|
+
registerWithService(ctx)
|
|
80
|
+
} else if (typeof ctx.inject === 'function') {
|
|
81
|
+
ctx.inject(['commands'], registerWithService)
|
|
82
|
+
}
|
|
83
|
+
}
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized LLM and MoA Pipeline Error Taxonomy and Classification
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class MoaError extends Error {
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} message
|
|
8
|
+
* @param {object} [options]
|
|
9
|
+
*/
|
|
10
|
+
constructor(message, options = {}) {
|
|
11
|
+
super(message)
|
|
12
|
+
this.name = new.target.name || 'MoaError'
|
|
13
|
+
this.code = options.code || 'MOA_ERROR'
|
|
14
|
+
this.status = typeof options.status === 'number' ? options.status : undefined
|
|
15
|
+
this.isRetryable = Boolean(options.isRetryable)
|
|
16
|
+
this.fallbackEligible = options.fallbackEligible !== false
|
|
17
|
+
this.cause = options.cause
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class RateLimitError extends MoaError {
|
|
22
|
+
constructor(message, options = {}) {
|
|
23
|
+
super(message, {
|
|
24
|
+
code: 'RATE_LIMIT_EXCEEDED',
|
|
25
|
+
status: 429,
|
|
26
|
+
isRetryable: true,
|
|
27
|
+
fallbackEligible: true,
|
|
28
|
+
...options,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class ContextOverflowError extends MoaError {
|
|
34
|
+
constructor(message, options = {}) {
|
|
35
|
+
super(message, {
|
|
36
|
+
code: 'CONTEXT_WINDOW_EXCEEDED',
|
|
37
|
+
status: 400,
|
|
38
|
+
isRetryable: false,
|
|
39
|
+
fallbackEligible: true,
|
|
40
|
+
...options,
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class AuthenticationError extends MoaError {
|
|
46
|
+
constructor(message, options = {}) {
|
|
47
|
+
super(message, {
|
|
48
|
+
code: 'AUTHENTICATION_FAILED',
|
|
49
|
+
status: options.status || 401,
|
|
50
|
+
isRetryable: false,
|
|
51
|
+
fallbackEligible: false,
|
|
52
|
+
...options,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class ModelNotFoundError extends MoaError {
|
|
58
|
+
constructor(message, options = {}) {
|
|
59
|
+
super(message, {
|
|
60
|
+
code: 'MODEL_NOT_FOUND',
|
|
61
|
+
status: 404,
|
|
62
|
+
isRetryable: false,
|
|
63
|
+
fallbackEligible: true,
|
|
64
|
+
...options,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class TransientNetworkError extends MoaError {
|
|
70
|
+
constructor(message, options = {}) {
|
|
71
|
+
super(message, {
|
|
72
|
+
code: 'TRANSIENT_NETWORK_ERROR',
|
|
73
|
+
status: options.status || 503,
|
|
74
|
+
isRetryable: true,
|
|
75
|
+
fallbackEligible: true,
|
|
76
|
+
...options,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Classifies an unknown error or exception into a structured MoA domain error.
|
|
83
|
+
*
|
|
84
|
+
* @param {unknown} err
|
|
85
|
+
* @returns {MoaError}
|
|
86
|
+
*/
|
|
87
|
+
export function classifyLlmError(err) {
|
|
88
|
+
if (!err) return new MoaError('Unknown error')
|
|
89
|
+
if (err instanceof MoaError) return err
|
|
90
|
+
|
|
91
|
+
const msg = String(err.message || err).toLowerCase()
|
|
92
|
+
const status = Number(err.status || err.statusCode || (msg.match(/\b(401|403|404|429|500|502|503|504)\b/)?.[1])) || undefined
|
|
93
|
+
const code = String(err.code || '').toUpperCase()
|
|
94
|
+
|
|
95
|
+
// Rate limit / Quota
|
|
96
|
+
if (
|
|
97
|
+
status === 429 ||
|
|
98
|
+
msg.includes('rate limit') ||
|
|
99
|
+
msg.includes('too many requests') ||
|
|
100
|
+
msg.includes('tpm') ||
|
|
101
|
+
msg.includes('rpm') ||
|
|
102
|
+
msg.includes('quota exceeded') ||
|
|
103
|
+
code.includes('RATE_LIMIT') ||
|
|
104
|
+
code.includes('QUOTA')
|
|
105
|
+
) {
|
|
106
|
+
return new RateLimitError(err.message || String(err), { status: 429, cause: err })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Context window overflow
|
|
110
|
+
if (
|
|
111
|
+
msg.includes('context window') ||
|
|
112
|
+
msg.includes('context length') ||
|
|
113
|
+
msg.includes('maximum context') ||
|
|
114
|
+
msg.includes('too long') ||
|
|
115
|
+
msg.includes('token limit') ||
|
|
116
|
+
code.includes('CONTEXT_WINDOW')
|
|
117
|
+
) {
|
|
118
|
+
return new ContextOverflowError(err.message || String(err), { status: 400, cause: err })
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Authentication & permissions
|
|
122
|
+
if (
|
|
123
|
+
status === 401 ||
|
|
124
|
+
status === 403 ||
|
|
125
|
+
msg.includes('unauthorized') ||
|
|
126
|
+
msg.includes('invalid api key') ||
|
|
127
|
+
msg.includes('forbidden') ||
|
|
128
|
+
msg.includes('authentication') ||
|
|
129
|
+
code.includes('AUTH') ||
|
|
130
|
+
code.includes('INVALID_CREDENTIAL')
|
|
131
|
+
) {
|
|
132
|
+
return new AuthenticationError(err.message || String(err), { status: status || 401, cause: err })
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Model not found / unavailable
|
|
136
|
+
if (
|
|
137
|
+
status === 404 ||
|
|
138
|
+
msg.includes('model not found') ||
|
|
139
|
+
msg.includes('does not exist') ||
|
|
140
|
+
msg.includes('unknown model') ||
|
|
141
|
+
code.includes('MODEL_NOT_FOUND')
|
|
142
|
+
) {
|
|
143
|
+
return new ModelNotFoundError(err.message || String(err), { status: 404, cause: err })
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Transient network / gateway / timeout
|
|
147
|
+
if (
|
|
148
|
+
status === 502 ||
|
|
149
|
+
status === 503 ||
|
|
150
|
+
status === 504 ||
|
|
151
|
+
msg.includes('timeout') ||
|
|
152
|
+
msg.includes('econnreset') ||
|
|
153
|
+
msg.includes('etimedout') ||
|
|
154
|
+
msg.includes('network') ||
|
|
155
|
+
msg.includes('socket hang up') ||
|
|
156
|
+
msg.includes('service unavailable') ||
|
|
157
|
+
msg.includes('bad gateway') ||
|
|
158
|
+
code.includes('TIMEOUT') ||
|
|
159
|
+
code.includes('ECONNRESET')
|
|
160
|
+
) {
|
|
161
|
+
return new TransientNetworkError(err.message || String(err), { status: status || 503, cause: err })
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return new MoaError(err.message || String(err), { status, cause: err })
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Returns true if the error qualifies for candidate failover to a reserve model.
|
|
169
|
+
*
|
|
170
|
+
* @param {unknown} err
|
|
171
|
+
* @returns {boolean}
|
|
172
|
+
*/
|
|
173
|
+
export function isFailoverEligible(err) {
|
|
174
|
+
const classified = classifyLlmError(err)
|
|
175
|
+
return classified.fallbackEligible
|
|
176
|
+
}
|
package/lib/events.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cordis Event Bus Definitions and Helpers for dsh-moa
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const MOA_EVENTS = {
|
|
6
|
+
RUN_START: 'moa/run-start',
|
|
7
|
+
CANDIDATE_COMPLETE: 'moa/candidate-complete',
|
|
8
|
+
CRITIQUE_COMPLETE: 'moa/critique-complete',
|
|
9
|
+
JUDGE_VERDICT: 'moa/judge-verdict',
|
|
10
|
+
RUN_FINISH: 'moa/run-finish',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Dispatches an event onto the Cordis event bus with best-effort error isolation.
|
|
15
|
+
* If ctx or ctx.emit is not available, or an event listener throws, the pipeline is not interrupted.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} [ctx] - Cordis context or mock context
|
|
18
|
+
* @param {string} eventName - One of MOA_EVENTS
|
|
19
|
+
* @param {object} payload - Event data payload
|
|
20
|
+
*/
|
|
21
|
+
export function emitEvent(ctx, eventName, payload) {
|
|
22
|
+
if (!ctx || !eventName) return
|
|
23
|
+
try {
|
|
24
|
+
if (typeof ctx.emit === 'function') {
|
|
25
|
+
ctx.emit(eventName, payload)
|
|
26
|
+
}
|
|
27
|
+
} catch (_err) {
|
|
28
|
+
/* best-effort event dispatch: listener exceptions must never abort pipeline */
|
|
29
|
+
}
|
|
30
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registerMoaCommands } from './commands.js'
|
|
1
2
|
import { service, getWebServerPort } from './services.js'
|
|
2
3
|
import crypto from 'node:crypto'
|
|
3
4
|
import path from 'node:path'
|
|
@@ -22,6 +23,9 @@ import { createLiveCanvasClient } from './live-canvas.js'
|
|
|
22
23
|
import { registerPluginUpdater } from './updater.js'
|
|
23
24
|
import { bestEffort } from './best-effort.js'
|
|
24
25
|
import { getMoaHistory } from './history.js'
|
|
26
|
+
import { runMoAPipeline } from './moa-runner.js'
|
|
27
|
+
import { MoaService } from './moa-service.js'
|
|
28
|
+
import { MOA_EVENTS, emitEvent } from './events.js'
|
|
25
29
|
|
|
26
30
|
export const name = '@goodandready/dsh-moa'
|
|
27
31
|
export { collectProjectContext, formatProjectContext } from './file-workspace.js'
|
|
@@ -132,6 +136,23 @@ export function apply(ctx, config) {
|
|
|
132
136
|
// required to be installed).
|
|
133
137
|
const liveCanvas = createLiveCanvasClient({ getPort: () => getWebServerPort(ctx) })
|
|
134
138
|
|
|
139
|
+
// Native Cordis service: ctx.moa
|
|
140
|
+
const moaService = new MoaService(ctx, {
|
|
141
|
+
live,
|
|
142
|
+
runMoAPipeline,
|
|
143
|
+
resolvePresetForPrompt,
|
|
144
|
+
getMoaHistory,
|
|
145
|
+
callLlm,
|
|
146
|
+
liveCanvas,
|
|
147
|
+
get webServerPort() { return getWebServerPort(ctx) },
|
|
148
|
+
get timeMachineEngine() { return service(ctx, 'timeMachineEngine') },
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
if (typeof ctx.provide === 'function') {
|
|
152
|
+
ctx.provide('moa', moaService)
|
|
153
|
+
}
|
|
154
|
+
ctx.moa = moaService
|
|
155
|
+
|
|
135
156
|
// Route: /dsh-moa/status & one-click updater
|
|
136
157
|
const registerWebSurfaces = (webCtx) => {
|
|
137
158
|
if (!webCtx) return
|
|
@@ -165,6 +186,9 @@ export function apply(ctx, config) {
|
|
|
165
186
|
ctx.inject(['webServer'], registerWebSurfaces)
|
|
166
187
|
}
|
|
167
188
|
|
|
189
|
+
// Official slash command /moa registration via dsh-commands (#275)
|
|
190
|
+
registerMoaCommands(ctx, { live, moaService })
|
|
191
|
+
|
|
168
192
|
// Session turn interceptor: route turn to real aggregator model & stream MoA synthesis
|
|
169
193
|
ctx.effect(() => {
|
|
170
194
|
return () => {
|
|
@@ -284,6 +308,7 @@ export function apply(ctx, config) {
|
|
|
284
308
|
liveCanvas,
|
|
285
309
|
webServerPort: getWebServerPort(ctx),
|
|
286
310
|
timeMachineEngine: service(ctx, 'timeMachineEngine') || ctx.reflect?.get?.('timeMachineEngine', false),
|
|
311
|
+
ctx,
|
|
287
312
|
}
|
|
288
313
|
|
|
289
314
|
if (event.signal) {
|
|
@@ -337,6 +362,8 @@ export function apply(ctx, config) {
|
|
|
337
362
|
}
|
|
338
363
|
|
|
339
364
|
export { runMoAPipeline } from './moa-runner.js'
|
|
365
|
+
export { MoaService } from './moa-service.js'
|
|
366
|
+
export { MOA_EVENTS, emitEvent } from './events.js'
|
|
340
367
|
export { executeTestGateForCandidates, runCandidateTestGate } from './moa-test-gate.js'
|
|
341
368
|
export {
|
|
342
369
|
COMPANION_PLUGINS,
|
|
@@ -347,3 +374,14 @@ export {
|
|
|
347
374
|
handleWorkbenchGet,
|
|
348
375
|
handleWorkbenchAssemble,
|
|
349
376
|
} from './moa-workbench.js'
|
|
377
|
+
export { registerMoaCommands } from './commands.js'
|
|
378
|
+
export {
|
|
379
|
+
MoaError,
|
|
380
|
+
RateLimitError,
|
|
381
|
+
ContextOverflowError,
|
|
382
|
+
AuthenticationError,
|
|
383
|
+
ModelNotFoundError,
|
|
384
|
+
TransientNetworkError,
|
|
385
|
+
classifyLlmError,
|
|
386
|
+
isFailoverEligible,
|
|
387
|
+
} from './errors.js'
|
package/lib/moa-candidates.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { classifyLlmError, isFailoverEligible } from './errors.js'
|
|
1
2
|
import { logger } from './logger.js'
|
|
2
3
|
/**
|
|
3
4
|
* DeepSeek Harness Mixture of Agents (MoA) — Candidate Fan-Out & Retry
|
|
@@ -18,14 +19,13 @@ export async function callWithTransientRetry(callLlmFn, callArgs, maxRetries = 0
|
|
|
18
19
|
return await callLlmFn(callArgs)
|
|
19
20
|
} catch (err) {
|
|
20
21
|
attempt++
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
if (attempt <= maxRetries && isTransient && !callArgs?.signal?.aborted) {
|
|
22
|
+
const classified = classifyLlmError(err)
|
|
23
|
+
if (attempt <= maxRetries && classified.isRetryable && !callArgs?.signal?.aborted) {
|
|
24
24
|
await new Promise((r) => setTimeout(r, retryDelayMs))
|
|
25
25
|
if (callArgs?.signal?.aborted) throw new Error('Aborted')
|
|
26
26
|
continue
|
|
27
27
|
}
|
|
28
|
-
throw
|
|
28
|
+
throw classified
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
}
|
|
@@ -100,6 +100,7 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
100
100
|
|
|
101
101
|
const runOne = async () => {
|
|
102
102
|
let candidateTimer = null
|
|
103
|
+
const candidateStart = Date.now()
|
|
103
104
|
try {
|
|
104
105
|
const callPromise = callWithTransientRetry(
|
|
105
106
|
callLlm,
|
|
@@ -144,6 +145,8 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
144
145
|
text,
|
|
145
146
|
usage: costInfo,
|
|
146
147
|
costUsd: costInfo.costUsd,
|
|
148
|
+
durationMs: Date.now() - candidateStart,
|
|
149
|
+
model: slot?.model ? (slot.provider ? (slot.provider + ":" + slot.model) : slot.model) : label,
|
|
147
150
|
ok: true,
|
|
148
151
|
}
|
|
149
152
|
|
|
@@ -173,10 +176,29 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
173
176
|
return
|
|
174
177
|
}
|
|
175
178
|
|
|
176
|
-
|
|
179
|
+
const classified = classifyLlmError(err)
|
|
180
|
+
const canFailover = isFailoverEligible(classified)
|
|
181
|
+
|
|
182
|
+
// Reserve model failover: checks slot.fallbacks, options.fallback_models, or options.local_fallback_models
|
|
177
183
|
let fallbackRecovered = false
|
|
178
|
-
|
|
179
|
-
|
|
184
|
+
const fallbackCandidates = [
|
|
185
|
+
...(Array.isArray(slot?.fallbacks) ? slot.fallbacks : []),
|
|
186
|
+
...(Array.isArray(options.fallback_models) ? options.fallback_models : []),
|
|
187
|
+
...(options.local_fallback_enabled && Array.isArray(options.local_fallback_models) ? options.local_fallback_models : []),
|
|
188
|
+
].filter(Boolean)
|
|
189
|
+
|
|
190
|
+
const seenFb = new Set([`${slot.provider}:${slot.model}`])
|
|
191
|
+
const eligibleFallbacks = []
|
|
192
|
+
for (const fb of fallbackCandidates) {
|
|
193
|
+
const key = `${fb.provider}:${fb.model}`
|
|
194
|
+
if (!seenFb.has(key)) {
|
|
195
|
+
seenFb.add(key)
|
|
196
|
+
eligibleFallbacks.push(fb)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (canFailover && eligibleFallbacks.length > 0) {
|
|
201
|
+
for (const fbSlot of eligibleFallbacks) {
|
|
180
202
|
if (options.signal?.aborted) break
|
|
181
203
|
const fbLabel = slotLabel(fbSlot)
|
|
182
204
|
const fbAbortCtrl = new AbortController()
|
|
@@ -195,7 +217,7 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
195
217
|
|
|
196
218
|
try {
|
|
197
219
|
if (typeof onProgress === 'function') {
|
|
198
|
-
onProgress(`🔄 *Candidate ${i + 1} (${label})
|
|
220
|
+
onProgress(`🔄 *Candidate ${i + 1} (${label}) hit ${classified.name}, failover to ${fbLabel}...*\n`)
|
|
199
221
|
}
|
|
200
222
|
const fbCallPromise = callWithTransientRetry(
|
|
201
223
|
callLlm,
|
|
@@ -240,14 +262,15 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
240
262
|
ok: true,
|
|
241
263
|
was_fallback: true,
|
|
242
264
|
original_model: label,
|
|
265
|
+
failoverReason: classified.code,
|
|
243
266
|
}
|
|
244
267
|
fallbackRecovered = true
|
|
245
268
|
if (typeof onProgress === 'function') {
|
|
246
|
-
onProgress(`✅ *Candidate ${i + 1}/${total} recovered using
|
|
269
|
+
onProgress(`✅ *Candidate ${i + 1}/${total} recovered using failover (${fbLabel})*\n`)
|
|
247
270
|
}
|
|
248
271
|
break
|
|
249
272
|
} catch (fbErr) {
|
|
250
|
-
logger.warn(`[dsh-moa]
|
|
273
|
+
logger.warn(`[dsh-moa] Reserve model ${fbLabel} also failed:`, fbErr?.message || fbErr)
|
|
251
274
|
} finally {
|
|
252
275
|
if (fbTimer) clearTimeout(fbTimer)
|
|
253
276
|
if (removeParentAbort) removeParentAbort()
|
|
@@ -257,7 +280,7 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
257
280
|
|
|
258
281
|
if (!fallbackRecovered) {
|
|
259
282
|
finishedCount++
|
|
260
|
-
const errMsg = err?.message || String(err)
|
|
283
|
+
const errMsg = classified?.message || err?.message || String(err)
|
|
261
284
|
logger.warn(`[dsh-moa] Reference ${label} failed:`, errMsg)
|
|
262
285
|
if (typeof onProgress === 'function') {
|
|
263
286
|
onProgress(`⚠️ *Candidate ${i + 1}/${total} (${label}) error: ${errMsg}*\n`)
|
|
@@ -273,6 +296,8 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
273
296
|
costUsd: 0,
|
|
274
297
|
ok: false,
|
|
275
298
|
error: errMsg,
|
|
299
|
+
errorClass: classified.name,
|
|
300
|
+
errorCode: classified.code,
|
|
276
301
|
}
|
|
277
302
|
}
|
|
278
303
|
} finally {
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Cordis Service: ctx.moa
|
|
3
|
+
* Allows in-process MoA consensus pipeline execution and telemetry by peer plugins.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { emitEvent, MOA_EVENTS } from './events.js'
|
|
7
|
+
|
|
8
|
+
export class MoaService {
|
|
9
|
+
/**
|
|
10
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
11
|
+
* @param {object} [deps={}]
|
|
12
|
+
*/
|
|
13
|
+
constructor(ctx, deps = {}) {
|
|
14
|
+
this.ctx = ctx
|
|
15
|
+
this.deps = deps
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get live() {
|
|
19
|
+
return typeof this.deps.live === 'function' ? this.deps.live() : (this.deps.live || {})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get all active configured presets
|
|
24
|
+
* @returns {Array<object>}
|
|
25
|
+
*/
|
|
26
|
+
getPresets() {
|
|
27
|
+
const cfg = this.live
|
|
28
|
+
return Array.isArray(cfg?.presets) ? [...cfg.presets] : []
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get preset by ID or name
|
|
33
|
+
* @param {string} [idOrName]
|
|
34
|
+
* @returns {object|null}
|
|
35
|
+
*/
|
|
36
|
+
getPreset(idOrName) {
|
|
37
|
+
const presets = this.getPresets()
|
|
38
|
+
if (!idOrName) return presets[0] || null
|
|
39
|
+
return presets.find((p) => p.id === idOrName || p.name === idOrName) || presets[0] || null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get historical MoA runs
|
|
44
|
+
* @param {number} [limit=20]
|
|
45
|
+
* @param {number} [offset=0]
|
|
46
|
+
* @returns {object}
|
|
47
|
+
*/
|
|
48
|
+
getHistory(limit = 20, offset = 0) {
|
|
49
|
+
const getHistoryFn = this.deps.getMoaHistory
|
|
50
|
+
if (typeof getHistoryFn === 'function') {
|
|
51
|
+
return getHistoryFn(limit, offset)
|
|
52
|
+
}
|
|
53
|
+
return { runs: [], total: 0 }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve best preset for a given user prompt
|
|
58
|
+
* @param {string} prompt
|
|
59
|
+
* @returns {Promise<object>}
|
|
60
|
+
*/
|
|
61
|
+
async resolvePresetForPrompt(prompt) {
|
|
62
|
+
const resolveFn = this.deps.resolvePresetForPrompt
|
|
63
|
+
if (typeof resolveFn === 'function') {
|
|
64
|
+
return resolveFn({ prompt, live: () => this.live, callLlm: this.deps.callLlm })
|
|
65
|
+
}
|
|
66
|
+
return { preset: this.getPreset() }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Run MoA consensus pipeline in-process
|
|
71
|
+
* @param {object} options
|
|
72
|
+
* @returns {Promise<object>}
|
|
73
|
+
*/
|
|
74
|
+
async run(options = {}) {
|
|
75
|
+
const runFn = this.deps.runMoAPipeline
|
|
76
|
+
if (typeof runFn !== 'function') {
|
|
77
|
+
throw new Error('[dsh-moa] runMoAPipeline is not available in MoaService')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const {
|
|
81
|
+
prompt,
|
|
82
|
+
userPrompt,
|
|
83
|
+
messages = [],
|
|
84
|
+
preset: customPreset,
|
|
85
|
+
presetId,
|
|
86
|
+
cwd = process.cwd(),
|
|
87
|
+
signal,
|
|
88
|
+
onProgress,
|
|
89
|
+
onStreamDelta,
|
|
90
|
+
checkpoint,
|
|
91
|
+
force,
|
|
92
|
+
testGateEnabled,
|
|
93
|
+
testCommand,
|
|
94
|
+
} = options
|
|
95
|
+
|
|
96
|
+
const effectivePrompt = prompt || userPrompt || (messages[messages.length - 1]?.content) || ''
|
|
97
|
+
if (!effectivePrompt && messages.length === 0) {
|
|
98
|
+
throw new Error('[dsh-moa] prompt or userPrompt is required to run MoA')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let preset = customPreset
|
|
102
|
+
if (!preset) {
|
|
103
|
+
if (presetId) {
|
|
104
|
+
preset = this.getPreset(presetId)
|
|
105
|
+
} else {
|
|
106
|
+
const routed = await this.resolvePresetForPrompt(effectivePrompt)
|
|
107
|
+
preset = routed?.preset || this.getPreset()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const cfg = this.live
|
|
112
|
+
return runFn({
|
|
113
|
+
userPrompt: effectivePrompt,
|
|
114
|
+
messages,
|
|
115
|
+
preset,
|
|
116
|
+
callLlm: options.callLlm || this.deps.callLlm,
|
|
117
|
+
cwd,
|
|
118
|
+
prices: options.prices || cfg?.prices || this.deps.prices || {},
|
|
119
|
+
onProgress,
|
|
120
|
+
onStreamDelta,
|
|
121
|
+
signal,
|
|
122
|
+
checkpoint,
|
|
123
|
+
force,
|
|
124
|
+
testGateEnabled,
|
|
125
|
+
testCommand,
|
|
126
|
+
liveCanvas: options.liveCanvas || this.deps.liveCanvas,
|
|
127
|
+
webServerPort: options.webServerPort || this.deps.webServerPort,
|
|
128
|
+
timeMachineEngine: options.timeMachineEngine || this.deps.timeMachineEngine,
|
|
129
|
+
historyFilePath: options.historyFilePath || this.deps.historyFilePath,
|
|
130
|
+
ctx: this.ctx,
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Attach listener to MoA events on the underlying Cordis Context
|
|
136
|
+
* @param {string} event
|
|
137
|
+
* @param {Function} listener
|
|
138
|
+
* @returns {Function|undefined}
|
|
139
|
+
*/
|
|
140
|
+
on(event, listener) {
|
|
141
|
+
if (typeof this.ctx?.on === 'function') {
|
|
142
|
+
return this.ctx.on(event, listener)
|
|
143
|
+
}
|
|
144
|
+
return undefined
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Emit an event on the Cordis event bus
|
|
149
|
+
* @param {string} event
|
|
150
|
+
* @param {any} payload
|
|
151
|
+
*/
|
|
152
|
+
emit(event, payload) {
|
|
153
|
+
emitEvent(this.ctx, event, payload)
|
|
154
|
+
}
|
|
155
|
+
}
|
package/lib/moa-stream.js
CHANGED
|
@@ -11,7 +11,7 @@ import { runMoAPipeline } from './moa-runner.js'
|
|
|
11
11
|
/**
|
|
12
12
|
* Streams a full MoA turn into chat with live aggregator tokens and progress feedback.
|
|
13
13
|
*/
|
|
14
|
-
export async function* streamMoATurn({ targetPreset, userPrompt, messages, callLlm, cwd, prices, historyFilePath, liveCanvas, webServerPort, timeMachineEngine, force }, options = {}) {
|
|
14
|
+
export async function* streamMoATurn({ targetPreset, userPrompt, messages, callLlm, cwd, prices, historyFilePath, liveCanvas, webServerPort, timeMachineEngine, force, ctx }, options = {}) {
|
|
15
15
|
const signal = options?.signal
|
|
16
16
|
const runId = options?.runId || null
|
|
17
17
|
if (signal?.aborted) return
|
|
@@ -58,6 +58,7 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
|
|
|
58
58
|
force,
|
|
59
59
|
signal: combinedSignal,
|
|
60
60
|
runId,
|
|
61
|
+
ctx,
|
|
61
62
|
})
|
|
62
63
|
.catch((err) => ({ error: err }))
|
|
63
64
|
.finally(() => {
|
package/lib/pipeline/context.js
CHANGED
|
@@ -14,8 +14,10 @@ export async function stageProjectContext({
|
|
|
14
14
|
startTime,
|
|
15
15
|
onProgress,
|
|
16
16
|
}) {
|
|
17
|
-
const
|
|
18
|
-
? preset.reference_models
|
|
17
|
+
const rawRefs = (Array.isArray(preset?.reference_models) && preset.reference_models.length > 0)
|
|
18
|
+
? preset.reference_models
|
|
19
|
+
: (Array.isArray(preset?.references) && preset.references.length > 0 ? preset.references : [])
|
|
20
|
+
const referenceModels = rawRefs.length > 0 ? rawRefs : [{ provider: '', model: '' }]
|
|
19
21
|
const primaryJudge = preset?.aggregator?.provider && preset?.aggregator?.model
|
|
20
22
|
? preset.aggregator : { provider: '', model: '' }
|
|
21
23
|
const fallbackJudges = Array.isArray(preset?.aggregator_fallbacks) ? preset.aggregator_fallbacks : []
|
|
@@ -2,6 +2,7 @@ import { logger } from '../logger.js'
|
|
|
2
2
|
import { promoteCandidateWorkspace, cleanMoaWorkspaces } from '../workspace/fs.js'
|
|
3
3
|
import { createPromotedPreview } from '../live-canvas.js'
|
|
4
4
|
import { recordMoaRunAsync, candidatesForHistory } from '../history.js'
|
|
5
|
+
import { emitEvent, MOA_EVENTS } from '../events.js'
|
|
5
6
|
|
|
6
7
|
export async function stageFastMode({
|
|
7
8
|
successfulRefs,
|
|
@@ -18,6 +19,7 @@ export async function stageFastMode({
|
|
|
18
19
|
startTime,
|
|
19
20
|
collectedCtx,
|
|
20
21
|
liveCanvas,
|
|
22
|
+
ctx = null,
|
|
21
23
|
}) {
|
|
22
24
|
if (signal?.aborted) {
|
|
23
25
|
await cleanMoaWorkspaces(cwd, { runId })
|
|
@@ -51,6 +53,9 @@ export async function stageFastMode({
|
|
|
51
53
|
} catch (histErr) {
|
|
52
54
|
logger.warn('[dsh-moa] Failed to record fast synthesis failure in history:', histErr?.message || histErr)
|
|
53
55
|
}
|
|
56
|
+
emitEvent(ctx, MOA_EVENTS.RUN_FINISH, {
|
|
57
|
+
runId, durationMs, totalTokens, totalCostUsd, promotedFiles: [], winnerIndex: 0, error: promoteErr?.message || String(promoteErr),
|
|
58
|
+
})
|
|
54
59
|
return {
|
|
55
60
|
ok: false,
|
|
56
61
|
payload: {
|
|
@@ -87,6 +92,9 @@ export async function stageFastMode({
|
|
|
87
92
|
logger.warn('[dsh-moa] Failed to record fast synthesis run in history:', histErr?.message || histErr)
|
|
88
93
|
}
|
|
89
94
|
|
|
95
|
+
emitEvent(ctx, MOA_EVENTS.RUN_FINISH, {
|
|
96
|
+
runId, durationMs, totalTokens, totalCostUsd, promotedFiles, winnerIndex: single.index,
|
|
97
|
+
})
|
|
90
98
|
return {
|
|
91
99
|
ok: true,
|
|
92
100
|
payload: {
|
|
@@ -7,6 +7,7 @@ import { generateMoABenchmarkReport } from '../moa-report.js'
|
|
|
7
7
|
import { parseFileScores } from '../moa-workbench.js'
|
|
8
8
|
import { recordMoaRunAsync, candidatesForHistory } from '../history.js'
|
|
9
9
|
import { slotLabel } from '../moa-prompts.js'
|
|
10
|
+
import { emitEvent, MOA_EVENTS } from '../events.js'
|
|
10
11
|
|
|
11
12
|
export async function stagePromotion({
|
|
12
13
|
synthesizedText,
|
|
@@ -31,6 +32,7 @@ export async function stagePromotion({
|
|
|
31
32
|
referenceModels,
|
|
32
33
|
historyFilePath,
|
|
33
34
|
collectedCtx,
|
|
35
|
+
ctx = null,
|
|
34
36
|
}) {
|
|
35
37
|
const winningIndex = (multiJudgeResult?.consensus && multiJudgeResult?.winningCandidateIndex)
|
|
36
38
|
? multiJudgeResult.winningCandidateIndex
|
|
@@ -89,6 +91,13 @@ export async function stagePromotion({
|
|
|
89
91
|
? parseFileScores(synthesizedText, referenceOutputs)
|
|
90
92
|
: null
|
|
91
93
|
|
|
94
|
+
emitEvent(ctx, MOA_EVENTS.JUDGE_VERDICT, {
|
|
95
|
+
runId,
|
|
96
|
+
winningIndex,
|
|
97
|
+
winningModel: winnerModel,
|
|
98
|
+
scores: fileScores || multiJudgeResult?.scores || null,
|
|
99
|
+
})
|
|
100
|
+
|
|
92
101
|
try {
|
|
93
102
|
await recordMoaRunAsync({
|
|
94
103
|
id: runId, cwd, prompt: userPrompt, preset: preset?.name || 'default', isRefinement,
|
|
@@ -101,6 +110,15 @@ export async function stagePromotion({
|
|
|
101
110
|
logger.warn('[dsh-moa] Failed to record run in history:', histErr)
|
|
102
111
|
}
|
|
103
112
|
|
|
113
|
+
emitEvent(ctx, MOA_EVENTS.RUN_FINISH, {
|
|
114
|
+
runId,
|
|
115
|
+
durationMs,
|
|
116
|
+
totalTokens: usageSummary.totalTokens,
|
|
117
|
+
totalCostUsd: usageSummary.totalCostUsd,
|
|
118
|
+
promotedFiles,
|
|
119
|
+
winnerIndex: winningIndex,
|
|
120
|
+
})
|
|
121
|
+
|
|
104
122
|
return {
|
|
105
123
|
ok: true,
|
|
106
124
|
payload: {
|
package/lib/pipeline/runner.js
CHANGED
|
@@ -11,6 +11,7 @@ import { stagePeerCritique } from './peer-critique.js'
|
|
|
11
11
|
import { stageMultiJudge } from './multi-judge.js'
|
|
12
12
|
import { stageSynthesis } from './synthesis.js'
|
|
13
13
|
import { stagePromotion } from './promotion.js'
|
|
14
|
+
import { emitEvent, MOA_EVENTS } from '../events.js'
|
|
14
15
|
|
|
15
16
|
export async function runMoAPipeline({
|
|
16
17
|
userPrompt,
|
|
@@ -22,12 +23,21 @@ export async function runMoAPipeline({
|
|
|
22
23
|
testGateEnabled = null, testCommand = null, execFn = null,
|
|
23
24
|
webServerPort = null, timeMachineEngine = null, timeMachineUrl = null,
|
|
24
25
|
force = isTestEnvironment() ? true : false, checkpoint = true, checkpointFn = null, runId: customRunId = null,
|
|
26
|
+
ctx = null,
|
|
25
27
|
}) {
|
|
26
28
|
const runId = (customRunId && String(customRunId).trim()) || crypto.randomUUID()
|
|
27
29
|
const promoteOpts = { webServerPort, timeMachineEngine, timeMachineUrl, force: Boolean(force), checkpoint, checkpointFn, signal }
|
|
28
30
|
if (signal?.aborted) return { error: new Error('Turn aborted') }
|
|
29
31
|
const startTime = Date.now()
|
|
30
32
|
|
|
33
|
+
emitEvent(ctx, MOA_EVENTS.RUN_START, {
|
|
34
|
+
runId,
|
|
35
|
+
prompt: userPrompt,
|
|
36
|
+
preset: preset?.name || preset?.id || 'default',
|
|
37
|
+
candidatesCount: Array.isArray(preset?.references) ? preset.references.length : 0,
|
|
38
|
+
timestamp: new Date(startTime).toISOString(),
|
|
39
|
+
})
|
|
40
|
+
|
|
31
41
|
// Stage 1: Context & Configuration
|
|
32
42
|
const ctxStage = await stageProjectContext({
|
|
33
43
|
userPrompt, messages, preset, cwd, prices, maxTokens: preset?.max_tokens, startTime, onProgress,
|
|
@@ -54,10 +64,30 @@ export async function runMoAPipeline({
|
|
|
54
64
|
})
|
|
55
65
|
if (!candStage.ok) {
|
|
56
66
|
if (candStage.abort) return { error: candStage.error }
|
|
67
|
+
emitEvent(ctx, MOA_EVENTS.RUN_FINISH, {
|
|
68
|
+
runId,
|
|
69
|
+
durationMs: Date.now() - startTime,
|
|
70
|
+
totalTokens: candStage.failurePayload?.usage?.totalTokens || 0,
|
|
71
|
+
totalCostUsd: candStage.failurePayload?.usage?.totalCostUsd || 0,
|
|
72
|
+
promotedFiles: [],
|
|
73
|
+
winnerIndex: 0,
|
|
74
|
+
error: candStage.failurePayload?.content,
|
|
75
|
+
})
|
|
57
76
|
return candStage.failurePayload
|
|
58
77
|
}
|
|
59
78
|
const { referenceOutputs, successfulRefs } = candStage
|
|
60
79
|
|
|
80
|
+
for (const ref of referenceOutputs) {
|
|
81
|
+
emitEvent(ctx, MOA_EVENTS.CANDIDATE_COMPLETE, {
|
|
82
|
+
runId,
|
|
83
|
+
round: 1,
|
|
84
|
+
model: ref.model || ref.label,
|
|
85
|
+
success: Boolean(ref.ok),
|
|
86
|
+
durationMs: ref.durationMs || 0,
|
|
87
|
+
tokens: ref.usage?.totalTokens || 0,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
61
91
|
// Stage 3: Clarifying Questions Branch
|
|
62
92
|
if (ctxStage.needsQuestions) {
|
|
63
93
|
return await stageClarifyingQuestions({
|
|
@@ -75,7 +105,7 @@ export async function runMoAPipeline({
|
|
|
75
105
|
allowCandidateOverride: ctxStage.allowCandidateOverride,
|
|
76
106
|
promoteOpts, cwd, runId, signal, userPrompt, preset,
|
|
77
107
|
isRefinement: ctxStage.isRefinement, historyFilePath, startTime,
|
|
78
|
-
collectedCtx: ctxStage.collectedCtx, liveCanvas,
|
|
108
|
+
collectedCtx: ctxStage.collectedCtx, liveCanvas, ctx,
|
|
79
109
|
})
|
|
80
110
|
if (!fastRes.ok) {
|
|
81
111
|
if (fastRes.abort) return { error: fastRes.error }
|
|
@@ -102,6 +132,12 @@ export async function runMoAPipeline({
|
|
|
102
132
|
options: { testGateEnabled, testCommand, execFn },
|
|
103
133
|
})
|
|
104
134
|
if (!peerRes.ok) return peerRes.abortPayload
|
|
135
|
+
if (ctxStage.isPeerCritiqueEnabled && peerRes.ok && peerRes.critiquesCount > 0) {
|
|
136
|
+
emitEvent(ctx, MOA_EVENTS.CRITIQUE_COMPLETE, {
|
|
137
|
+
runId,
|
|
138
|
+
critiquesCount: peerRes.critiquesCount,
|
|
139
|
+
})
|
|
140
|
+
}
|
|
105
141
|
|
|
106
142
|
if (signal?.aborted) {
|
|
107
143
|
await cleanMoaWorkspaces(cwd, { runId })
|
|
@@ -153,7 +189,7 @@ export async function runMoAPipeline({
|
|
|
153
189
|
promoteOpts, cwd, runId, signal, liveCanvas, startTime, chosenJudge, aggUsage,
|
|
154
190
|
userPrompt, preset, isRefinement: ctxStage.isRefinement, isFastMode: ctxStage.isFastMode,
|
|
155
191
|
effectiveReferences: ctxStage.effectiveReferences, referenceModels: ctxStage.referenceModels,
|
|
156
|
-
historyFilePath, collectedCtx: ctxStage.collectedCtx,
|
|
192
|
+
historyFilePath, collectedCtx: ctxStage.collectedCtx, ctx,
|
|
157
193
|
})
|
|
158
194
|
if (!promRes.ok) {
|
|
159
195
|
if (promRes.abort) return { error: promRes.error }
|
package/lib/routes/pipeline.js
CHANGED