@goodandready/dsh-moa 0.2.3 → 0.2.5

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/index.js CHANGED
@@ -1,218 +1,236 @@
1
- import os from 'node:os'
2
- import path from 'node:path'
3
- import z from '@deepseek-ai/schemastery'
4
- import { runMoAPipeline, parseMoACommand, formatMoAResponse, MoaRunnerAdapter } from './moa-runner.js'
1
+ import { refreshCatalogInBackground } from './pricing.js'
2
+ /**
3
+ * DeepSeek Harness Mixture of Agents (MoA) Plugin
4
+ *
5
+ * Implements an intelligent ensemble pipeline:
6
+ * 1. User issues /moa <prompt>
7
+ * 2. Parallel candidate proposers write to isolated workspaces (.moa/candidate-X/)
8
+ * 3. Aggregator evaluates and synthesizes the optimal solution
9
+ * 4. Winner candidate files are promoted to the project root and previewed in Live Canvas
10
+ * 5. Native cost tracking, history logging and leaderboard analytics
11
+ */
5
12
 
6
- export const name = '@goodandready/dsh-moa'
7
- export const inject = ['settings', 'webServer', 'llm', 'credentials', 'sessions', 'agents']
8
-
9
- export { parseMoACommand }
13
+ import z from '@deepseek-ai/schemastery'
14
+ import path from 'node:path'
15
+ import os from 'node:os'
16
+ import {
17
+ parseMoACommand,
18
+ runMoAPipeline,
19
+ MoaRunnerAdapter,
20
+ } from './moa-runner.js'
21
+
22
+ import {
23
+ getMoaHistory,
24
+ getMoaLeaderboard,
25
+ getMoaRunById,
26
+ } from './history.js'
27
+
28
+ export const name = 'dsh-moa'
29
+ export const inject = ['webServer', 'llm', 'settings', 'sessions', 'tools']
30
+
31
+ export const NS = 'dsh-moa'
32
+
33
+ export const PriceRow = z.object({
34
+ input: z.number().default(0),
35
+ output: z.number().default(0),
36
+ cacheHit: z.number().default(0),
37
+ })
10
38
 
11
39
  export const ModelSlotSchema = z.object({
12
- provider: z.string().default(''),
13
- model: z.string().default(''),
40
+ provider: z.string().default('opencode-go'),
41
+ model: z.string().default('deepseek-v4-flash'),
14
42
  })
15
43
 
16
44
  export const PresetSchema = z.object({
17
- name: z.string().default('default'),
45
+ name: z.string(),
18
46
  enabled: z.boolean().default(true),
19
- reference_models: z.array(ModelSlotSchema).default([
20
- { provider: 'opencode-go', model: 'deepseek-v4-flash' },
21
- { provider: 'codex', model: 'gpt-5.6-sol' },
22
- ]),
23
- aggregator: ModelSlotSchema.default({
24
- provider: 'antigravity',
25
- model: 'gemini-3-flash',
26
- }),
27
- reference_temperature: z.number().step(0.05).min(0).max(2).default(0.6),
28
- aggregator_temperature: z.number().step(0.05).min(0).max(2).default(0.4),
29
- max_tokens: z.number().min(256).max(65536).default(4096),
47
+ reference_models: z.array(ModelSlotSchema).default([]),
48
+ aggregator: ModelSlotSchema.default({ provider: 'codex', model: 'gpt-5.6-sol' }),
49
+ reference_temperature: z.number().default(0.6),
50
+ aggregator_temperature: z.number().default(0.4),
51
+ max_tokens: z.number().default(4096),
52
+ judge_criteria: z.string().default(''),
53
+ judge_mode: z.string().default('auto'),
30
54
  })
31
55
 
