@goodandready/dsh-moa 0.2.2 → 0.2.4

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,32 +1,49 @@
1
- import os from 'node:os'
1
+ /**
2
+ * DeepSeek Harness Mixture of Agents (MoA) Plugin
3
+ *
4
+ * Implements an intelligent ensemble pipeline:
5
+ * 1. User issues /moa <prompt>
6
+ * 2. Parallel candidate proposers write to isolated workspaces (.moa/candidate-X/)
7
+ * 3. Aggregator evaluates and synthesizes the optimal solution
8
+ * 4. Winner candidate files are promoted to the project root and previewed in Live Canvas
9
+ * 5. Native cost tracking, history logging and leaderboard analytics
10
+ */
11
+
12
+ import { z } from 'zod'
2
13
  import path from 'node:path'
3
- import z from '@deepseek-ai/schemastery'
4
- import { runMoAPipeline, parseMoACommand, formatMoAResponse, MoaRunnerAdapter } from './moa-runner.js'
14
+ import os from 'node:os'
15
+ import {
16
+ parseMoACommand,
17
+ runMoAPipeline,
18
+ MoaRunnerAdapter,
19
+ } from './moa-runner.js'
20
+
21
+ import {
22
+ getMoaHistory,
23
+ getMoaLeaderboard,
24
+ getMoaRunById,
25
+ } from './history.js'
5
26
 
6
- export const name = '@goodandready/dsh-moa'
7
- export const inject = ['settings', 'webServer', 'llm', 'credentials', 'sessions', 'agents']
27
+ export const name = 'dsh-moa'
28
+ export const inject = ['webServer', 'llm', 'settings', 'sessions', 'tools']
8
29
 
9
- export { parseMoACommand }
30
+ export const NS = 'dsh-moa'
10
31
 
11
32
  export const ModelSlotSchema = z.object({
12
- provider: z.string().default(''),
13
- model: z.string().default(''),
33
+ provider: z.string().default('opencode-go'),
34
+ model: z.string().default('deepseek-v4-flash'),
14
35
  })
15
36
 
16
37
  export const PresetSchema = z.object({
17
- name: z.string().default('default'),
38
+ name: z.string(),
18
39
  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),
40
+ reference_models: z.array(ModelSlotSchema).default([]),
41
+ aggregator: ModelSlotSchema.default({ provider: 'codex', model: 'gpt-5.6-sol' }),
42
+ reference_temperature: z.number().min(0).max(2).default(0.6),
43
+ aggregator_temperature: z.number().min(0).max(2).default(0.4),
44
+ max_tokens: z.number().int().positive().default(4096),
45
+ judge_criteria: z.string().optional().default(''),
46
+ judge_mode: z.enum(['auto', 'review']).optional().default('auto'),
30
47
  })
31
48
 
32
49
  export const Config = z.object({
@@ -38,162 +55,141 @@ export const Config = z.object({
38
55
  enabled: true,
39
56
  reference_models: [
40
57
  { provider: 'opencode-go', model: 'deepseek-v4-flash' },
41
- { provider: 'codex', model: 'gpt-5.6-sol' },
58
+ { provider: 'grok', model: 'grok-build-0.1' },
42
59
  ],
43
- aggregator: {
44
- provider: 'antigravity',
45
- model: 'gemini-3-flash',
46
- },
60
+ aggregator: { provider: 'codex', model: 'gpt-5.6-sol' },
47
61
  reference_temperature: 0.6,
48
62
  aggregator_temperature: 0.4,
49
63
  max_tokens: 4096,
64
+ judge_criteria: '',
65
+ judge_mode: 'auto',
66
+ },
67
+ {
68
+ name: 'fast',
69
+ enabled: true,
70
+ reference_models: [
71
+ { provider: 'opencode-go', model: 'deepseek-v4-flash' },
72
+ ],
73
+ aggregator: { provider: 'opencode-go', model: 'deepseek-v4-flash' },
74
+ reference_temperature: 0.6,
75
+ aggregator_temperature: 0.2,
76
+ max_tokens: 4096,
77
+ judge_criteria: '',
78
+ judge_mode: 'auto',
50
79
  },
51
80
  {
52
81
  name: 'deep-reasoning',
53
82
  enabled: true,
54
83
  reference_models: [
55
- { provider: 'grok', model: 'grok-4.20-0309-reasoning' },
56
- { provider: 'codex', model: 'gpt-5.6-terra' },
84
+ { provider: 'grok', model: 'grok-build-0.1' },
85
+ { provider: 'commandcode', model: 'deepseek/deepseek-v4-flash' },
57
86
  ],
58
- aggregator: {
59
- provider: 'antigravity',
60
- model: 'gemini-3-flash',
61
- },
62
- reference_temperature: 0.7,
87
+ aggregator: { provider: 'codex', model: 'gpt-5.6-sol' },
88
+ reference_temperature: 0.6,
63
89
  aggregator_temperature: 0.2,
64
90
  max_tokens: 8192,
91
+ judge_criteria: 'Приоритет: математическая строгость и отсутствие галлюцинаций',
92
+ judge_mode: 'auto',
65
93
  },
66
94
  ]),
67
95
  })
