@goodandready/dsh-moa 0.2.63 → 0.2.64

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/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
@@ -22,6 +22,9 @@ import { createLiveCanvasClient } from './live-canvas.js'
22
22
  import { registerPluginUpdater } from './updater.js'
23
23
  import { bestEffort } from './best-effort.js'
24
24
  import { getMoaHistory } from './history.js'
25
+ import { runMoAPipeline } from './moa-runner.js'
26
+ import { MoaService } from './moa-service.js'
27
+ import { MOA_EVENTS, emitEvent } from './events.js'
25
28
 
26
29
  export const name = '@goodandready/dsh-moa'
27
30
  export { collectProjectContext, formatProjectContext } from './file-workspace.js'
@@ -132,6 +135,23 @@ export function apply(ctx, config) {
132
135
  // required to be installed).
133
136
  const liveCanvas = createLiveCanvasClient({ getPort: () => getWebServerPort(ctx) })
134
137
 
138
+ // Native Cordis service: ctx.moa
139
+ const moaService = new MoaService(ctx, {
140
+ live,
141
+ runMoAPipeline,
142
+ resolvePresetForPrompt,
143
+ getMoaHistory,
144
+ callLlm,
145
+ liveCanvas,
146
+ get webServerPort() { return getWebServerPort(ctx) },
147
+ get timeMachineEngine() { return service(ctx, 'timeMachineEngine') },
148
+ })
149
+
150
+ if (typeof ctx.provide === 'function') {
151
+ ctx.provide('moa', moaService)
152
+ }
153
+ ctx.moa = moaService
154
+
135
155
  // Route: /dsh-moa/status & one-click updater
136
156
  const registerWebSurfaces = (webCtx) => {
137
157
  if (!webCtx) return
@@ -284,6 +304,7 @@ export function apply(ctx, config) {
284
304
  liveCanvas,
285
305
  webServerPort: getWebServerPort(ctx),
286
306
  timeMachineEngine: service(ctx, 'timeMachineEngine') || ctx.reflect?.get?.('timeMachineEngine', false),
307
+ ctx,
287
308
  }
288
309
 
289
310
  if (event.signal) {
@@ -337,6 +358,8 @@ export function apply(ctx, config) {
337
358
  }
338
359
 
339
360
  export { runMoAPipeline } from './moa-runner.js'
361
+ export { MoaService } from './moa-service.js'
362
+ export { MOA_EVENTS, emitEvent } from './events.js'
340
363
  export { executeTestGateForCandidates, runCandidateTestGate } from './moa-test-gate.js'
341
364
  export {
342
365
  COMPANION_PLUGINS,
@@ -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
 
@@ -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(() => {
@@ -14,8 +14,10 @@ export async function stageProjectContext({
14
14
  startTime,
15
15
  onProgress,
16
16
  }) {
17
- const referenceModels = Array.isArray(preset?.reference_models) && preset.reference_models.length > 0
18
- ? preset.reference_models : [{ provider: '', model: '' }]
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: {
@@ -109,5 +109,5 @@ export async function stagePeerCritique({
109
109
  })
110
110
  }
111
111
 
112
- return { ok: true }
112
+ return { ok: true, critiquesCount: runRound2 ? successfulRefs.length : 0 }
113
113
  }
@@ -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: {
@@ -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 }
@@ -73,6 +73,7 @@ export function registerPipelineRoutes(ctx, { live, callLlm, liveCanvas, webServ
73
73
  timeMachineUrl: payload.timeMachineUrl, force: false,
74
74
  checkpoint: true,
75
75
  runId: payload.runId,
76
+ ctx,
76
77
  })
77
78
 
78
79
  if (result?.skippedFiles > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.63",
3
+ "version": "0.2.64",
4
4
  "description": "Mixture of Agents (MoA) plugin for DeepSeek Harness with /moa slash command",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",