32
56
  export const Config = z.object({
33
57
  enabled: z.boolean().default(true),
34
58
  default_preset: z.string().default('default'),
59
+ prices: z.dict(PriceRow).default({}),
35
60
  presets: z.array(PresetSchema).default([
36
61
  {
37
62
  name: 'default',
38
63
  enabled: true,
39
64
  reference_models: [
40
65
  { provider: 'opencode-go', model: 'deepseek-v4-flash' },
41
- { provider: 'codex', model: 'gpt-5.6-sol' },
66
+ { provider: 'grok', model: 'grok-build-0.1' },
42
67
  ],
43
- aggregator: {
44
- provider: 'antigravity',
45
- model: 'gemini-3-flash',
46
- },
68
+ aggregator: { provider: 'codex', model: 'gpt-5.6-sol' },
47
69
  reference_temperature: 0.6,
48
70
  aggregator_temperature: 0.4,
49
71
  max_tokens: 4096,
72
+ judge_criteria: '',
73
+ judge_mode: 'auto',
74
+ },
75
+ {
76
+ name: 'fast',
77
+ enabled: true,
78
+ reference_models: [
79
+ { provider: 'opencode-go', model: 'deepseek-v4-flash' },
80
+ ],
81
+ aggregator: { provider: 'opencode-go', model: 'deepseek-v4-flash' },
82
+ reference_temperature: 0.6,
83
+ aggregator_temperature: 0.2,
84
+ max_tokens: 4096,
85
+ judge_criteria: '',
86
+ judge_mode: 'auto',
50
87
  },
51
88
  {
52
89
  name: 'deep-reasoning',
53
90
  enabled: true,
54
91
  reference_models: [
55
- { provider: 'grok', model: 'grok-4.20-0309-reasoning' },
56
- { provider: 'codex', model: 'gpt-5.6-terra' },
92
+ { provider: 'grok', model: 'grok-build-0.1' },
93
+ { provider: 'commandcode', model: 'deepseek/deepseek-v4-flash' },
57
94
  ],
58
- aggregator: {
59
- provider: 'antigravity',
60
- model: 'gemini-3-flash',
61
- },
62
- reference_temperature: 0.7,
95
+ aggregator: { provider: 'codex', model: 'gpt-5.6-sol' },
96
+ reference_temperature: 0.6,
63
97
  aggregator_temperature: 0.2,
64
98
  max_tokens: 8192,
99
+ judge_criteria: 'Приоритет: математическая строгость и отсутствие галлюцинаций',
100
+ judge_mode: 'auto',
65
101
  },
66
102
  ]),
67
103
  })
68
104
 