68
96
 
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
- }
97
+ function writeJson(res, status, body) {
98
+ res.writeHead(status, {
99
+ 'Content-Type': 'application/json; charset=utf-8',
100
+ 'Cache-Control': 'no-store',
101
+ })
102
+ res.end(JSON.stringify(body))
78
103
  }
79
104
 
80
105
  function readBody(req) {
81
106
  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')))
107
+ let acc = ''
108
+ req.on('data', (chunk) => {
109
+ acc += chunk
110
+ if (acc.length > 5 * 1024 * 1024) {
111
+ req.destroy()
112
+ reject(new Error('Payload Too Large'))
113
+ }
114
+ })
115
+ req.on('end', () => resolve(acc))
85
116
  req.on('error', reject)
86
117
  })
87
118
  }
88
119
 
89
-
90
120
  export function apply(ctx, config) {
121
+ let currentConfig = Config.parse(config || {})
91
122
  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 {}
106
123
 
107
124
  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
125
+ try {
126
+ settingsApi = sctx.settings.register(NS, Config, { base: currentConfig })
127
+ const val = settingsApi.get()
128
+ if (val) currentConfig = Config.parse(val)
129
+ settingsApi.onChange((next) => {
130
+ if (next) currentConfig = Config.parse(next)
131
+ })
132
+ } catch (e) {
133
+ console.warn('[dsh-moa] Settings registration warning:', e)
113
134
  }
114
- sctx.effect(() => () => {
115
- settingsApi = null
116
- })
117
135
  })
118
136
 
119
- const getConfig = () => {
120
- if (settingsApi) {
121
- const live = settingsApi.get()
122
- if (live && live.presets && live.presets.length) return live
123
- }
124
- return currentConfig || config
125
- }
137
+ const getConfig = () => currentConfig
126
138
 
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')
142
- }
139
+ /**
140
+ * Unified LLM dispatch function using ctx.llm.prepareCall / stream
141
+ */
142
+ const callLlm = async ({ provider, model, messages, temperature = 0.6, maxTokens = 4096 }) => {
143
+ if (!ctx.llm) {
144
+ throw new Error('ctx.llm is not available in cordis context')
143
145
  }
144
- return out.trim()
145
- }
146
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')
147
+ const prep = await ctx.llm.prepareCall(provider, model)
148
+ if (!prep || typeof prep.stream !== 'function') {
149
+ throw new Error(`LLM provider/model ${provider}:${model} could not be prepared`)
151
150
  }
152
151
 
153
- const { provider, model, messages, temperature, maxTokens, signal } = params
154
-
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
- }
152
+ const abortCtrl = new AbortController()
153
+ const timeoutId = setTimeout(() => abortCtrl.abort(new Error('LLM call timeout')), 120000)
154
+ timeoutId.unref?.()
166
155
 
167
- const role = m.role || 'user'
168
- const source = m.source || (role === 'assistant' ? { kind: 'model', provider, model } : { kind: 'user' })
156
+ try {
157
+ const stream = prep.stream({
158
+ messages,
159
+ temperature,
160
+ maxTokens,
161
+ signal: abortCtrl.signal,
162
+ })
169
163
 
170
- return {
171
- role,
172
- content,
173
- source,
174
- }
175
- })
164
+ let fullText = ''
165
+ let usageInfo = null
176
166
 
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
- }
167
+ for await (const chunk of stream) {
168
+ if (chunk.type === 'text-delta' && typeof chunk.text === 'string') {
169
+ fullText += chunk.text
170
+ } else if (chunk.type === 'block-end' && chunk.block?.text) {
171
+ if (!fullText) fullText = chunk.block.text
172
+ } else if (chunk.type === 'usage' && chunk.usage) {
173
+ usageInfo = chunk.usage
174
+ }
175
+ }
185
176
 
186
- if (!isCodexOrReasoning) {
187
- if (temperature !== undefined) streamOpts.temperature = temperature
188
- if (maxTokens !== undefined) streamOpts.maxTokens = maxTokens
177
+ clearTimeout(timeoutId)
178
+ return {
179
+ content: fullText,
180
+ text: fullText,
181
+ usage: usageInfo || {
182
+ inputTokens: Math.round(JSON.stringify(messages).length / 4),
183
+ outputTokens: Math.round(fullText.length / 4),
184
+ },
185
+ }
186
+ } catch (err) {
187
+ clearTimeout(timeoutId)
188
+ throw err
189
189
  }
