@goodandready/dsh-moa 0.2.27 → 0.2.30
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/README.md +12 -12
- package/README.ru.md +12 -12
- package/lib/file-workspace.js +28 -6
- package/lib/history.js +48 -17
- package/lib/index.js +20 -16
- package/lib/logger.js +40 -0
- package/lib/moa-candidates.js +3 -2
- package/lib/moa-router.js +2 -2
- package/lib/moa-runner.js +31 -16
- package/lib/moa-stream.js +4 -1
- package/lib/moa-test-gate.js +4 -3
- package/lib/pricing.js +25 -7
- package/lib/routes.js +42 -10
- package/lib/updater.js +96 -5
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -151,8 +151,8 @@ When `multi_judge_enabled: true`, candidate solutions are independently evaluate
|
|
|
151
151
|
### 11. Composite Hybrid Synthesis (AST / Block Merge)
|
|
152
152
|
When `composite_merge_enabled: true`, the aggregator assembles a modular hybrid: combining the strongest core logic from one model, robust error handling from another, and complete types/tests from a third.
|
|
153
153
|
|
|
154
|
-
### 12. Smart Dynamic Preset Router &
|
|
155
|
-
When invoking `/moa` without explicit preset flags, the router classifies prompt intent using keyword heuristics or a zero-shot
|
|
154
|
+
### 12. Smart Dynamic Preset Router & LLM Classifier
|
|
155
|
+
When invoking `/moa` without explicit preset flags, the router classifies prompt intent using keyword heuristics or a zero-shot router model (`smart_routing_model`) to automatically select the optimal preset.
|
|
156
156
|
|
|
157
157
|
### 13. Real-Time Live Cost Counter & Streaming Ticker
|
|
158
158
|
As each candidate completes, live token counts and USD costs are streamed directly into the chat based on catalog pricing and vendor rates.
|
|
@@ -195,16 +195,16 @@ v0.2.13 introduces 10 ready-to-use presets engineered for real-world software wo
|
|
|
195
195
|
|
|
196
196
|
| Preset Name | Purpose | Default Aggregator | Peer Critique | Blind Eval |
|
|
197
197
|
| :--- | :--- | :--- | :---: | :---: |
|
|
198
|
-
| `default` | Balanced multi-model generation |
|
|
199
|
-
| `code-review` | Thorough peer review & vulnerability detection |
|
|
200
|
-
| `fast-audit` | Ultra-fast single-model audit (Fast Mode) |
|
|
201
|
-
| `deep-architect` | Distributed systems & complex architectures |
|
|
202
|
-
| `bug-hunter` | Root cause discovery & adversarial edge cases |
|
|
203
|
-
| `refactor-cleanup` | Dead-code pruning & standard-library simplicity |
|
|
204
|
-
| `frontend-ui` | High-fidelity responsive web interfaces |
|
|
205
|
-
| `security-audit` | Zero-trust threat analysis & sanitization |
|
|
206
|
-
| `math-logic` | Deterministic algorithmic proofs & math logic |
|
|
207
|
-
| `creative-brainstorm`| Divergent lateral thinking & ideation |
|
|
198
|
+
| `default` | Balanced multi-model generation | `<provider>:<model>` | Optional | Off |
|
|
199
|
+
| `code-review` | Thorough peer review & vulnerability detection | `<provider>:<model>` | On | On |
|
|
200
|
+
| `fast-audit` | Ultra-fast single-model audit (Fast Mode) | `<provider>:<model>` | Off | Off |
|
|
201
|
+
| `deep-architect` | Distributed systems & complex architectures | `<provider>:<model>` | On | Off |
|
|
202
|
+
| `bug-hunter` | Root cause discovery & adversarial edge cases | `<provider>:<model>` | On | Off |
|
|
203
|
+
| `refactor-cleanup` | Dead-code pruning & standard-library simplicity | `<provider>:<model>` | Off | Off |
|
|
204
|
+
| `frontend-ui` | High-fidelity responsive web interfaces | `<provider>:<model>` | Off | Off |
|
|
205
|
+
| `security-audit` | Zero-trust threat analysis & sanitization | `<provider>:<model>` | On | On |
|
|
206
|
+
| `math-logic` | Deterministic algorithmic proofs & math logic | `<provider>:<model>` | On | Off |
|
|
207
|
+
| `creative-brainstorm`| Divergent lateral thinking & ideation | `<provider>:<model>` | Off | Off |
|
|
208
208
|
|
|
209
209
|
### Candidate Personas (`role_persona`)
|
|
210
210
|
Assign archetypal engineering mentalities to individual candidate slots to ensure genuine perspective divergence:
|
package/README.ru.md
CHANGED
|
@@ -151,8 +151,8 @@ graph TD
|
|
|
151
151
|
### 11. Композитный гибридный синтез (Composite AST/Block Merge)
|
|
152
152
|
Опция `composite_merge_enabled` активирует алгоритм объединения модулей: судья не просто выбирает одного кандидата, а синтезирует итоговое решение из лучших компонентов всех участников (чистейшая алгоритмическая база от одного, надежные обработчики ошибок от второго, исчерпывающие типы и тесты от третьего).
|
|
153
153
|
|
|
154
|
-
### 12. Умный динамический роутинг пресетов и модель
|
|
155
|
-
При запуске `/moa` без явного указания флага встроенный классификатор определяет намерение (интерфейсы, безопасность, рефакторинг, баги, архитектура) по эвристическим правилам или с помощью легковесной модели
|
|
154
|
+
### 12. Умный динамический роутинг пресетов и модель роутера
|
|
155
|
+
При запуске `/moa` без явного указания флага встроенный классификатор определяет намерение (интерфейсы, безопасность, рефакторинг, баги, архитектура) по эвристическим правилам или с помощью легковесной модели роутера (`smart_routing_model`) и автоматически подбирает оптимальный пресет.
|
|
156
156
|
|
|
157
157
|
### 13. Живой счетчик стоимости и потоковая телеметрия
|
|
158
158
|
По мере завершения каждого кандидата плагин в реальном времени транслирует в чат накопленный расход токенов и расчетную стоимость в USD на базе встроенного каталога тарифов OpenRouter и прямых цен провайдеров.
|
|
@@ -195,16 +195,16 @@ dsh plugin --profile web add @goodandready/dsh-moa
|
|
|
195
195
|
|
|
196
196
|
| Имя пресета | Назначение | Судья по умолчанию | Рецензия (Round 2) | Слепая оценка |
|
|
197
197
|
| :--- | :--- | :--- | :---: | :---: |
|
|
198
|
-
| `default` | Сбалансированная генерация |
|
|
199
|
-
| `code-review` | Глубокое взаимное ревью кода |
|
|
200
|
-
| `fast-audit` | Сверхбыстрый экспресс-аудит (Fast Mode) |
|
|
201
|
-
| `deep-architect` | Проектирование распределенных систем |
|
|
202
|
-
| `bug-hunter` | Поиск скрытых багов и граничных случаев |
|
|
203
|
-
| `refactor-cleanup` | Чистка легаси и удаление оверинжиниринга |
|
|
204
|
-
| `frontend-ui` | Качественные адаптивные веб-интерфейсы |
|
|
205
|
-
| `security-audit` | Анализ уязвимостей и санитайзинг |
|
|
206
|
-
| `math-logic` | Алгоритмы, математическая логика и доказательства |
|
|
207
|
-
| `creative-brainstorm`| Нестандартные идеи и дивергентный поиск |
|
|
198
|
+
| `default` | Сбалансированная генерация | `<provider>:<model>` | Опционально | Выкл |
|
|
199
|
+
| `code-review` | Глубокое взаимное ревью кода | `<provider>:<model>` | Вкл | Вкл |
|
|
200
|
+
| `fast-audit` | Сверхбыстрый экспресс-аудит (Fast Mode) | `<provider>:<model>` | Выкл | Выкл |
|
|
201
|
+
| `deep-architect` | Проектирование распределенных систем | `<provider>:<model>` | Вкл | Выкл |
|
|
202
|
+
| `bug-hunter` | Поиск скрытых багов и граничных случаев | `<provider>:<model>` | Вкл | Выкл |
|
|
203
|
+
| `refactor-cleanup` | Чистка легаси и удаление оверинжиниринга | `<provider>:<model>` | Выкл | Выкл |
|
|
204
|
+
| `frontend-ui` | Качественные адаптивные веб-интерфейсы | `<provider>:<model>` | Выкл | Выкл |
|
|
205
|
+
| `security-audit` | Анализ уязвимостей и санитайзинг | `<provider>:<model>` | Вкл | Вкл |
|
|
206
|
+
| `math-logic` | Алгоритмы, математическая логика и доказательства | `<provider>:<model>` | Вкл | Выкл |
|
|
207
|
+
| `creative-brainstorm`| Нестандартные идеи и дивергентный поиск | `<provider>:<model>` | Выкл | Выкл |
|
|
208
208
|
|
|
209
209
|
### Инженерные персоны кандидатов (`role_persona`)
|
|
210
210
|
Задавайте кандидатам четкие роли для максимального разнообразия подходов:
|
package/lib/file-workspace.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { bestEffort } from './best-effort.js'
|
|
2
|
+
import { logger } from './logger.js'
|
|
2
3
|
import vm from 'node:vm'
|
|
3
4
|
import fs from 'node:fs/promises'
|
|
4
5
|
import path from 'node:path'
|
|
@@ -11,7 +12,8 @@ export function isSafeRelativePath(relPath) {
|
|
|
11
12
|
const trimmed = relPath.trim()
|
|
12
13
|
if (!trimmed || path.isAbsolute(trimmed) || trimmed.startsWith('/') || trimmed.startsWith('\\')) return false
|
|
13
14
|
const normalized = path.normalize(trimmed)
|
|
14
|
-
|
|
15
|
+
if (!normalized || normalized === '.' || normalized.startsWith('..') || path.isAbsolute(normalized)) return false
|
|
16
|
+
return true
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -150,9 +152,9 @@ export async function collectProjectContext(baseDir, maxCharBudget = 16000) {
|
|
|
150
152
|
const truncated = content.length > budgetRemaining ? content.slice(0, budgetRemaining) + '\n... [truncated]' : content
|
|
151
153
|
totalChars += truncated.length
|
|
152
154
|
collected.push({ relativePath: rel, content: truncated, size: stat.size })
|
|
153
|
-
} catch {
|
|
155
|
+
} catch (err) {
|
|
154
156
|
skippedFiles++
|
|
155
|
-
skippedList.push(rel)
|
|
157
|
+
skippedList.push(`${rel} (read error: ${err?.message || String(err)})`)
|
|
156
158
|
}
|
|
157
159
|
}
|
|
158
160
|
}
|
|
@@ -236,13 +238,28 @@ export async function createPrePromotionCheckpoint(baseDir, candidateTarget, opt
|
|
|
236
238
|
}
|
|
237
239
|
|
|
238
240
|
const label = options.checkpointLabel || `moa-pre-promotion: candidate-${candidateTarget}`
|
|
239
|
-
|
|
241
|
+
|
|
242
|
+
// 1. Direct Cordis timeMachineEngine service call if available
|
|
243
|
+
if (options.timeMachineEngine && typeof options.timeMachineEngine.createSnapshot === 'function') {
|
|
244
|
+
const snap = await bestEffort('dsh-time-machine direct service createSnapshot', async () => {
|
|
245
|
+
return await options.timeMachineEngine.createSnapshot(label, { cwd: baseDir })
|
|
246
|
+
})
|
|
247
|
+
if (snap) return snap
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 2. HTTP endpoint call if port or timeMachineUrl is provided
|
|
251
|
+
const port = options.port || options.webServerPort || process.env.DSH_PORT || process.env.PORT
|
|
252
|
+
if (!port && !options.timeMachineUrl) {
|
|
253
|
+
logger.warn('[dsh-moa] Cannot create pre-promotion checkpoint: webServerPort is unknown and no timeMachineUrl provided')
|
|
254
|
+
return null
|
|
255
|
+
}
|
|
256
|
+
|
|
240
257
|
const endpoint = options.timeMachineUrl || `http://127.0.0.1:${port}/dsh-time-machine/create`
|
|
241
258
|
|
|
242
259
|
const snap = await bestEffort('dsh-time-machine pre-promotion checkpoint', async () => {
|
|
243
260
|
const res = await fetch(endpoint, {
|
|
244
261
|
method: 'POST',
|
|
245
|
-
headers: { 'Content-Type': 'application/json'
|
|
262
|
+
headers: { 'Content-Type': 'application/json' },
|
|
246
263
|
body: JSON.stringify({ label, cwd: baseDir }),
|
|
247
264
|
signal: AbortSignal.timeout(options.timeoutMs ?? 1500),
|
|
248
265
|
})
|
|
@@ -274,6 +291,11 @@ export async function promoteCandidateWorkspace(baseDir, winningIndex, options =
|
|
|
274
291
|
}
|
|
275
292
|
|
|
276
293
|
const checkpoint = await createPrePromotionCheckpoint(baseDir, winningIndex, options)
|
|
294
|
+
if (options.checkpoint !== false && !checkpoint && !options.force) {
|
|
295
|
+
const errMsg = `[dsh-moa] Pre-promotion checkpoint failed for candidate-${winningIndex} in ${baseDir}. File overwrite blocked to prevent uncheckpointed changes (pass force: true to override).`
|
|
296
|
+
logger.warn(errMsg)
|
|
297
|
+
throw new Error(errMsg)
|
|
298
|
+
}
|
|
277
299
|
|
|
278
300
|
const promoted = []
|
|
279
301
|
for (const rel of entries) {
|
|
@@ -294,7 +316,7 @@ export async function promoteCandidateWorkspace(baseDir, winningIndex, options =
|
|
|
294
316
|
if (!options.keepMoa) {
|
|
295
317
|
await bestEffort('clean moaRoot on promote error', () => fs.rm(moaRoot, { recursive: true, force: true }))
|
|
296
318
|
}
|
|
297
|
-
|
|
319
|
+
throw err
|
|
298
320
|
}
|
|
299
321
|
}
|
|
300
322
|
|
package/lib/history.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logger } from './logger.js'
|
|
1
2
|
/**
|
|
2
3
|
* MoA Run History & Analytics Module
|
|
3
4
|
* Records every MoA pipeline execution, tracks model win rates and costs,
|
|
@@ -9,8 +10,38 @@ import path from 'node:path'
|
|
|
9
10
|
import os from 'node:os'
|
|
10
11
|
import crypto from 'node:crypto'
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
export function isTestEnvironment() {
|
|
14
|
+
return (
|
|
15
|
+
process.env.NODE_ENV === 'test' ||
|
|
16
|
+
process.env.DSH_TEST === '1' ||
|
|
17
|
+
Boolean(process.env.NODE_TEST_CONTEXT) ||
|
|
18
|
+
Boolean(process.execArgv?.some((arg) => arg.includes('test'))) ||
|
|
19
|
+
Boolean(process.argv?.[1]?.includes('test'))
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getDefaultHistoryDir() {
|
|
24
|
+
if (process.env.DSH_HISTORY_DIR) {
|
|
25
|
+
return process.env.DSH_HISTORY_DIR
|
|
26
|
+
}
|
|
27
|
+
if (process.env.DSH_HOME) {
|
|
28
|
+
return path.join(process.env.DSH_HOME, '.dsh')
|
|
29
|
+
}
|
|
30
|
+
if (isTestEnvironment()) {
|
|
31
|
+
return path.join(os.tmpdir(), '.dsh-test')
|
|
32
|
+
}
|
|
33
|
+
return path.join(os.homedir(), '.dsh')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getDefaultHistoryFile() {
|
|
37
|
+
if (process.env.DSH_HISTORY_FILE) {
|
|
38
|
+
return process.env.DSH_HISTORY_FILE
|
|
39
|
+
}
|
|
40
|
+
return path.join(getDefaultHistoryDir(), 'moa-history.jsonl')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const DEFAULT_HISTORY_DIR = getDefaultHistoryDir()
|
|
44
|
+
export const DEFAULT_HISTORY_FILE = getDefaultHistoryFile()
|
|
14
45
|
|
|
15
46
|
/** Max history file size before rotation (10 MB) */
|
|
16
47
|
export const MAX_HISTORY_BYTES = 10 * 1024 * 1024
|
|
@@ -22,18 +53,18 @@ export function invalidateHistoryCache() {
|
|
|
22
53
|
_historyCache = null
|
|
23
54
|
}
|
|
24
55
|
|
|
25
|
-
function getHistoryFilePath(customDir) {
|
|
56
|
+
export function getHistoryFilePath(customDir) {
|
|
26
57
|
if (customDir) {
|
|
27
58
|
return path.join(customDir, '.moa-history.jsonl')
|
|
28
59
|
}
|
|
29
|
-
return
|
|
60
|
+
return getDefaultHistoryFile()
|
|
30
61
|
}
|
|
31
62
|
|
|
32
63
|
/**
|
|
33
64
|
* Checks if the history file exceeds the size threshold and rotates it.
|
|
34
65
|
* Moves current file to `${filePath}.1`, removing any older `.1` backup.
|
|
35
66
|
*/
|
|
36
|
-
export function rotateHistoryFileIfNeeded(filePath =
|
|
67
|
+
export function rotateHistoryFileIfNeeded(filePath = getDefaultHistoryFile(), maxBytes = MAX_HISTORY_BYTES) {
|
|
37
68
|
try {
|
|
38
69
|
if (!fs.existsSync(filePath)) return false
|
|
39
70
|
const stat = fs.statSync(filePath)
|
|
@@ -47,7 +78,7 @@ export function rotateHistoryFileIfNeeded(filePath = DEFAULT_HISTORY_FILE, maxBy
|
|
|
47
78
|
return true
|
|
48
79
|
}
|
|
49
80
|
} catch (err) {
|
|
50
|
-
|
|
81
|
+
logger.warn('[dsh-moa] History rotation warning:', err?.message || err)
|
|
51
82
|
}
|
|
52
83
|
return false
|
|
53
84
|
}
|
|
@@ -95,7 +126,7 @@ function normalizeRunRecord(record) {
|
|
|
95
126
|
* Appends a completed MoA run record to the history file synchronously (preserves sync API).
|
|
96
127
|
* Performs automatic size-based rotation.
|
|
97
128
|
*/
|
|
98
|
-
export function recordMoaRun(record, filePath =
|
|
129
|
+
export function recordMoaRun(record, filePath = getDefaultHistoryFile()) {
|
|
99
130
|
try {
|
|
100
131
|
const dir = path.dirname(filePath)
|
|
101
132
|
if (!fs.existsSync(dir)) {
|
|
@@ -110,7 +141,7 @@ export function recordMoaRun(record, filePath = DEFAULT_HISTORY_FILE) {
|
|
|
110
141
|
invalidateHistoryCache()
|
|
111
142
|
return entry
|
|
112
143
|
} catch (err) {
|
|
113
|
-
|
|
144
|
+
logger.warn('[dsh-moa] Failed to record run to history:', err)
|
|
114
145
|
return null
|
|
115
146
|
}
|
|
116
147
|
}
|
|
@@ -118,7 +149,7 @@ export function recordMoaRun(record, filePath = DEFAULT_HISTORY_FILE) {
|
|
|
118
149
|
/**
|
|
119
150
|
* Asynchronously appends a completed MoA run record without blocking the event loop.
|
|
120
151
|
*/
|
|
121
|
-
export async function recordMoaRunAsync(record, filePath =
|
|
152
|
+
export async function recordMoaRunAsync(record, filePath = getDefaultHistoryFile()) {
|
|
122
153
|
try {
|
|
123
154
|
const dir = path.dirname(filePath)
|
|
124
155
|
await fs.promises.mkdir(dir, { recursive: true }).catch(() => {})
|
|
@@ -131,7 +162,7 @@ export async function recordMoaRunAsync(record, filePath = DEFAULT_HISTORY_FILE)
|
|
|
131
162
|
invalidateHistoryCache()
|
|
132
163
|
return entry
|
|
133
164
|
} catch (err) {
|
|
134
|
-
|
|
165
|
+
logger.warn('[dsh-moa] Failed to async-record run to history:', err)
|
|
135
166
|
return null
|
|
136
167
|
}
|
|
137
168
|
}
|
|
@@ -182,7 +213,7 @@ export function readTailLinesSync(filePath, countNeeded = 50) {
|
|
|
182
213
|
/**
|
|
183
214
|
* Reads history runs with pagination (latest first).
|
|
184
215
|
*/
|
|
185
|
-
export function getMoaHistory(limit = 20, offset = 0, filePath =
|
|
216
|
+
export function getMoaHistory(limit = 20, offset = 0, filePath = getDefaultHistoryFile()) {
|
|
186
217
|
try {
|
|
187
218
|
if (!fs.existsSync(filePath)) {
|
|
188
219
|
return { total: 0, runs: [] }
|
|
@@ -232,7 +263,7 @@ export function getMoaHistory(limit = 20, offset = 0, filePath = DEFAULT_HISTORY
|
|
|
232
263
|
|
|
233
264
|
return { total, runs }
|
|
234
265
|
} catch (err) {
|
|
235
|
-
|
|
266
|
+
logger.warn('[dsh-moa] Failed to read history:', err)
|
|
236
267
|
return { total: 0, runs: [] }
|
|
237
268
|
}
|
|
238
269
|
}
|
|
@@ -241,7 +272,7 @@ export function getMoaHistory(limit = 20, offset = 0, filePath = DEFAULT_HISTORY
|
|
|
241
272
|
* Calculates win-rate leaderboard and model performance statistics across history.
|
|
242
273
|
* Employs in-memory caching keyed by file mtime and size for O(1) repeated queries.
|
|
243
274
|
*/
|
|
244
|
-
export function getMoaLeaderboard(filePath =
|
|
275
|
+
export function getMoaLeaderboard(filePath = getDefaultHistoryFile(), options = {}) {
|
|
245
276
|
try {
|
|
246
277
|
if (!fs.existsSync(filePath)) {
|
|
247
278
|
return { totalRuns: 0, models: [] }
|
|
@@ -335,7 +366,7 @@ export function getMoaLeaderboard(filePath = DEFAULT_HISTORY_FILE, options = {})
|
|
|
335
366
|
_historyCache.leaderboards.set(filterKey, result)
|
|
336
367
|
return result
|
|
337
368
|
} catch (err) {
|
|
338
|
-
|
|
369
|
+
logger.warn('[dsh-moa] Failed to calculate leaderboard:', err)
|
|
339
370
|
return { totalRuns: 0, models: [] }
|
|
340
371
|
}
|
|
341
372
|
}
|
|
@@ -343,7 +374,7 @@ export function getMoaLeaderboard(filePath = DEFAULT_HISTORY_FILE, options = {})
|
|
|
343
374
|
/**
|
|
344
375
|
* Retrieves a single history run by ID.
|
|
345
376
|
*/
|
|
346
|
-
export function getMoaRunById(runId, filePath =
|
|
377
|
+
export function getMoaRunById(runId, filePath = getDefaultHistoryFile()) {
|
|
347
378
|
try {
|
|
348
379
|
if (!fs.existsSync(filePath)) return null
|
|
349
380
|
const raw = fs.readFileSync(filePath, 'utf8')
|
|
@@ -363,7 +394,7 @@ export function getMoaRunById(runId, filePath = DEFAULT_HISTORY_FILE) {
|
|
|
363
394
|
/**
|
|
364
395
|
* Exports history data in JSON or CSV format with optional preset filtering.
|
|
365
396
|
*/
|
|
366
|
-
export function exportMoaHistory(filePath =
|
|
397
|
+
export function exportMoaHistory(filePath = getDefaultHistoryFile(), options = {}) {
|
|
367
398
|
try {
|
|
368
399
|
if (!fs.existsSync(filePath)) {
|
|
369
400
|
return options?.format === 'csv' ? 'id,timestamp,preset,winnerModel,totalTokens,totalCostUsd,durationMs\n' : '[]'
|
|
@@ -405,7 +436,7 @@ export function exportMoaHistory(filePath = DEFAULT_HISTORY_FILE, options = {})
|
|
|
405
436
|
|
|
406
437
|
return JSON.stringify(records, null, 2)
|
|
407
438
|
} catch (err) {
|
|
408
|
-
|
|
439
|
+
logger.warn('[dsh-moa] Failed to export history:', err)
|
|
409
440
|
return options?.format === 'csv' ? '' : '[]'
|
|
410
441
|
}
|
|
411
442
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setLogger, logger } from './logger.js'
|
|
1
2
|
import { refreshCatalogInBackground } from './pricing.js'
|
|
2
3
|
/**
|
|
3
4
|
* DeepSeek Harness Mixture of Agents (MoA) Plugin
|
|
@@ -50,8 +51,8 @@ export const PriceRow = z.object({
|
|
|
50
51
|
})
|
|
51
52
|
|
|
52
53
|
export const ModelSlotSchema = z.object({
|
|
53
|
-
provider: z.string().default('
|
|
54
|
-
model: z.string().default('
|
|
54
|
+
provider: z.string().default(''),
|
|
55
|
+
model: z.string().default(''),
|
|
55
56
|
role_persona: z.string().default(''),
|
|
56
57
|
temperature: z.number().default(-1),
|
|
57
58
|
})
|
|
@@ -65,7 +66,7 @@ export const PresetSchema = z.object({
|
|
|
65
66
|
quorum_enabled: z.boolean().default(false),
|
|
66
67
|
grace_period_sec: z.number().default(10),
|
|
67
68
|
reference_models: z.array(ModelSlotSchema).default([]),
|
|
68
|
-
aggregator: ModelSlotSchema.default({ provider: '
|
|
69
|
+
aggregator: ModelSlotSchema.default({ provider: '', model: '' }),
|
|
69
70
|
aggregator_fallbacks: z.array(ModelSlotSchema).default([]),
|
|
70
71
|
reference_temperature: z.number().default(0.6),
|
|
71
72
|
aggregator_temperature: z.number().default(0.4),
|
|
@@ -84,7 +85,7 @@ export const PresetSchema = z.object({
|
|
|
84
85
|
judge_voting_strategy: z.string().default('majority'),
|
|
85
86
|
composite_merge_enabled: z.boolean().default(false),
|
|
86
87
|
smart_routing_enabled: z.boolean().default(false),
|
|
87
|
-
smart_routing_model: ModelSlotSchema.default({ provider: '
|
|
88
|
+
smart_routing_model: ModelSlotSchema.default({ provider: '', model: '' }),
|
|
88
89
|
budget_guard_enabled: z.boolean().default(false),
|
|
89
90
|
max_budget_usd: z.number().default(0),
|
|
90
91
|
budget_action: z.string().default('trim'),
|
|
@@ -99,7 +100,7 @@ export const Config = z.object({
|
|
|
99
100
|
enabled: z.boolean().default(true).volatile(),
|
|
100
101
|
default_preset: z.string().default('default').volatile(),
|
|
101
102
|
smart_routing_enabled: z.boolean().default(false).volatile(),
|
|
102
|
-
smart_routing_model: ModelSlotSchema.default({ provider: '
|
|
103
|
+
smart_routing_model: ModelSlotSchema.default({ provider: '', model: '' }).volatile(),
|
|
103
104
|
prices: z.dict(PriceRow).default({}).volatile(),
|
|
104
105
|
presets: z.array(PresetSchema).default([
|
|
105
106
|
{
|
|
@@ -107,10 +108,10 @@ export const Config = z.object({
|
|
|
107
108
|
enabled: true,
|
|
108
109
|
ask_clarifying_questions: true,
|
|
109
110
|
reference_models: [
|
|
110
|
-
{ provider: '
|
|
111
|
-
{ provider: '
|
|
111
|
+
{ provider: '', model: '', role_persona: 'proposer' },
|
|
112
|
+
{ provider: '', model: '', role_persona: 'challenger' },
|
|
112
113
|
],
|
|
113
|
-
aggregator: { provider: '
|
|
114
|
+
aggregator: { provider: '', model: '' },
|
|
114
115
|
reference_temperature: 0.6,
|
|
115
116
|
aggregator_temperature: 0.4,
|
|
116
117
|
max_tokens: 4096,
|
|
@@ -121,9 +122,9 @@ export const Config = z.object({
|
|
|
121
122
|
enabled: true,
|
|
122
123
|
ask_clarifying_questions: false,
|
|
123
124
|
reference_models: [
|
|
124
|
-
{ provider: '
|
|
125
|
+
{ provider: '', model: '', role_persona: 'fast' },
|
|
125
126
|
],
|
|
126
|
-
aggregator: { provider: '
|
|
127
|
+
aggregator: { provider: '', model: '' },
|
|
127
128
|
reference_temperature: 0.6,
|
|
128
129
|
aggregator_temperature: 0.2,
|
|
129
130
|
max_tokens: 4096,
|
|
@@ -134,10 +135,10 @@ export const Config = z.object({
|
|
|
134
135
|
enabled: true,
|
|
135
136
|
ask_clarifying_questions: true,
|
|
136
137
|
reference_models: [
|
|
137
|
-
{ provider: '
|
|
138
|
-
{ provider: '
|
|
138
|
+
{ provider: '', model: '', role_persona: 'architect' },
|
|
139
|
+
{ provider: '', model: '', role_persona: 'critic' },
|
|
139
140
|
],
|
|
140
|
-
aggregator: { provider: '
|
|
141
|
+
aggregator: { provider: '', model: '' },
|
|
141
142
|
reference_temperature: 0.6,
|
|
142
143
|
aggregator_temperature: 0.2,
|
|
143
144
|
max_tokens: 8192,
|
|
@@ -163,6 +164,7 @@ export function plainConfig(value) {
|
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
export function apply(ctx, config) {
|
|
167
|
+
if (ctx.logger) setLogger(ctx.logger)
|
|
166
168
|
refreshCatalogInBackground()
|
|
167
169
|
|
|
168
170
|
let localConfig = plainConfig(config ?? {})
|
|
@@ -209,7 +211,7 @@ export function apply(ctx, config) {
|
|
|
209
211
|
await settingsService.update(NS, plain)
|
|
210
212
|
}
|
|
211
213
|
} catch (err) {
|
|
212
|
-
|
|
214
|
+
logger.warn('[dsh-moa] Settings persistence warning:', err?.message || err)
|
|
213
215
|
}
|
|
214
216
|
return localConfig
|
|
215
217
|
}
|
|
@@ -375,8 +377,8 @@ export function apply(ctx, config) {
|
|
|
375
377
|
}
|
|
376
378
|
}
|
|
377
379
|
const targetPreset = (cfg.presets || []).find((p) => p.name === parsed.presetName) || cfg.presets?.[0]
|
|
378
|
-
const aggProvider = targetPreset?.aggregator?.provider || '
|
|
379
|
-
const aggModel = targetPreset?.aggregator?.model || '
|
|
380
|
+
const aggProvider = targetPreset?.aggregator?.provider || ''
|
|
381
|
+
const aggModel = targetPreset?.aggregator?.model || ''
|
|
380
382
|
const sessCwd = event.cwd || event.session?.cwd || (event.session?.path ? path.dirname(event.session.path) : undefined) || process.cwd()
|
|
381
383
|
|
|
382
384
|
const turnContext = {
|
|
@@ -388,6 +390,8 @@ export function apply(ctx, config) {
|
|
|
388
390
|
callLlm,
|
|
389
391
|
prices: cfg.prices || {},
|
|
390
392
|
liveCanvas,
|
|
393
|
+
webServerPort: ctx.webServer?.port,
|
|
394
|
+
timeMachineEngine: ctx.get?.('timeMachineEngine') || ctx.timeMachineEngine,
|
|
391
395
|
}
|
|
392
396
|
|
|
393
397
|
if (event.signal) {
|
package/lib/logger.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context logger abstraction for dsh-moa.
|
|
3
|
+
* Avoids raw direct console warnings in server-side modules and respects Cordis ctx.logger.
|
|
4
|
+
*/
|
|
5
|
+
let currentLogger = null
|
|
6
|
+
|
|
7
|
+
export function setLogger(logger) {
|
|
8
|
+
currentLogger = logger
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const logger = {
|
|
12
|
+
warn(...args) {
|
|
13
|
+
if (currentLogger && typeof currentLogger.warn === 'function') {
|
|
14
|
+
currentLogger.warn(...args)
|
|
15
|
+
} else if (typeof console !== 'undefined' && typeof console.debug === 'function') {
|
|
16
|
+
console.debug(...args)
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
debug(...args) {
|
|
20
|
+
if (currentLogger && typeof currentLogger.debug === 'function') {
|
|
21
|
+
currentLogger.debug(...args)
|
|
22
|
+
} else if (typeof console !== 'undefined' && typeof console.debug === 'function') {
|
|
23
|
+
console.debug(...args)
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
info(...args) {
|
|
27
|
+
if (currentLogger && typeof currentLogger.info === 'function') {
|
|
28
|
+
currentLogger.info(...args)
|
|
29
|
+
} else if (typeof console !== 'undefined' && typeof console.debug === 'function') {
|
|
30
|
+
console.debug(...args)
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
error(...args) {
|
|
34
|
+
if (currentLogger && typeof currentLogger.error === 'function') {
|
|
35
|
+
currentLogger.error(...args)
|
|
36
|
+
} else if (typeof console !== 'undefined' && typeof console.debug === 'function') {
|
|
37
|
+
console.debug(...args)
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
}
|
package/lib/moa-candidates.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logger } from './logger.js'
|
|
1
2
|
/**
|
|
2
3
|
* DeepSeek Harness Mixture of Agents (MoA) — Candidate Fan-Out & Retry
|
|
3
4
|
* With Per-Candidate Temperature, Temperature Gradient & Local Fallback Resilience
|
|
@@ -200,7 +201,7 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
200
201
|
}
|
|
201
202
|
break
|
|
202
203
|
} catch (fbErr) {
|
|
203
|
-
|
|
204
|
+
logger.warn(`[dsh-moa] Fallback ${fbLabel} also failed:`, fbErr?.message || fbErr)
|
|
204
205
|
}
|
|
205
206
|
}
|
|
206
207
|
}
|
|
@@ -208,7 +209,7 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
208
209
|
if (!fallbackRecovered) {
|
|
209
210
|
finishedCount++
|
|
210
211
|
const errMsg = err?.message || String(err)
|
|
211
|
-
|
|
212
|
+
logger.warn(`[dsh-moa] Reference ${label} failed:`, errMsg)
|
|
212
213
|
if (typeof onProgress === 'function') {
|
|
213
214
|
onProgress(`⚠️ *Candidate ${i + 1}/${total} (${label}) error: ${errMsg}*\n`)
|
|
214
215
|
}
|
package/lib/moa-router.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Smart Preset Router for Mixture of Agents (MoA).
|
|
3
|
-
* Supports heuristic keyword classification and optional
|
|
3
|
+
* Supports heuristic keyword classification and optional LLM zero-shot routing.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const INTENT_RULES = [
|
|
@@ -48,7 +48,7 @@ export async function resolvePresetForPrompt({
|
|
|
48
48
|
return { presetName: safeDefault, reason: 'default', isAutoRouted: false }
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// 1. If LLM routing model
|
|
51
|
+
// 1. If LLM routing model is configured and callable, run fast zero-shot classifier
|
|
52
52
|
if (routingModel?.provider && routingModel?.model && typeof callLlm === 'function') {
|
|
53
53
|
try {
|
|
54
54
|
const systemPrompt = `You are the MoA Preset Router. Given user prompt, classify which preset is best suited.
|
package/lib/moa-runner.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { logger } from './logger.js'
|
|
1
2
|
import path from 'node:path'
|
|
2
3
|
import crypto from 'node:crypto'
|
|
3
4
|
import { bestEffort } from './best-effort.js'
|
|
4
5
|
import { extractFileBlocks, collectProjectContext, formatProjectContext, isRefinementTask, writeCandidateWorkspace, promoteCandidateWorkspace, cleanMoaWorkspaces, verifyFileSyntax } from './file-workspace.js'
|
|
5
6
|
import { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
|
|
6
|
-
import {
|
|
7
|
+
import { recordMoaRunAsync, candidatesForHistory, isTestEnvironment } from './history.js'
|
|
7
8
|
import { createPromotedPreview } from './live-canvas.js'
|
|
8
9
|
import { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildCuratorSynthesisPrompt, buildSynthesisPrompt, buildPeerCritiquePrompt, ROLE_PERSONA_PROMPTS, SYSTEM_ROLE_PROPOSER, ANTIPATTERNS_RUBRIC } from './moa-prompts.js'
|
|
9
10
|
import { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
|
|
@@ -40,15 +41,29 @@ export async function runMoAPipeline({
|
|
|
40
41
|
testGateEnabled = null,
|
|
41
42
|
testCommand = null,
|
|
42
43
|
execFn = null,
|
|
44
|
+
webServerPort = null,
|
|
45
|
+
timeMachineEngine = null,
|
|
46
|
+
timeMachineUrl = null,
|
|
47
|
+
force = isTestEnvironment() ? true : false,
|
|
48
|
+
checkpoint = true,
|
|
49
|
+
checkpointFn = null,
|
|
43
50
|
}) {
|
|
51
|
+
const promoteOpts = {
|
|
52
|
+
webServerPort,
|
|
53
|
+
timeMachineEngine,
|
|
54
|
+
timeMachineUrl,
|
|
55
|
+
force: Boolean(force),
|
|
56
|
+
checkpoint,
|
|
57
|
+
checkpointFn,
|
|
58
|
+
}
|
|
44
59
|
if (signal?.aborted) return { error: new Error('Turn aborted') }
|
|
45
60
|
const startTime = Date.now()
|
|
46
61
|
|
|
47
62
|
// 1. Resolve configurations
|
|
48
63
|
const referenceModels = Array.isArray(preset?.reference_models) && preset.reference_models.length > 0
|
|
49
|
-
? preset.reference_models : [{ provider: '
|
|
64
|
+
? preset.reference_models : [{ provider: '', model: '' }]
|
|
50
65
|
const primaryJudge = preset?.aggregator?.provider && preset?.aggregator?.model
|
|
51
|
-
? preset.aggregator : { provider: '
|
|
66
|
+
? preset.aggregator : { provider: '', model: '' }
|
|
52
67
|
|
|
53
68
|
const fallbackJudges = Array.isArray(preset?.aggregator_fallbacks) ? preset.aggregator_fallbacks : []
|
|
54
69
|
const judgesChain = [primaryJudge, ...fallbackJudges]
|
|
@@ -227,7 +242,7 @@ export async function runMoAPipeline({
|
|
|
227
242
|
}
|
|
228
243
|
qUsage = estimateTokenCost(primaryJudge, qFallbackUsage, prices)
|
|
229
244
|
} catch (err) {
|
|
230
|
-
|
|
245
|
+
logger.warn('[dsh-moa] Questionnaire synthesis failed, proceeding with fallback questions:', err)
|
|
231
246
|
questionsContent = `### Clarification of Requirements: "${userPrompt}"\n\n1. **Architecture & Scope**: Single-file deliverable or multi-module project structure?\n2. **Design & Style**: Minimalist, dark mode, or clean neutral theme?\n3. **Functional Priorities**: Core MVP or comprehensive extended implementation?\n\n*Reply with your preferences (e.g. "1, 2") or proceed with defaults.*`
|
|
232
247
|
}
|
|
233
248
|
|
|
@@ -237,7 +252,7 @@ export async function runMoAPipeline({
|
|
|
237
252
|
const durationMs = Date.now() - startTime
|
|
238
253
|
|
|
239
254
|
try {
|
|
240
|
-
|
|
255
|
+
await recordMoaRunAsync({
|
|
241
256
|
prompt: userPrompt,
|
|
242
257
|
preset: preset?.name || 'default',
|
|
243
258
|
isRefinement,
|
|
@@ -251,7 +266,7 @@ export async function runMoAPipeline({
|
|
|
251
266
|
durationMs,
|
|
252
267
|
}, historyFilePath || undefined)
|
|
253
268
|
} catch (histErr) {
|
|
254
|
-
|
|
269
|
+
logger.warn('[dsh-moa] Failed to record questionnaire run in history:', histErr?.message || histErr)
|
|
255
270
|
}
|
|
256
271
|
|
|
257
272
|
return {
|
|
@@ -282,7 +297,7 @@ export async function runMoAPipeline({
|
|
|
282
297
|
const single = successfulRefs[0]
|
|
283
298
|
let promotedFiles = []
|
|
284
299
|
if (single.files?.length > 0) {
|
|
285
|
-
promotedFiles = await promoteCandidateWorkspace(cwd, single.index)
|
|
300
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, single.index, promoteOpts)
|
|
286
301
|
} else {
|
|
287
302
|
await cleanMoaWorkspaces(cwd)
|
|
288
303
|
}
|
|
@@ -293,7 +308,7 @@ export async function runMoAPipeline({
|
|
|
293
308
|
const durationMs = Date.now() - startTime
|
|
294
309
|
|
|
295
310
|
try {
|
|
296
|
-
|
|
311
|
+
await recordMoaRunAsync({
|
|
297
312
|
prompt: userPrompt,
|
|
298
313
|
preset: preset?.name || 'default',
|
|
299
314
|
isRefinement,
|
|
@@ -308,7 +323,7 @@ export async function runMoAPipeline({
|
|
|
308
323
|
durationMs,
|
|
309
324
|
}, historyFilePath || undefined)
|
|
310
325
|
} catch (histErr) {
|
|
311
|
-
|
|
326
|
+
logger.warn('[dsh-moa] Failed to record fast synthesis run in history:', histErr?.message || histErr)
|
|
312
327
|
}
|
|
313
328
|
|
|
314
329
|
return {
|
|
@@ -376,7 +391,7 @@ export async function runMoAPipeline({
|
|
|
376
391
|
cand.costUsd = Number(((cand.costUsd || 0) + extraCost.costUsd).toFixed(5))
|
|
377
392
|
}
|
|
378
393
|
} catch (r2CostErr) {
|
|
379
|
-
|
|
394
|
+
logger.warn('[dsh-moa] Failed to estimate Round 2 cost:', r2CostErr?.message || r2CostErr)
|
|
380
395
|
}
|
|
381
396
|
})
|
|
382
397
|
await Promise.allSettled(r2Promises)
|
|
@@ -460,7 +475,7 @@ export async function runMoAPipeline({
|
|
|
460
475
|
break
|
|
461
476
|
} catch (err) {
|
|
462
477
|
lastJudgeError = err
|
|
463
|
-
|
|
478
|
+
logger.warn(`[dsh-moa] Judge ${currentLabel} failed:`, err)
|
|
464
479
|
if (typeof onProgress === 'function') {
|
|
465
480
|
const nextJudge = judgesChain[jIdx + 1]
|
|
466
481
|
const nextHint = nextJudge ? ` Trying fallback ${slotLabel(nextJudge)}...` : ''
|
|
@@ -491,12 +506,12 @@ export async function runMoAPipeline({
|
|
|
491
506
|
const keepWorkspaces = allowCandidateOverride
|
|
492
507
|
if (synthesizedFiles.length > 0) {
|
|
493
508
|
await writeCandidateWorkspace(cwd, 'curator-synthesis', synthesizedFiles)
|
|
494
|
-
promotedFiles = await promoteCandidateWorkspace(cwd, 'curator-synthesis', { keepMoa: keepWorkspaces })
|
|
509
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, 'curator-synthesis', { ...promoteOpts, keepMoa: keepWorkspaces })
|
|
495
510
|
} else if (referenceOutputs[winningIndex - 1]?.files?.length > 0) {
|
|
496
|
-
promotedFiles = await promoteCandidateWorkspace(cwd, winningIndex, { keepMoa: keepWorkspaces })
|
|
511
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, winningIndex, { ...promoteOpts, keepMoa: keepWorkspaces })
|
|
497
512
|
} else if (successfulRefs[0]?.files?.length > 0) {
|
|
498
513
|
const fallbackIdx = successfulRefs[0].index
|
|
499
|
-
promotedFiles = await promoteCandidateWorkspace(cwd, fallbackIdx, { keepMoa: keepWorkspaces })
|
|
514
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, fallbackIdx, { ...promoteOpts, keepMoa: keepWorkspaces })
|
|
500
515
|
} else if (!keepWorkspaces) {
|
|
501
516
|
await cleanMoaWorkspaces(cwd)
|
|
502
517
|
}
|
|
@@ -529,7 +544,7 @@ export async function runMoAPipeline({
|
|
|
529
544
|
}
|
|
530
545
|
|
|
531
546
|
try {
|
|
532
|
-
|
|
547
|
+
await recordMoaRunAsync({
|
|
533
548
|
id: runId,
|
|
534
549
|
prompt: userPrompt,
|
|
535
550
|
preset: preset?.name || 'default',
|
|
@@ -546,7 +561,7 @@ export async function runMoAPipeline({
|
|
|
546
561
|
durationMs,
|
|
547
562
|
}, historyFilePath || undefined)
|
|
548
563
|
} catch (histErr) {
|
|
549
|
-
|
|
564
|
+
logger.warn('[dsh-moa] Failed to record run in history:', histErr)
|
|
550
565
|
}
|
|
551
566
|
|
|
552
567
|
return {
|
package/lib/moa-stream.js
CHANGED
|
@@ -9,7 +9,7 @@ import { runMoAPipeline } from './moa-runner.js'
|
|
|
9
9
|
/**
|
|
10
10
|
* Streams a full MoA turn into chat with live aggregator tokens and progress feedback.
|
|
11
11
|
*/
|
|
12
|
-
export async function* streamMoATurn({ targetPreset, userPrompt, messages, callLlm, cwd, prices, historyFilePath, liveCanvas }, options = {}) {
|
|
12
|
+
export async function* streamMoATurn({ targetPreset, userPrompt, messages, callLlm, cwd, prices, historyFilePath, liveCanvas, webServerPort, timeMachineEngine, force }, options = {}) {
|
|
13
13
|
const signal = options?.signal
|
|
14
14
|
if (signal?.aborted) return
|
|
15
15
|
|
|
@@ -40,6 +40,9 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
|
|
|
40
40
|
prices,
|
|
41
41
|
historyFilePath,
|
|
42
42
|
liveCanvas,
|
|
43
|
+
webServerPort,
|
|
44
|
+
timeMachineEngine,
|
|
45
|
+
force,
|
|
43
46
|
signal,
|
|
44
47
|
})
|
|
45
48
|
.catch((err) => ({ error: err }))
|
package/lib/moa-test-gate.js
CHANGED
|
@@ -4,6 +4,7 @@ import fsSync from 'node:fs'
|
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { spawn } from 'node:child_process'
|
|
6
6
|
import { assertPathContained } from './file-workspace.js'
|
|
7
|
+
import { bestEffort } from './best-effort.js'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Resolves the directory for candidate sandbox.
|
|
@@ -179,9 +180,9 @@ export async function runCandidateTestGate({
|
|
|
179
180
|
}
|
|
180
181
|
} finally {
|
|
181
182
|
if (stageDir) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
await bestEffort('moa-test-gate cleanup stageDir', () =>
|
|
184
|
+
fs.rm(stageDir, { recursive: true, force: true })
|
|
185
|
+
)
|
|
185
186
|
}
|
|
186
187
|
}
|
|
187
188
|
}
|
package/lib/pricing.js
CHANGED
|
@@ -11,7 +11,21 @@ import { homedir } from 'node:os'
|
|
|
11
11
|
import { dirname, join } from 'node:path'
|
|
12
12
|
|
|
13
13
|
export const CATALOG_URL = 'https://openrouter.ai/api/v1/models'
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
export function getDefaultCacheFile() {
|
|
16
|
+
if (process.env.DSH_CATALOG_FILE) {
|
|
17
|
+
return process.env.DSH_CATALOG_FILE
|
|
18
|
+
}
|
|
19
|
+
if (process.env.DSH_STORAGE_DIR) {
|
|
20
|
+
return join(process.env.DSH_STORAGE_DIR, 'dsh-moa-catalog.json')
|
|
21
|
+
}
|
|
22
|
+
const baseDir = process.env.DSH_HOME
|
|
23
|
+
? join(process.env.DSH_HOME, '.dsh')
|
|
24
|
+
: join(homedir(), '.dsh')
|
|
25
|
+
return join(baseDir, 'storages', 'dsh-moa-catalog.json')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const CACHE_FILE = getDefaultCacheFile()
|
|
15
29
|
export const REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000 // 24 hours
|
|
16
30
|
|
|
17
31
|
/**
|
|
@@ -43,7 +57,7 @@ export function resetMemoryCatalog() {
|
|
|
43
57
|
/**
|
|
44
58
|
* Load cached catalog from disk into memory.
|
|
45
59
|
*/
|
|
46
|
-
export function loadCachedCatalog(cachePath =
|
|
60
|
+
export function loadCachedCatalog(cachePath = getDefaultCacheFile()) {
|
|
47
61
|
if (memoryCatalog && memoryCachePath === cachePath) return memoryCatalog
|
|
48
62
|
try {
|
|
49
63
|
if (existsSync(cachePath)) {
|
|
@@ -69,7 +83,7 @@ export function loadCachedCatalog(cachePath = CACHE_FILE) {
|
|
|
69
83
|
*/
|
|
70
84
|
export async function fetchCatalog({
|
|
71
85
|
url = CATALOG_URL,
|
|
72
|
-
cachePath =
|
|
86
|
+
cachePath = getDefaultCacheFile(),
|
|
73
87
|
signal = null,
|
|
74
88
|
} = {}) {
|
|
75
89
|
try {
|
|
@@ -118,7 +132,7 @@ export async function fetchCatalog({
|
|
|
118
132
|
/**
|
|
119
133
|
* Background refresh catalog if stale (> 24 hours).
|
|
120
134
|
*/
|
|
121
|
-
export function refreshCatalogInBackground(cachePath =
|
|
135
|
+
export function refreshCatalogInBackground(cachePath = getDefaultCacheFile()) {
|
|
122
136
|
const catalog = loadCachedCatalog(cachePath)
|
|
123
137
|
const isStale = !lastFetchedAt || Date.now() - lastFetchedAt > REFRESH_INTERVAL_MS
|
|
124
138
|
if (isStale) {
|
|
@@ -134,16 +148,20 @@ export function refreshCatalogInBackground(cachePath = CACHE_FILE) {
|
|
|
134
148
|
/**
|
|
135
149
|
* Resolve price rates (USD per 1M tokens) for a given provider/model slot.
|
|
136
150
|
*/
|
|
137
|
-
export function resolveModelRates(slot = {}, customPrices = {}, cachePath =
|
|
151
|
+
export function resolveModelRates(slot = {}, customPrices = {}, cachePath = getDefaultCacheFile()) {
|
|
138
152
|
const provider = (slot?.provider || '').trim().toLowerCase()
|
|
139
153
|
const model = (slot?.model || '').trim().toLowerCase()
|
|
140
154
|
const fullKey = provider ? `${provider}/${model}` : model
|
|
155
|
+
const colonKey = provider ? `${provider}:${model}` : model
|
|
141
156
|
|
|
142
157
|
// 1. Check custom user overrides in config
|
|
143
158
|
if (customPrices && typeof customPrices === 'object') {
|
|
144
159
|
if (customPrices[fullKey]) return normalizeRates(customPrices[fullKey])
|
|
160
|
+
if (customPrices[colonKey]) return normalizeRates(customPrices[colonKey])
|
|
145
161
|
if (customPrices[model]) return normalizeRates(customPrices[model])
|
|
146
|
-
if (provider && customPrices[`${provider}/*`]
|
|
162
|
+
if (provider && (customPrices[`${provider}/*`] || customPrices[`${provider}:*`])) {
|
|
163
|
+
return normalizeRates(customPrices[`${provider}/*`] || customPrices[`${provider}:*`])
|
|
164
|
+
}
|
|
147
165
|
if (customPrices['*']) return normalizeRates(customPrices['*'])
|
|
148
166
|
}
|
|
149
167
|
|
|
@@ -271,7 +289,7 @@ function normalizeRates(rate) {
|
|
|
271
289
|
/**
|
|
272
290
|
* Estimate USD cost and token totals for a single model call.
|
|
273
291
|
*/
|
|
274
|
-
export function estimateTokenCost(slot = {}, usage = {}, customPrices = {}, cachePath =
|
|
292
|
+
export function estimateTokenCost(slot = {}, usage = {}, customPrices = {}, cachePath = getDefaultCacheFile()) {
|
|
275
293
|
const promptTokens = usage.prompt_tokens ?? usage.inputTokens ?? usage.input ?? usage.promptTokens ?? 0
|
|
276
294
|
const completionTokens = usage.completion_tokens ?? usage.outputTokens ?? usage.output ?? usage.completionTokens ?? 0
|
|
277
295
|
const cacheHitTokens = usage.prompt_tokens_details?.cached_tokens ?? usage.cacheHitTokens ?? 0
|
package/lib/routes.js
CHANGED
|
@@ -46,7 +46,11 @@ ctx.effect(() => {
|
|
|
46
46
|
return ctx.webServer.register({
|
|
47
47
|
kind: 'exact',
|
|
48
48
|
path: '/dsh-moa/status',
|
|
49
|
-
handler: (
|
|
49
|
+
handler: (req, res) => {
|
|
50
|
+
if (req.method !== 'GET') {
|
|
51
|
+
writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
52
|
+
return
|
|
53
|
+
}
|
|
50
54
|
try {
|
|
51
55
|
const cfg = live()
|
|
52
56
|
writeJson(res, 200, {
|
|
@@ -67,7 +71,11 @@ ctx.effect(() => {
|
|
|
67
71
|
return ctx.webServer.register({
|
|
68
72
|
kind: 'exact',
|
|
69
73
|
path: '/dsh-moa/models',
|
|
70
|
-
handler: async (
|
|
74
|
+
handler: async (req, res) => {
|
|
75
|
+
if (req.method !== 'GET') {
|
|
76
|
+
writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
77
|
+
return
|
|
78
|
+
}
|
|
71
79
|
if (_modelsCache.models.length > 0 && Date.now() - _modelsCache.timestamp < MODELS_CACHE_TTL) {
|
|
72
80
|
writeJson(res, 200, { ok: true, models: _modelsCache.models, cached: true })
|
|
73
81
|
return
|
|
@@ -94,7 +102,7 @@ ctx.effect(() => {
|
|
|
94
102
|
}
|
|
95
103
|
}
|
|
96
104
|
} catch (e) {
|
|
97
|
-
|
|
105
|
+
ctx.logger?.warn?.('[dsh-moa] ctx.llm.listAvailableModels error:', e)
|
|
98
106
|
}
|
|
99
107
|
|
|
100
108
|
try {
|
|
@@ -119,7 +127,7 @@ ctx.effect(() => {
|
|
|
119
127
|
await Promise.allSettled(queries)
|
|
120
128
|
}
|
|
121
129
|
} catch (e) {
|
|
122
|
-
|
|
130
|
+
ctx.logger?.warn?.('[dsh-moa] ctx.llm.listConfigurableProviders error:', e)
|
|
123
131
|
}
|
|
124
132
|
}
|
|
125
133
|
|
|
@@ -156,7 +164,7 @@ ctx.effect(() => {
|
|
|
156
164
|
}
|
|
157
165
|
}
|
|
158
166
|
} catch (e) {
|
|
159
|
-
|
|
167
|
+
ctx.logger?.warn?.('[dsh-moa] Error reading settings:', e)
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
_modelsCache = { timestamp: Date.now(), models: result }
|
|
@@ -241,8 +249,10 @@ ctx.effect(() => {
|
|
|
241
249
|
}
|
|
242
250
|
try {
|
|
243
251
|
const u = new URL(req.url, 'http://localhost')
|
|
244
|
-
const
|
|
245
|
-
const
|
|
252
|
+
const rawLimit = parseInt(u.searchParams.get('limit') || '20', 10)
|
|
253
|
+
const rawOffset = parseInt(u.searchParams.get('offset') || '0', 10)
|
|
254
|
+
const limit = Math.max(1, Math.min(100, Number.isFinite(rawLimit) ? rawLimit : 20))
|
|
255
|
+
const offset = Math.max(0, Number.isFinite(rawOffset) ? rawOffset : 0)
|
|
246
256
|
const data = getMoaHistory(limit, offset)
|
|
247
257
|
writeJson(res, 200, { ok: true, ...data })
|
|
248
258
|
} catch (err) {
|
|
@@ -309,7 +319,12 @@ ctx.effect(() => {
|
|
|
309
319
|
return
|
|
310
320
|
}
|
|
311
321
|
const cwd = body?.cwd || process.cwd()
|
|
312
|
-
const promotedFiles = await promoteCandidateWorkspace(cwd, candidateIndex, {
|
|
322
|
+
const promotedFiles = await promoteCandidateWorkspace(cwd, candidateIndex, {
|
|
323
|
+
keepMoa: true,
|
|
324
|
+
port: ctx.webServer?.port,
|
|
325
|
+
timeMachineEngine: ctx.get?.('timeMachineEngine') || ctx.timeMachineEngine,
|
|
326
|
+
force: Boolean(body?.force),
|
|
327
|
+
})
|
|
313
328
|
writeJson(res, 200, { ok: true, candidateIndex, promotedFiles, checkpoint: promotedFiles?.checkpoint || null })
|
|
314
329
|
} catch (err) {
|
|
315
330
|
writeJson(res, 500, { ok: false, error: err?.message || String(err) })
|
|
@@ -470,18 +485,26 @@ ctx.effect(() => {
|
|
|
470
485
|
writeJson(res, 405, { ok: false, error: 'POST required' })
|
|
471
486
|
return
|
|
472
487
|
}
|
|
488
|
+
if (!isSafeWriteRequest(req)) {
|
|
489
|
+
writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
|
|
490
|
+
return
|
|
491
|
+
}
|
|
473
492
|
try {
|
|
493
|
+
const cfg = live()
|
|
494
|
+
if (cfg.enabled === false) {
|
|
495
|
+
writeJson(res, 400, { ok: false, error: 'MoA is disabled in settings' })
|
|
496
|
+
return
|
|
497
|
+
}
|
|
474
498
|
const raw = await readBody(req)
|
|
475
499
|
const body = JSON.parse(raw || '{}')
|
|
476
500
|
const prompt = body?.prompt || ''
|
|
477
|
-
const cfg = live()
|
|
478
501
|
const result = await resolvePresetForPrompt({
|
|
479
502
|
prompt,
|
|
480
503
|
presets: cfg.presets || [],
|
|
481
504
|
defaultPreset: cfg.default_preset || 'default',
|
|
482
505
|
routingModel: cfg.smart_routing_model,
|
|
483
506
|
callLlm,
|
|
484
|
-
enabled:
|
|
507
|
+
enabled: cfg.enabled !== false,
|
|
485
508
|
})
|
|
486
509
|
writeJson(res, 200, { ok: true, ...result })
|
|
487
510
|
} catch (err) {
|
|
@@ -491,6 +514,9 @@ ctx.effect(() => {
|
|
|
491
514
|
})
|
|
492
515
|
}, 'dsh-moa: route preset endpoint')
|
|
493
516
|
|
|
517
|
+
let _lastRunTimestamp = 0
|
|
518
|
+
const RUN_MIN_INTERVAL_MS = 1000
|
|
519
|
+
|
|
494
520
|
// Route: /dsh-moa/run
|
|
495
521
|
ctx.effect(() => {
|
|
496
522
|
return ctx.webServer.register({
|
|
@@ -505,6 +531,12 @@ ctx.effect(() => {
|
|
|
505
531
|
writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
|
|
506
532
|
return
|
|
507
533
|
}
|
|
534
|
+
const now = Date.now()
|
|
535
|
+
if (now - _lastRunTimestamp < RUN_MIN_INTERVAL_MS) {
|
|
536
|
+
writeJson(res, 429, { ok: false, error: 'Rate limit exceeded: please wait before starting another run' })
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
_lastRunTimestamp = now
|
|
508
540
|
|
|
509
541
|
try {
|
|
510
542
|
const raw = await readBody(req)
|
package/lib/updater.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Implements canonical DSH plugin-updater specification.
|
|
3
3
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { existsSync, readFileSync, statSync, rmSync } from 'node:fs';
|
|
6
6
|
import { readFile } from 'node:fs/promises';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
import { basename, dirname, isAbsolute, resolve } from 'node:path';
|
|
@@ -183,8 +183,79 @@ export async function getUpdateStatus(options, target = runtime()) {
|
|
|
183
183
|
};
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
export function readLockPid(lockPath) {
|
|
187
|
+
try {
|
|
188
|
+
if (!existsSync(lockPath)) return undefined;
|
|
189
|
+
const stat = statSync(lockPath);
|
|
190
|
+
if (stat.isDirectory()) return undefined;
|
|
191
|
+
const content = readFileSync(lockPath, 'utf8').trim();
|
|
192
|
+
if (!content) return undefined;
|
|
193
|
+
try {
|
|
194
|
+
const parsed = JSON.parse(content);
|
|
195
|
+
if (parsed && typeof parsed.pid === 'number') return parsed.pid;
|
|
196
|
+
} catch (parseErr) { /* ignore json parse error */ }
|
|
197
|
+
const num = parseInt(content, 10);
|
|
198
|
+
if (!isNaN(num) && num > 0) return num;
|
|
199
|
+
const match = /\b(\d{1,8})\b/.exec(content);
|
|
200
|
+
if (match) return Number(match[1]);
|
|
201
|
+
return undefined;
|
|
202
|
+
} catch {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function isPidAlive(pid) {
|
|
208
|
+
if (typeof pid !== 'number' || isNaN(pid) || pid <= 0) return false;
|
|
209
|
+
try {
|
|
210
|
+
process.kill(pid, 0);
|
|
211
|
+
return true;
|
|
212
|
+
} catch (err) {
|
|
213
|
+
return err?.code === 'EPERM';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function checkAndCleanLock(profileDir, childPid) {
|
|
218
|
+
const lockPath = resolve(profileDir, 'package.json.lock');
|
|
219
|
+
if (!existsSync(lockPath)) return { locked: false };
|
|
220
|
+
const pid = readLockPid(lockPath);
|
|
221
|
+
if (pid !== undefined) {
|
|
222
|
+
if (isPidAlive(pid)) {
|
|
223
|
+
if (childPid !== undefined && pid === childPid) {
|
|
224
|
+
try {
|
|
225
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
226
|
+
return { locked: false, cleaned: true };
|
|
227
|
+
} catch (rmErr) { return { locked: true, pid }; }
|
|
228
|
+
}
|
|
229
|
+
return { locked: true, pid };
|
|
230
|
+
}
|
|
231
|
+
// PID is dead, remove stale lock
|
|
232
|
+
try {
|
|
233
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
234
|
+
return { locked: false, cleaned: true };
|
|
235
|
+
} catch (rmErr) { return { locked: true, pid }; }
|
|
236
|
+
}
|
|
237
|
+
// Unknown or empty lockfile format
|
|
238
|
+
if (childPid !== undefined) {
|
|
239
|
+
try {
|
|
240
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
241
|
+
return { locked: false, cleaned: true };
|
|
242
|
+
} catch (parseErr) { /* ignore json parse error */ }
|
|
243
|
+
}
|
|
244
|
+
return { locked: true };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function installExact(target, packageSpec, options = {}) {
|
|
187
248
|
if (target.cliEntry === undefined) throw new Error('Automatic update is unavailable in this runtime.');
|
|
249
|
+
|
|
250
|
+
const lockStatus = checkAndCleanLock(target.profileDir);
|
|
251
|
+
if (lockStatus.locked) {
|
|
252
|
+
const error = new Error('Another plugin installation is currently in progress.');
|
|
253
|
+
error.status = 409;
|
|
254
|
+
error.statusCode = 409;
|
|
255
|
+
error.code = 'ELOCKED';
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
|
|
188
259
|
await new Promise((resolvePromise, reject) => {
|
|
189
260
|
// Note: Respect strict ecosystem quarantine policy without age bypass
|
|
190
261
|
const args = [
|
|
@@ -201,13 +272,24 @@ async function installExact(target, packageSpec, options) {
|
|
|
201
272
|
let detail = '';
|
|
202
273
|
child.stdout?.on('data', chunk => { detail = (detail + String(chunk)).slice(-4_000); });
|
|
203
274
|
child.stderr?.on('data', chunk => { detail = (detail + String(chunk)).slice(-4_000); });
|
|
275
|
+
let timedOut = false;
|
|
276
|
+
const timeoutMs = options.timeoutMs ?? UPDATE_TIMEOUT_MS;
|
|
204
277
|
const timer = setTimeout(() => {
|
|
205
|
-
|
|
278
|
+
timedOut = true;
|
|
279
|
+
try { child.kill('SIGTERM'); } catch (killErr) { /* best-effort */ }
|
|
280
|
+
setTimeout(() => {
|
|
281
|
+
try { child.kill('SIGKILL'); } catch (killErr) { /* best-effort */ }
|
|
282
|
+
checkAndCleanLock(target.profileDir, child.pid);
|
|
283
|
+
}, 500).unref?.();
|
|
206
284
|
reject(new Error('Update timed out; use the normal DSH update flow.'));
|
|
207
|
-
},
|
|
285
|
+
}, timeoutMs);
|
|
208
286
|
child.once('error', error => { clearTimeout(timer); reject(error); });
|
|
209
|
-
child.once('exit', code => {
|
|
287
|
+
child.once('exit', (code, signal) => {
|
|
210
288
|
clearTimeout(timer);
|
|
289
|
+
if (timedOut) {
|
|
290
|
+
checkAndCleanLock(target.profileDir, child.pid);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
211
293
|
if (code === 0) resolvePromise();
|
|
212
294
|
else reject(new Error(detail.trim() || `Update exited with code ${String(code)}.`));
|
|
213
295
|
});
|
|
@@ -253,6 +335,11 @@ export function registerPluginUpdater(ctx, options) {
|
|
|
253
335
|
json(response, 409, { error: 'This plugin is already updating.' });
|
|
254
336
|
return;
|
|
255
337
|
}
|
|
338
|
+
const lockStatus = checkAndCleanLock(target.profileDir);
|
|
339
|
+
if (lockStatus.locked) {
|
|
340
|
+
json(response, 409, { error: 'Another plugin installation is currently in progress.' });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
256
343
|
installing = true;
|
|
257
344
|
try {
|
|
258
345
|
const before = await getUpdateStatus(options, target);
|
|
@@ -274,6 +361,10 @@ export function registerPluginUpdater(ctx, options) {
|
|
|
274
361
|
installing = false;
|
|
275
362
|
}
|
|
276
363
|
} catch (error) {
|
|
364
|
+
if (error?.status === 409 || error?.statusCode === 409) {
|
|
365
|
+
json(response, 409, { error: error.message || 'Another plugin installation is currently in progress.' });
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
277
368
|
ctx.logger?.warn?.(`plugin updater failed: ${String(error)}`);
|
|
278
369
|
json(response, 503, { error: 'Plugin update failed; see server logs.' });
|
|
279
370
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-moa",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.30",
|
|
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",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
|
-
"test": "node --test test/*.test.mjs",
|
|
22
|
+
"test": "node --import ./test/_setup.mjs --test test/*.test.mjs",
|
|
23
23
|
"build:client": "node scripts/build-client.mjs"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
"dsh-plugin",
|
|
28
28
|
"moa",
|
|
29
29
|
"mixture-of-agents",
|
|
30
|
-
"slash-command"
|
|
31
|
-
"hermes-agent"
|
|
30
|
+
"slash-command"
|
|
32
31
|
],
|
|
33
32
|
"repository": {
|
|
34
33
|
"type": "git",
|