69
- const NS = 'dsh-moa'
70
-
71
- function writeJson(res, code, data) {
72
- try {
73
- res.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
74
- res.end(JSON.stringify(data))
75
- } catch {
76
- /* socket closed */
77
- }
105
+ function writeJson(res, status, body) {
106
+ res.writeHead(status, {
107
+ 'Content-Type': 'application/json; charset=utf-8',
108
+ 'Cache-Control': 'no-store',
109
+ })
110
+ res.end(JSON.stringify(body))
78
111
  }
79
112
 
80
113
  function readBody(req) {
81
114
  return new Promise((resolve, reject) => {
82
- const chunks = []
83
- req.on('data', (c) => chunks.push(c))
84
- req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
115
+ let acc = ''
116
+ req.on('data', (chunk) => {
117
+ acc += chunk
118
+ if (acc.length > 5 * 1024 * 1024) {
119
+ req.destroy()
120
+ reject(new Error('Payload Too Large'))
121
+ }
122
+ })
123
+ req.on('end', () => resolve(acc))
85
124
  req.on('error', reject)
86
125
  })
87
126
  }
88
127
 
89
-
90
128
  export function apply(ctx, config) {
91
- let settingsApi = null
92
- let currentConfig = structuredClone(config)
93
-
94
- // Try reading persisted config from ~/.dsh/settings.yaml on load
95
- try {
96
- const fs = require('node:fs')
97
- const yaml = require('yaml')
98
- const p = path.join(os.homedir(), '.dsh', 'settings.yaml')
99
- if (fs.existsSync(p)) {
100
- const parsed = yaml.parse(fs.readFileSync(p, 'utf8'))
101
- if (parsed && parsed[NS]) {
102
- currentConfig = Config(parsed[NS])
103
- }
104
- }
105
- } catch {}
129
+ refreshCatalogInBackground()
130
+
131
+ let settingsScope = null
132
+ let getConfig = () => config
133
+ const live = () => Config(structuredClone(getConfig() ?? {})) ?? config
106
134
 
107
135
  ctx.inject(['settings'], (sctx) => {
108
- const scope = sctx.settings.register(NS, Config, { base: currentConfig })
109
- settingsApi = scope
110
- const val = scope.get()
111
- if (val && Object.keys(val).length > 0) {
112
- currentConfig = val
136
+ try {
137
+ const scope = sctx.settings.register(NS, Config, { base: config })
138
+ settingsScope = scope
139
+ getConfig = () => scope.get() ?? config
140
+ sctx.effect(() => () => {
141
+ settingsScope = null
142
+ getConfig = () => config
143
+ }, 'dsh-moa: settings')
144
+ } catch (e) {
145
+ console.warn('[dsh-moa] Settings registration warning:', e)
113
146
  }
114
- sctx.effect(() => () => {
115
- settingsApi = null
116
- })
117
147
  })
118
148
 
119
- const getConfig = () => {
120
- if (settingsApi) {
121
- const live = settingsApi.get()
122
- if (live && live.presets && live.presets.length) return live
149
+ /**
150
+ * Unified LLM dispatch function using ctx.llm.prepareCall / stream
151
+ */
152
+ const callLlm = async ({ provider, model, messages, temperature = 0.6, maxTokens = 4096 }) => {
153
+ if (!ctx.llm) {
154
+ throw new Error('ctx.llm is not available in cordis context')
123
155
  }
124
- return currentConfig || config
125
- }
126
156
 
127
- // Canonical collector for ctx.llm.stream chunk streams
128
- const collectStreamText = async (iterable) => {
129
- let out = ''
130
- let sawDelta = false
131
- for await (const chunk of iterable) {
132
- if (chunk && chunk.type === 'text-delta' && typeof chunk.text === 'string') {
133
- out += chunk.text
134
- sawDelta = true
135
- } else if (
136
- !sawDelta && chunk && chunk.type === 'block-end' &&
137
- chunk.block && chunk.block.type === 'text' && typeof chunk.block.text === 'string'
138
- ) {
139
- out += chunk.block.text
140
- } else if (chunk && chunk.type === 'finish' && chunk.reason && chunk.reason.kind === 'error') {
141
- throw new Error(chunk.reason.failure?.message || 'LLM stream failed')
157
+ let prep
158
+ try {
159
+ // DSH 0.1.2-rc.1 API expects { provider, model } object
160
+ prep = await ctx.llm.prepareCall({ provider, model })
161
+ } catch (err) {
162
+ if (typeof ctx.llm.prepareCall === 'function') {
163
+ try {
164
+ prep = await ctx.llm.prepareCall(provider, model)
165
+ } catch {
166
+ throw err
167
+ }
168
+ } else {
169
+ throw err
142
170
  }
143
171
  }
144
- return out.trim()
145
- }
146
-
147
- // Adapter to ctx.llm.stream
148
- const callLlm = async (params) => {
149
- if (!ctx.llm || typeof ctx.llm.stream !== 'function') {
150
- throw new Error('ctx.llm.stream service is not available')
172
+ if (!prep || typeof prep.stream !== 'function') {
173
+ throw new Error(`LLM provider/model ${provider}:${model} could not be prepared`)
151
174
  }
152
175
 
153
- const { provider, model, messages, temperature, maxTokens, signal } = params
176
+ const abortCtrl = new AbortController()
177
+ const timeoutId = setTimeout(() => abortCtrl.abort(new Error('LLM call timeout')), 120000)
178
+ timeoutId.unref?.()
154
179
 
155
- // Convert string/simple role messages to format expected by ctx.llm.stream
156
- // Each message must have role, content (array of blocks or string), and source
157
- const formattedMessages = (messages || []).map((m) => {
158
- let content = m.content
159
- if (typeof content === 'string') {
160
- content = [{ type: 'text', text: content }]
161
- } else if (Array.isArray(content)) {
162
- content = content.map((b) => (typeof b === 'string' ? { type: 'text', text: b } : b))
163
- } else {
164
- content = [{ type: 'text', text: String(content || '') }]
165
- }
180
+ try {
181
+ const stream = prep.stream({
182
+ messages,
183
+ temperature,
184
+ maxTokens,
185
+ signal: abortCtrl.signal,
186
+ })
166
187
 
167
- const role = m.role || 'user'
168
- const source = m.source || (role === 'assistant' ? { kind: 'model', provider, model } : { kind: 'user' })
188
+ let fullText = ''
189
+ let usageInfo = null
169
190
 
170
- return {
171
- role,
172
- content,
173
- source,
191
+ for await (const chunk of stream) {
192
+ if (chunk.type === 'text-delta' && typeof chunk.text === 'string') {
193
+ fullText += chunk.text
194
+ } else if (chunk.type === 'block-end' && chunk.block?.text) {
195
+ if (!fullText) fullText = chunk.block.text
196
+ } else if (chunk.type === 'usage' && chunk.usage) {
197
+ usageInfo = chunk.usage
198
+ }
174
199
  }
175
- })
176
200
 
177
- // Provider-specific parameter cleansing (Codex / reasoning models reject temperature, max_output_tokens, etc.)
178
- const isCodexOrReasoning = provider === 'codex' || String(model).includes('sol') || String(model).includes('reasoner')
179
- const streamOpts = {
180
- provider,
181
- model,
182
- messages: formattedMessages,
183
- ...(signal ? { signal } : {}),
184
- }
185
-
186
- if (!isCodexOrReasoning) {
187
- if (temperature !== undefined) streamOpts.temperature = temperature
188
- if (maxTokens !== undefined) streamOpts.maxTokens = maxTokens
201
+ clearTimeout(timeoutId)
202
+ return {
203
+ content: fullText,
204
+ text: fullText,
205
+ usage: usageInfo || {
206
+ inputTokens: Math.round(JSON.stringify(messages).length / 4),
207
+ outputTokens: Math.round(fullText.length / 4),
208
+ },
209
+ }
210
+ } catch (err) {
211
+ clearTimeout(timeoutId)
212
+ throw err
189
213
  }
190
-
191
- const chunks = ctx.llm.stream(streamOpts)
192
-
193
- return await collectStreamText(chunks)
194
214
  }
195
215
 
196
- // HTTP endpoints
216
+ // Route: /dsh-moa/status
197
217
  ctx.effect(() => {
198
218
  return ctx.webServer.register({
199
219
  kind: 'exact',
200
220
  path: '/dsh-moa/status',
201
221
  handler: (_req, res) => {
202
- const cfg = getConfig()
222
+ const cfg = live()
203
223
  writeJson(res, 200, {
204
224
  ok: true,
205
225
  enabled: cfg.enabled !== false,
206
226
  defaultPreset: cfg.default_preset || 'default',
207
- hasCtxLlm: Boolean(ctx.llm),
208
- llmKeys: ctx.llm ? Object.keys(ctx.llm) : [],
209
- llmProto: ctx.llm ? Object.getOwnPropertyNames(Object.getPrototypeOf(ctx.llm)) : [],
210
227
  presetsCount: (cfg.presets || []).length,
211
228
  })
212
229
  },
213
230
  })
214
231
  }, 'dsh-moa: status route')
215
232
 
233
+ // Route: /dsh-moa/models
216
234
  ctx.effect(() => {
217
235
  return ctx.webServer.register({
218
236
  kind: 'exact',
@@ -221,43 +239,25 @@ export function apply(ctx, config) {
221
239
  const result = []
222
240
  const seen = new Set()
223
241
 
224
- const add = (prov, mod, label) => {
225
- if (!prov || !mod) return
226
- const p = String(prov).trim()
227
- const m = String(mod).trim()
242
+ const add = (p, m) => {
228
243
  if (!p || !m) return
229
244
  const key = `${p}:${m}`
230
- if (seen.has(key)) return
231
- seen.add(key)
232
- result.push({ provider: p, model: m, label: label || key })
245
+ if (!seen.has(key)) {
246
+ seen.add(key)
247
+ result.push({ provider: p, model: m, label: key })
248
+ }
233
249
  }
234
250
 
235
- // ONLY query REAL models from runtime adapters & settings. NO hardcoded fallbacks!
236
251
  if (ctx.llm) {
237
252
  try {
238
- if (typeof ctx.llm.listProviders === 'function') {
239
- const provList = ctx.llm.listProviders() || []
240
- for (const p of provList) {
241
- const pId = typeof p === 'string' ? p : (p.id || p.provider || p.name)
242
- if (!pId) continue
243
- if (typeof ctx.llm.listModels === 'function') {
244
- try {
245
- const mList = await ctx.llm.listModels(pId)
246
- for (const m of (mList || [])) {
247
- const mId = typeof m === 'string' ? m : (m.id || m.model || m.name)
248
- const mLabel = m.displayName || m.name || mId
249
- if (mId) add(pId, mId, `${pId}: ${mLabel}`)
250
- }
251
- } catch {}
252
- }
253
- for (const m of (p.models || [])) {
254
- const mId = typeof m === 'string' ? m : (m.id || m.model || m.name)
255
- if (mId) add(pId, mId)
256
- }
253
+ if (typeof ctx.llm.listAvailableModels === 'function') {
254
+ const list = await ctx.llm.listAvailableModels()
255
+ for (const item of (list || [])) {
256
+ if (item?.provider && item?.id) add(item.provider, item.id)
257
257
  }
258
258
  }
259
259
  } catch (e) {
260
- console.warn('[dsh-moa] ctx.llm.listProviders error:', e)
260
+ console.warn('[dsh-moa] ctx.llm.listAvailableModels error:', e)
261
261
  }
262
262
 
263
263
  try {
@@ -317,12 +317,13 @@ export function apply(ctx, config) {
317
317
  })
318
318
  }, 'dsh-moa: models route')
319
319
 
320
+ // Route: /dsh-moa/presets
320
321
  ctx.effect(() => {
321
322
  return ctx.webServer.register({
322
323
  kind: 'exact',
323
324
  path: '/dsh-moa/presets',
324
325
  handler: async (req, res) => {
325
- const cfg = getConfig()
326
+ const cfg = live()
326
327
  if (req.method === 'GET') {
327
328
  writeJson(res, 200, {
328
329
  ok: true,
@@ -342,24 +343,12 @@ export function apply(ctx, config) {
342
343
  presets: Array.isArray(payload.presets) ? payload.presets : (cfg.presets || []),
343
344
  }
344
345
 
345
- // 1. Persist via Cordis Settings API
346
- if (settingsApi && typeof settingsApi.replace === 'function') {
347
- await settingsApi.replace(newConfig)
348
- }
349
- currentConfig = newConfig
350
-
351
- // 2. Direct persistence to ~/.dsh/settings.yaml
352
- try {
353
- const fs = await import('node:fs')
354
- const yaml = await import('yaml')
355
- const p = path.join(os.homedir(), '.dsh', 'settings.yaml')
356
- if (fs.existsSync(p)) {
357
- const curYaml = yaml.parse(fs.readFileSync(p, 'utf8')) || {}
358
- curYaml[NS] = newConfig
359
- fs.writeFileSync(p, yaml.stringify(curYaml), 'utf8')
360
- }
361
- } catch (fsErr) {
362
- console.warn('[dsh-moa] direct file write error:', fsErr)
346
+ // Persist via Cordis Settings API
347
+ if (settingsScope && typeof settingsScope.replace === 'function') {
348
+ await settingsScope.replace(newConfig)
349
+ } else {
350
+ // In-memory fallback if settings service is unavailable
351
+ getConfig = () => newConfig
363
352
  }
364
353
 
365
354
  writeJson(res, 200, {
@@ -378,6 +367,50 @@ export function apply(ctx, config) {
378
367
  })
379
368
  }, 'dsh-moa: presets route')
380
369
 
370
+ // Route: /dsh-moa/history (#9)
371
+ ctx.effect(() => {
372
+ return ctx.webServer.register({
373
+ kind: 'exact',
374
+ path: '/dsh-moa/history',
375
+ handler: async (req, res) => {
376
+ if (req.method !== 'GET') {
377
+ writeJson(res, 405, { ok: false, error: 'GET required' })
378
+ return
379
+ }
380
+ try {
381
+ const u = new URL(req.url, 'http://localhost')
382
+ const limit = parseInt(u.searchParams.get('limit') || '20', 10)
383
+ const offset = parseInt(u.searchParams.get('offset') || '0', 10)
384
+ const data = getMoaHistory(limit, offset)
385
+ writeJson(res, 200, { ok: true, ...data })
386
+ } catch (err) {
387
+ writeJson(res, 500, { ok: false, error: err?.message || String(err) })
388
+ }
389
+ },
390
+ })
391
+ }, 'dsh-moa: history route')
392
+
393
+ // Route: /dsh-moa/leaderboard (#11)
394
+ ctx.effect(() => {
395
+ return ctx.webServer.register({
396
+ kind: 'exact',
397
+ path: '/dsh-moa/leaderboard',
398
+ handler: async (req, res) => {
399
+ if (req.method !== 'GET') {
400
+ writeJson(res, 405, { ok: false, error: 'GET required' })
401
+ return
402
+ }
403
+ try {
404
+ const data = getMoaLeaderboard()
405
+ writeJson(res, 200, { ok: true, ...data })
406
+ } catch (err) {
407
+ writeJson(res, 500, { ok: false, error: err?.message || String(err) })
408
+ }
409
+ },
410
+ })
411
+ }, 'dsh-moa: leaderboard route')
412
+
413
+ // Route: /dsh-moa/run
381
414
  ctx.effect(() => {
382
415
  return ctx.webServer.register({
383
416
  kind: 'exact',
@@ -397,7 +430,7 @@ export function apply(ctx, config) {
397
430
  return
398
431
  }
399
432
 
400
- const cfg = getConfig()
433
+ const cfg = live()
401
434
  const presetName = payload.preset || cfg.default_preset || 'default'
402
435
  const preset = (cfg.presets || []).find((p) => p.name === presetName) || cfg.presets?.[0]
403
436
 
@@ -429,7 +462,7 @@ export function apply(ctx, config) {
429
462
  ? lastUser.content.filter((p) => p.type === 'text').map((p) => p.text).join('\n')
430
463
  : '')
431
464
 
432
- const cfg = getConfig()
465
+ const cfg = live()
433
466
  const parsed = parseMoACommand(userText, cfg.presets || []) || { prompt: userText, presetName: 'default' }
434
467
  const targetPreset = (cfg.presets || []).find((p) => p.name === parsed.presetName) || cfg.presets?.[0]
435
468
 
@@ -466,7 +499,7 @@ export function apply(ctx, config) {
466
499
  : '')
467
500
 
468
501
  if (userText.startsWith('/moa')) {
469
- const cfg = getConfig()
502
+ const cfg = live()
470
503
  if (cfg.enabled !== false) {
471
504
  if (event.signal) {
472
505
  moaPendingSignals.add(event.signal)