190
-
191
- const chunks = ctx.llm.stream(streamOpts)
192
-
193
- return await collectStreamText(chunks)
194
190
  }
195
191
 
196
- // HTTP endpoints
192
+ // Route: /dsh-moa/status
197
193
  ctx.effect(() => {
198
194
  return ctx.webServer.register({
199
195
  kind: 'exact',
@@ -204,15 +200,13 @@ export function apply(ctx, config) {
204
200
  ok: true,
205
201
  enabled: cfg.enabled !== false,
206
202
  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
203
  presetsCount: (cfg.presets || []).length,
211
204
  })
212
205
  },
213
206
  })
214
207
  }, 'dsh-moa: status route')
215
208
 
209
+ // Route: /dsh-moa/models
216
210
  ctx.effect(() => {
217
211
  return ctx.webServer.register({
218
212
  kind: 'exact',
@@ -221,43 +215,25 @@ export function apply(ctx, config) {
221
215
  const result = []
222
216
  const seen = new Set()
223
217
 
224
- const add = (prov, mod, label) => {
225
- if (!prov || !mod) return
226
- const p = String(prov).trim()
227
- const m = String(mod).trim()
218
+ const add = (p, m) => {
228
219
  if (!p || !m) return
229
220
  const key = `${p}:${m}`
230
- if (seen.has(key)) return
231
- seen.add(key)
232
- result.push({ provider: p, model: m, label: label || key })
221
+ if (!seen.has(key)) {
222
+ seen.add(key)
223
+ result.push({ provider: p, model: m, label: key })
224
+ }
233
225
  }
234
226
 
235
- // ONLY query REAL models from runtime adapters & settings. NO hardcoded fallbacks!
236
227
  if (ctx.llm) {
237
228
  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
- }
229
+ if (typeof ctx.llm.listAvailableModels === 'function') {
230
+ const list = await ctx.llm.listAvailableModels()
231
+ for (const item of (list || [])) {
232
+ if (item?.provider && item?.id) add(item.provider, item.id)
257
233
  }
258
234
  }
259
235
  } catch (e) {
260
- console.warn('[dsh-moa] ctx.llm.listProviders error:', e)
236
+ console.warn('[dsh-moa] ctx.llm.listAvailableModels error:', e)
261
237
  }
262
238
 
263
239
  try {
@@ -317,6 +293,7 @@ export function apply(ctx, config) {
317
293
  })
318
294
  }, 'dsh-moa: models route')
319
295
 
296
+ // Route: /dsh-moa/presets
320
297
  ctx.effect(() => {
321
298
  return ctx.webServer.register({
322
299
  kind: 'exact',
@@ -378,6 +355,50 @@ export function apply(ctx, config) {
378
355
  })
379
356
  }, 'dsh-moa: presets route')
380
357
 
358
+ // Route: /dsh-moa/history (#9)
359
+ ctx.effect(() => {
360
+ return ctx.webServer.register({
361
+ kind: 'exact',
362
+ path: '/dsh-moa/history',
363
+ handler: async (req, res) => {
364
+ if (req.method !== 'GET') {
365
+ writeJson(res, 405, { ok: false, error: 'GET required' })
366
+ return
367
+ }
368
+ try {
369
+ const u = new URL(req.url, 'http://localhost')
370
+ const limit = parseInt(u.searchParams.get('limit') || '20', 10)
371
+ const offset = parseInt(u.searchParams.get('offset') || '0', 10)
372
+ const data = getMoaHistory(limit, offset)
373
+ writeJson(res, 200, { ok: true, ...data })
374
+ } catch (err) {
375
+ writeJson(res, 500, { ok: false, error: err?.message || String(err) })
376
+ }
377
+ },
378
+ })
379
+ }, 'dsh-moa: history route')
380
+
381
+ // Route: /dsh-moa/leaderboard (#11)
382
+ ctx.effect(() => {
383
+ return ctx.webServer.register({
384
+ kind: 'exact',
385
+ path: '/dsh-moa/leaderboard',
386
+ handler: async (req, res) => {
387
+ if (req.method !== 'GET') {
388
+ writeJson(res, 405, { ok: false, error: 'GET required' })
389
+ return
390
+ }
391
+ try {
392
+ const data = getMoaLeaderboard()
393
+ writeJson(res, 200, { ok: true, ...data })
394
+ } catch (err) {
395
+ writeJson(res, 500, { ok: false, error: err?.message || String(err) })
396
+ }
397
+ },
398
+ })
399
+ }, 'dsh-moa: leaderboard route')
400
+
401
+ // Route: /dsh-moa/run
381
402
  ctx.effect(() => {
382
403
  return ctx.webServer.register({
383
404
  kind: 'exact',