@goodandready/dsh-moa 0.2.70 → 0.2.72
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 +9 -0
- package/README.ru.md +9 -0
- package/README.zh.md +9 -0
- package/lib/client.js +103 -0
- package/lib/events.js +3 -0
- package/lib/moa-adversarial-tdd.js +222 -0
- package/lib/moa-ast-merge.js +233 -0
- package/lib/moa-schema.js +15 -0
- package/lib/moa-speculative.js +195 -0
- package/lib/moa-swarm.js +13 -4
- package/lib/moa-workbench.js +6 -0
- package/lib/pipeline/candidates.js +42 -2
- package/lib/pipeline/context.js +5 -0
- package/lib/pipeline/early-consensus.js +324 -0
- package/lib/pipeline/runner.js +62 -10
- package/lib/pipeline/syntax-filter.js +324 -0
- package/lib/tui-settings.js +2 -1
- package/package.json +2 -1
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-Level Composite Merge Integration via dsh-context-lens (#248)
|
|
3
|
+
* Semantically merges candidate files at the AST/declaration level (deduplicating imports,
|
|
4
|
+
* combining type signatures, JSDoc comments, and function bodies) using dsh-context-lens
|
|
5
|
+
* when available via ctx.reflect / service(ctx, 'contextLens'), with a clean fallback
|
|
6
|
+
* to structured declaration merge and standard string composite directives.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import vm from 'node:vm'
|
|
10
|
+
import { service } from './services.js'
|
|
11
|
+
import { logger } from './logger.js'
|
|
12
|
+
import { buildCompositeBlockDirectives } from './moa-multi-judge.js'
|
|
13
|
+
import { extractFileBlocks } from './workspace/fs.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Checks whether dsh-context-lens service is available on the Cordis context.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveContextLensService(ctx, explicitLens = null) {
|
|
19
|
+
if (explicitLens) return explicitLens
|
|
20
|
+
const fromSvc = service(ctx, 'contextLens')
|
|
21
|
+
if (fromSvc) return fromSvc
|
|
22
|
+
try {
|
|
23
|
+
if (ctx?.reflect && typeof ctx.reflect.get === 'function') {
|
|
24
|
+
const refl = ctx.reflect.get('contextLens') || ctx.reflect.get('dsh-context-lens')
|
|
25
|
+
if (refl) return refl
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
logger.debug('[dsh-moa:ast] context-lens reflection error:', err?.message || err)
|
|
29
|
+
}
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parses top-level imports, declarations, and JSDoc from JS/TS source code.
|
|
35
|
+
*/
|
|
36
|
+
export function parseModuleDeclarations(source = '') {
|
|
37
|
+
const lines = String(source || '').split(/\r?\n/)
|
|
38
|
+
const imports = []
|
|
39
|
+
const bodyLines = []
|
|
40
|
+
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
const trimmed = line.trim()
|
|
43
|
+
if (/^import\s+/.test(trimmed) && (trimmed.endsWith(';') || trimmed.includes(' from '))) {
|
|
44
|
+
imports.push(trimmed.replace(/;$/, ''))
|
|
45
|
+
} else {
|
|
46
|
+
bodyLines.push(line)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const bodyText = bodyLines.join('\n')
|
|
51
|
+
const declarations = new Map()
|
|
52
|
+
const declRegex = /((?:\/\*\*[\s\S]*?\*\/\s*)?)(export\s+(?:async\s+)?(?:function|class|const|let|type|interface)\s+([A-Za-z0-9_$]+)[\s\S]*?)(?=(?:\n(?:\/\*\*[\s\S]*?\*\/\s*)?export\s+(?:async\s+)?(?:function|class|const|let|type|interface)\s+)|$)/g
|
|
53
|
+
let match
|
|
54
|
+
while ((match = declRegex.exec(bodyText)) !== null) {
|
|
55
|
+
const jsdoc = (match[1] || '').trim()
|
|
56
|
+
const code = (match[2] || '').trim()
|
|
57
|
+
const name = match[3]
|
|
58
|
+
if (name) {
|
|
59
|
+
declarations.set(name, { name, jsdoc, code })
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { imports, declarations, rawBody: bodyText.trim() }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Validates JavaScript syntax using node:vm compileFunction (ignoring TypeScript-only syntax).
|
|
68
|
+
*/
|
|
69
|
+
export function isValidJsAstSyntax(code = '', filename = 'module.js') {
|
|
70
|
+
if (/\.tsx?$/i.test(filename)) return true
|
|
71
|
+
const stripped = String(code)
|
|
72
|
+
.replace(/^\s*import\s+[\s\S]*?from\s+['"][^'"]+['"];?\s*$/gm, '')
|
|
73
|
+
.replace(/^\s*import\s+['"][^'"]+['"];?\s*$/gm, '')
|
|
74
|
+
.replace(/^\s*export\s+default\s+/gm, 'const __default_export__ = ')
|
|
75
|
+
.replace(/^\s*export\s+(async\s+function|function|class|const|let|var)\b/gm, '$1')
|
|
76
|
+
.replace(/^\s*export\s*\{[^}]*\};?\s*$/gm, '')
|
|
77
|
+
try {
|
|
78
|
+
new vm.Script(stripped, { filename })
|
|
79
|
+
return true
|
|
80
|
+
} catch {
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Merges multiple candidate versions of the same file at declaration/AST level:
|
|
87
|
+
* - Deduplicates all import statements without collisions
|
|
88
|
+
* - Combines the richest JSDoc documentation, type signature, and implementation per symbol
|
|
89
|
+
*/
|
|
90
|
+
export async function mergeCandidateAstFiles(candidates = [], options = {}) {
|
|
91
|
+
const lens = resolveContextLensService(options.ctx, options.contextLens)
|
|
92
|
+
const byPath = new Map()
|
|
93
|
+
|
|
94
|
+
for (const cand of candidates) {
|
|
95
|
+
if (!cand || cand.ok === false) continue
|
|
96
|
+
const files = Array.isArray(cand.files) && cand.files.length > 0
|
|
97
|
+
? cand.files
|
|
98
|
+
: extractFileBlocks(cand.text || '')
|
|
99
|
+
for (const f of files) {
|
|
100
|
+
const rel = f.relativePath || f.path
|
|
101
|
+
if (!rel || !f.content) continue
|
|
102
|
+
if (!byPath.has(rel)) byPath.set(rel, [])
|
|
103
|
+
byPath.get(rel).push({
|
|
104
|
+
candidateIndex: cand.index,
|
|
105
|
+
label: cand.label,
|
|
106
|
+
content: f.content,
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const mergedFiles = []
|
|
112
|
+
for (const [relativePath, versions] of byPath.entries()) {
|
|
113
|
+
if (versions.length === 1) {
|
|
114
|
+
mergedFiles.push({
|
|
115
|
+
relativePath,
|
|
116
|
+
content: versions[0].content,
|
|
117
|
+
contributors: [versions[0].label || `Candidate ${versions[0].candidateIndex}`],
|
|
118
|
+
usedContextLens: Boolean(lens),
|
|
119
|
+
})
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// If dsh-context-lens provides a native mergeAst or analyzeAst method, use it first
|
|
124
|
+
if (lens && typeof lens.mergeAst === 'function') {
|
|
125
|
+
try {
|
|
126
|
+
const mergedCode = await lens.mergeAst({
|
|
127
|
+
filePath: relativePath,
|
|
128
|
+
versions: versions.map((v) => v.content),
|
|
129
|
+
})
|
|
130
|
+
if (mergedCode && typeof mergedCode === 'string' && isValidJsAstSyntax(mergedCode, relativePath)) {
|
|
131
|
+
mergedFiles.push({
|
|
132
|
+
relativePath,
|
|
133
|
+
content: mergedCode,
|
|
134
|
+
contributors: versions.map((v) => v.label || `Candidate ${v.candidateIndex}`),
|
|
135
|
+
usedContextLens: true,
|
|
136
|
+
})
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
logger.warn(`[dsh-moa:ast-merge] contextLens.mergeAst failed for ${relativePath}, using declaration AST merger:`, err?.message || err)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Declaration-level AST synthesis: deduplicate imports & merge JSDoc + bodies
|
|
145
|
+
const uniqueImports = new Set()
|
|
146
|
+
const mergedDecls = new Map()
|
|
147
|
+
|
|
148
|
+
for (const ver of versions) {
|
|
149
|
+
const parsed = parseModuleDeclarations(ver.content)
|
|
150
|
+
for (const imp of parsed.imports) uniqueImports.add(imp)
|
|
151
|
+
|
|
152
|
+
for (const [symName, decl] of parsed.declarations.entries()) {
|
|
153
|
+
if (!mergedDecls.has(symName)) {
|
|
154
|
+
mergedDecls.set(symName, { ...decl })
|
|
155
|
+
} else {
|
|
156
|
+
const existing = mergedDecls.get(symName)
|
|
157
|
+
// Keep the richest JSDoc comment from any candidate
|
|
158
|
+
if (decl.jsdoc && decl.jsdoc.length > (existing.jsdoc || '').length) {
|
|
159
|
+
existing.jsdoc = decl.jsdoc
|
|
160
|
+
}
|
|
161
|
+
// Prefer more comprehensive implementation if existing is a short stub
|
|
162
|
+
if (decl.code.length > existing.code.length && existing.code.length < 120) {
|
|
163
|
+
existing.code = decl.code
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let assembled = ''
|
|
170
|
+
if (uniqueImports.size > 0) {
|
|
171
|
+
assembled += Array.from(uniqueImports).map((i) => `${i};`).join('\n') + '\n\n'
|
|
172
|
+
}
|
|
173
|
+
if (mergedDecls.size > 0) {
|
|
174
|
+
assembled += Array.from(mergedDecls.values())
|
|
175
|
+
.map((d) => (d.jsdoc ? `${d.jsdoc}\n${d.code}` : d.code))
|
|
176
|
+
.join('\n\n') + '\n'
|
|
177
|
+
} else {
|
|
178
|
+
assembled = versions[0].content
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!isValidJsAstSyntax(assembled, relativePath)) {
|
|
182
|
+
assembled = versions[0].content
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
mergedFiles.push({
|
|
186
|
+
relativePath,
|
|
187
|
+
content: assembled,
|
|
188
|
+
contributors: versions.map((v) => v.label || `Candidate ${v.candidateIndex}`),
|
|
189
|
+
usedContextLens: Boolean(lens),
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return mergedFiles
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Builds the AST-level composite merge directive for the Judge/Curator.
|
|
198
|
+
* Gracefully falls back to standard buildCompositeBlockDirectives when dsh-context-lens is absent
|
|
199
|
+
* and no multi-version AST declarations are detected.
|
|
200
|
+
*/
|
|
201
|
+
export async function buildAstCompositeMergeDirective(candidates = [], options = {}) {
|
|
202
|
+
const lens = resolveContextLensService(options.ctx, options.contextLens)
|
|
203
|
+
const mergedFiles = await mergeCandidateAstFiles(candidates, options)
|
|
204
|
+
const baseDirective = buildCompositeBlockDirectives(candidates, options)
|
|
205
|
+
|
|
206
|
+
if (!lens && mergedFiles.length === 0) {
|
|
207
|
+
return {
|
|
208
|
+
degraded: true,
|
|
209
|
+
usedContextLens: false,
|
|
210
|
+
mergedFiles: [],
|
|
211
|
+
directive: baseDirective,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const astBlocks = mergedFiles
|
|
216
|
+
.map((mf) => `#### AST Pre-Merged Preview: \`${mf.relativePath}\` (Contributors: ${mf.contributors.join(', ')})\n\`\`\`js path=${mf.relativePath}\n${mf.content.trim()}\n\`\`\``)
|
|
217
|
+
.join('\n\n')
|
|
218
|
+
|
|
219
|
+
const directive = [
|
|
220
|
+
baseDirective.trim(),
|
|
221
|
+
`\n### 🌳 AST-Level Semantic Composite Merge (${lens ? 'via `dsh-context-lens`' : 'Fallback Declaration Merger'}):`,
|
|
222
|
+
'Imports have been deduplicated and declarations (JSDoc, type signatures, and function bodies) have been aligned across candidates:',
|
|
223
|
+
astBlocks,
|
|
224
|
+
].join('\n\n')
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
degraded: !lens,
|
|
228
|
+
usedContextLens: Boolean(lens),
|
|
229
|
+
mergedFiles,
|
|
230
|
+
directive,
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
package/lib/moa-schema.js
CHANGED
|
@@ -72,6 +72,13 @@ export const PresetSchema = z.object({
|
|
|
72
72
|
self_healing_command: z.string().default("npm test"),
|
|
73
73
|
visual_moa_enabled: z.boolean().default(false),
|
|
74
74
|
elo_ranking_enabled: z.boolean().default(true),
|
|
75
|
+
ast_composite_merge_enabled: z.boolean().default(false),
|
|
76
|
+
speculative_moa_enabled: z.boolean().default(false),
|
|
77
|
+
speculative_draft_model: ModelSlotSchema.default({ provider: "", model: "" }),
|
|
78
|
+
adversarial_tdd_enabled: z.boolean().default(false),
|
|
79
|
+
local_preflight_check: z.boolean().default(true),
|
|
80
|
+
early_exit_consensus_enabled: z.boolean().default(false),
|
|
81
|
+
early_exit_threshold: z.number().default(0.95),
|
|
75
82
|
})
|
|
76
83
|
|
|
77
84
|
export const Config = z.object({
|
|
@@ -90,6 +97,13 @@ export const Config = z.object({
|
|
|
90
97
|
self_healing_command: z.string().default("npm test").volatile(),
|
|
91
98
|
visual_moa_enabled: z.boolean().default(false).volatile(),
|
|
92
99
|
elo_ranking_enabled: z.boolean().default(true).volatile(),
|
|
100
|
+
ast_composite_merge_enabled: z.boolean().default(false).volatile(),
|
|
101
|
+
speculative_moa_enabled: z.boolean().default(false).volatile(),
|
|
102
|
+
speculative_draft_model: ModelSlotSchema.default({ provider: "", model: "" }).volatile(),
|
|
103
|
+
adversarial_tdd_enabled: z.boolean().default(false).volatile(),
|
|
104
|
+
local_preflight_check: z.boolean().default(true).volatile(),
|
|
105
|
+
early_exit_consensus_enabled: z.boolean().default(false).volatile(),
|
|
106
|
+
early_exit_threshold: z.number().default(0.95).volatile(),
|
|
93
107
|
smart_routing_model: ModelSlotSchema.default({ provider: '', model: '' }).volatile(),
|
|
94
108
|
prices: z.dict(PriceRow).default({}).volatile(),
|
|
95
109
|
presets: z.array(PresetSchema).default([
|
|
@@ -151,3 +165,4 @@ export function plainConfig(value) {
|
|
|
151
165
|
}
|
|
152
166
|
return value
|
|
153
167
|
}
|
|
168
|
+
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speculative MoA with Fast-Draft Model and Multi-Auditor Diffs (#247)
|
|
3
|
+
* Generates a rapid initial draft using speculative_draft_model, dispatches the draft
|
|
4
|
+
* to reference models in parallel as diff auditors (producing targeted SEARCH/REPLACE
|
|
5
|
+
* or unified diff hunks), applies valid hunks to the draft, and gracefully falls back
|
|
6
|
+
* to standard parallel MoA if the draft model fails.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { logger } from './logger.js'
|
|
10
|
+
import { slotLabel } from './moa-prompts.js'
|
|
11
|
+
import { estimateTokenCost } from './pricing.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Applies SEARCH/REPLACE or unified diff patches onto a base draft text.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} draftText
|
|
17
|
+
* @param {string} patchText
|
|
18
|
+
* @returns {{ patchedText: string, appliedCount: number }}
|
|
19
|
+
*/
|
|
20
|
+
export function applyAuditorPatchHunks(draftText = '', patchText = '') {
|
|
21
|
+
let result = String(draftText || '')
|
|
22
|
+
let appliedCount = 0
|
|
23
|
+
|
|
24
|
+
// 1. Support SEARCH / REPLACE blocks:
|
|
25
|
+
// <<<<<<< SEARCH
|
|
26
|
+
// ...
|
|
27
|
+
// =======
|
|
28
|
+
// ...
|
|
29
|
+
// >>>>>>> REPLACE
|
|
30
|
+
const searchReplaceRegex = /<{5,9}\s*SEARCH\r?\n([\s\S]*?)\r?\n={5,9}\r?\n([\s\S]*?)\r?\n>{5,9}\s*REPLACE/g
|
|
31
|
+
let srMatch
|
|
32
|
+
while ((srMatch = searchReplaceRegex.exec(patchText)) !== null) {
|
|
33
|
+
const searchBlock = srMatch[1]
|
|
34
|
+
const replaceBlock = srMatch[2]
|
|
35
|
+
if (searchBlock && result.includes(searchBlock)) {
|
|
36
|
+
result = result.replace(searchBlock, replaceBlock)
|
|
37
|
+
appliedCount++
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. Support unified diff hunks (- oldLine / + newLine)
|
|
42
|
+
if (appliedCount === 0 && /```diff[\r\n]+([\s\S]*?)```/i.test(patchText)) {
|
|
43
|
+
const diffMatch = /```diff[\r\n]+([\s\S]*?)```/i.exec(patchText)
|
|
44
|
+
const diffLines = diffMatch[1].split(/\r?\n/)
|
|
45
|
+
const removed = []
|
|
46
|
+
const added = []
|
|
47
|
+
for (const line of diffLines) {
|
|
48
|
+
if (line.startsWith('---') || line.startsWith('+++') || line.startsWith('@@')) continue
|
|
49
|
+
if (line.startsWith('-')) removed.push(line.slice(1))
|
|
50
|
+
else if (line.startsWith('+')) added.push(line.slice(1))
|
|
51
|
+
}
|
|
52
|
+
if (removed.length > 0) {
|
|
53
|
+
const targetChunk = removed.join('\n')
|
|
54
|
+
const replacementChunk = added.join('\n')
|
|
55
|
+
if (result.includes(targetChunk)) {
|
|
56
|
+
result = result.replace(targetChunk, replacementChunk)
|
|
57
|
+
appliedCount++
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { patchedText: result, appliedCount }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Executes Speculative MoA:
|
|
67
|
+
* 1. Calls fast draft model (`speculative_draft_model` or first reference slot).
|
|
68
|
+
* 2. Dispatches draft to reference models in parallel as diff auditors.
|
|
69
|
+
* 3. Applies valid diff hunks onto draft so each auditor produces a refined candidate output.
|
|
70
|
+
* 4. If the draft model fails, returns `{ fallbackToStandard: true }` so the caller runs standard parallel MoA.
|
|
71
|
+
*/
|
|
72
|
+
export async function runSpeculativeMoa(references = [], messages = [], options = {}, callLlm, onProgress) {
|
|
73
|
+
const draftSlot = (options.draftModel?.provider && options.draftModel?.model)
|
|
74
|
+
? options.draftModel
|
|
75
|
+
: references[0]
|
|
76
|
+
|
|
77
|
+
if (!draftSlot || !draftSlot.provider || !draftSlot.model) {
|
|
78
|
+
return { fallbackToStandard: true, reason: 'No valid speculative draft model configured' }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const draftLabel = slotLabel(draftSlot)
|
|
82
|
+
if (typeof onProgress === 'function') {
|
|
83
|
+
onProgress(`⚡ *Speculative MoA: Generating fast initial draft with \`${draftLabel}\`...*\n`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const draftStart = Date.now()
|
|
87
|
+
let draftText = ''
|
|
88
|
+
let draftUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const draftRes = await callLlm({
|
|
92
|
+
provider: draftSlot.provider,
|
|
93
|
+
model: draftSlot.model,
|
|
94
|
+
messages,
|
|
95
|
+
systemPrompt: options.systemPrompt,
|
|
96
|
+
temperature: options.temperature ?? 0.3,
|
|
97
|
+
maxTokens: options.maxTokens ?? 4096,
|
|
98
|
+
signal: options.signal,
|
|
99
|
+
})
|
|
100
|
+
draftText = typeof draftRes === 'string' ? draftRes : (draftRes?.content || draftRes?.text || '')
|
|
101
|
+
if (!draftText || !draftText.trim()) {
|
|
102
|
+
throw new Error('Empty draft response from speculative draft model')
|
|
103
|
+
}
|
|
104
|
+
const rawUsage = (typeof draftRes === 'object' && draftRes?.usage) ? draftRes.usage : {
|
|
105
|
+
inputTokens: 100,
|
|
106
|
+
outputTokens: Math.max(1, Math.round(draftText.length / 4)),
|
|
107
|
+
}
|
|
108
|
+
draftUsage = estimateTokenCost(draftSlot, rawUsage, options.prices || {})
|
|
109
|
+
} catch (err) {
|
|
110
|
+
logger.warn(`[dsh-moa:speculative] Draft model ${draftLabel} failed (${err?.message || err}); falling back to standard parallel MoA.`)
|
|
111
|
+
if (typeof onProgress === 'function') {
|
|
112
|
+
onProgress(`⚠️ *Speculative draft model \`${draftLabel}\` failed; falling back to standard parallel MoA...*\n`)
|
|
113
|
+
}
|
|
114
|
+
return { fallbackToStandard: true, reason: err?.message || String(err) }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const draftDurationMs = Date.now() - draftStart
|
|
118
|
+
if (typeof onProgress === 'function') {
|
|
119
|
+
onProgress(`🔍 *Speculative MoA: Dispatching draft to ${references.length} parallel diff auditor(s)...*\n`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const lastUserPrompt = messages[messages.length - 1]?.content || ''
|
|
123
|
+
const auditTasks = references.map(async (slot, idx) => {
|
|
124
|
+
const candidateIndex = idx + 1
|
|
125
|
+
const label = slotLabel(slot)
|
|
126
|
+
const auditStart = Date.now()
|
|
127
|
+
|
|
128
|
+
const auditPrompt = [
|
|
129
|
+
`You are Speculative Auditor #${candidateIndex} (${label}).`,
|
|
130
|
+
`Review the fast draft below for the user task and provide targeted diff fixes (logic bugs, edge cases, types, security) using SEARCH/REPLACE blocks or \`\`\`diff hunks.`,
|
|
131
|
+
`\n### Original User Task:\n${lastUserPrompt}`,
|
|
132
|
+
`\n### Fast Draft Solution (${draftLabel}):\n${draftText}`,
|
|
133
|
+
`\nOutput your audit notes and any SEARCH/REPLACE or \`\`\`diff blocks needed to perfect the draft.`,
|
|
134
|
+
].join('\n')
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const auditRes = await callLlm({
|
|
138
|
+
provider: slot.provider,
|
|
139
|
+
model: slot.model,
|
|
140
|
+
messages: [{ role: 'user', content: auditPrompt }],
|
|
141
|
+
temperature: 0.2,
|
|
142
|
+
maxTokens: Math.min(options.maxTokens ?? 2048, 2048),
|
|
143
|
+
signal: options.signal,
|
|
144
|
+
})
|
|
145
|
+
const auditText = typeof auditRes === 'string' ? auditRes : (auditRes?.content || auditRes?.text || '')
|
|
146
|
+
const rawAuditUsage = (typeof auditRes === 'object' && auditRes?.usage) ? auditRes.usage : {
|
|
147
|
+
inputTokens: Math.max(1, Math.round(auditPrompt.length / 4)),
|
|
148
|
+
outputTokens: Math.max(1, Math.round(auditText.length / 4)),
|
|
149
|
+
}
|
|
150
|
+
const auditCost = estimateTokenCost(slot, rawAuditUsage, options.prices || {})
|
|
151
|
+
|
|
152
|
+
const { patchedText, appliedCount } = applyAuditorPatchHunks(draftText, auditText)
|
|
153
|
+
const combinedText = appliedCount > 0
|
|
154
|
+
? `${patchedText}\n\n#### Speculative Auditor Diff Notes (${label}, ${appliedCount} hunk(s) applied):\n${auditText}`
|
|
155
|
+
: `${draftText}\n\n#### Speculative Auditor Review (${label}):\n${auditText}`
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
index: candidateIndex,
|
|
159
|
+
slot,
|
|
160
|
+
label,
|
|
161
|
+
text: combinedText,
|
|
162
|
+
speculativeAppliedHunks: appliedCount,
|
|
163
|
+
speculativeDraftModel: draftLabel,
|
|
164
|
+
usage: {
|
|
165
|
+
inputTokens: auditCost.inputTokens + (idx === 0 ? draftUsage.inputTokens : 0),
|
|
166
|
+
outputTokens: auditCost.outputTokens + (idx === 0 ? draftUsage.outputTokens : 0),
|
|
167
|
+
totalTokens: auditCost.totalTokens + (idx === 0 ? draftUsage.totalTokens : 0),
|
|
168
|
+
},
|
|
169
|
+
costUsd: Number((auditCost.costUsd + (idx === 0 ? draftUsage.costUsd : 0)).toFixed(6)),
|
|
170
|
+
durationMs: (Date.now() - auditStart) + (idx === 0 ? draftDurationMs : 0),
|
|
171
|
+
ok: true,
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
return {
|
|
175
|
+
index: candidateIndex,
|
|
176
|
+
slot,
|
|
177
|
+
label,
|
|
178
|
+
text: draftText,
|
|
179
|
+
error: err?.message || String(err),
|
|
180
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
181
|
+
costUsd: 0,
|
|
182
|
+
durationMs: Date.now() - auditStart,
|
|
183
|
+
ok: false,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const referenceOutputs = await Promise.all(auditTasks)
|
|
189
|
+
return {
|
|
190
|
+
fallbackToStandard: false,
|
|
191
|
+
draftModel: draftLabel,
|
|
192
|
+
draftText,
|
|
193
|
+
referenceOutputs,
|
|
194
|
+
}
|
|
195
|
+
}
|
package/lib/moa-swarm.js
CHANGED
|
@@ -114,17 +114,23 @@ export async function cleanupSwarmWorktrees(cwd, runId, worktrees = []) {
|
|
|
114
114
|
if (wt.isGitWorktree) {
|
|
115
115
|
try {
|
|
116
116
|
await execFileAsync('git', ['worktree', 'remove', '--force', wt.worktreePath], { cwd, timeout: 8000 })
|
|
117
|
-
} catch {
|
|
117
|
+
} catch (err) {
|
|
118
|
+
logger.debug('[dsh-moa:swarm] cleanup worktree remove error:', err?.message || err)
|
|
119
|
+
}
|
|
118
120
|
}
|
|
119
121
|
try {
|
|
120
122
|
await fs.rm(wt.worktreePath, { recursive: true, force: true })
|
|
121
|
-
} catch {
|
|
123
|
+
} catch (err) {
|
|
124
|
+
logger.debug('[dsh-moa:swarm] cleanup rm error:', err?.message || err)
|
|
125
|
+
}
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
if (await isGitRepository(cwd)) {
|
|
125
129
|
try {
|
|
126
130
|
await execFileAsync('git', ['worktree', 'prune'], { cwd, timeout: 5000 })
|
|
127
|
-
} catch {
|
|
131
|
+
} catch (err) {
|
|
132
|
+
logger.debug('[dsh-moa:swarm] cleanup prune error:', err?.message || err)
|
|
133
|
+
}
|
|
128
134
|
}
|
|
129
135
|
|
|
130
136
|
const safeRunId = String(runId || '').replace(/[^a-zA-Z0-9_-]/g, '')
|
|
@@ -132,7 +138,9 @@ export async function cleanupSwarmWorktrees(cwd, runId, worktrees = []) {
|
|
|
132
138
|
const swarmRoot = path.join(cwd, '.moa', `swarm-${safeRunId}`)
|
|
133
139
|
try {
|
|
134
140
|
await fs.rm(swarmRoot, { recursive: true, force: true })
|
|
135
|
-
} catch {
|
|
141
|
+
} catch (err) {
|
|
142
|
+
logger.debug('[dsh-moa:swarm] cleanup swarmRoot error:', err?.message || err)
|
|
143
|
+
}
|
|
136
144
|
}
|
|
137
145
|
}
|
|
138
146
|
|
|
@@ -217,3 +225,4 @@ export async function runSubagentSwarm(references, messages, options = {}, callL
|
|
|
217
225
|
await cleanupSwarmWorktrees(cwd, runId, worktrees)
|
|
218
226
|
}
|
|
219
227
|
}
|
|
228
|
+
|
package/lib/moa-workbench.js
CHANGED
|
@@ -51,6 +51,12 @@ export const COMPANION_PLUGINS = [
|
|
|
51
51
|
url: 'https://github.com/GooDAnDReaDY/dsh-live-canvas',
|
|
52
52
|
description: 'Isolated live HTML/CSS/SVG preview environment and screenshot capture',
|
|
53
53
|
},
|
|
54
|
+
{
|
|
55
|
+
id: 'dsh-context-lens',
|
|
56
|
+
name: 'DSH Context Lens',
|
|
57
|
+
url: 'https://github.com/GooDAnDReaDY/dsh-context-lens',
|
|
58
|
+
description: 'Semantic AST parsing and symbol-level composite code merging',
|
|
59
|
+
},
|
|
54
60
|
]
|
|
55
61
|
|
|
56
62
|
/**
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { runSpeculativeMoa } from '../moa-speculative.js'
|
|
2
|
+
import { runAdversarialTddLoop } from '../moa-adversarial-tdd.js'
|
|
1
3
|
import { assignAdaptivePersonas } from '../moa-adaptive-personas.js'
|
|
2
4
|
import { runSubagentSwarm } from '../moa-swarm.js'
|
|
3
5
|
import { runReferencesParallel } from '../moa-candidates.js'
|
|
@@ -37,7 +39,29 @@ export async function stageCandidateRound1({
|
|
|
37
39
|
? assignAdaptivePersonas(effectiveReferences, typeof lastMsgText === 'string' ? lastMsgText : '').references
|
|
38
40
|
: effectiveReferences
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
let referenceOutputs = null
|
|
43
|
+
if (preset?.speculative_moa_enabled && !preset?.subagent_swarm_enabled) {
|
|
44
|
+
const specRes = await runSpeculativeMoa(
|
|
45
|
+
adaptiveRefs,
|
|
46
|
+
enrichedMessages,
|
|
47
|
+
{
|
|
48
|
+
draftModel: preset.speculative_draft_model,
|
|
49
|
+
systemPrompt: candidateSystemPrompt,
|
|
50
|
+
temperature: refTemp,
|
|
51
|
+
maxTokens,
|
|
52
|
+
prices,
|
|
53
|
+
signal,
|
|
54
|
+
},
|
|
55
|
+
callLlm,
|
|
56
|
+
onProgress
|
|
57
|
+
)
|
|
58
|
+
if (!specRes.fallbackToStandard && Array.isArray(specRes.referenceOutputs)) {
|
|
59
|
+
referenceOutputs = specRes.referenceOutputs
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!referenceOutputs) {
|
|
64
|
+
referenceOutputs = preset?.subagent_swarm_enabled
|
|
41
65
|
? await runSubagentSwarm(adaptiveRefs, enrichedMessages, { temperature: refTemp, maxTokens, signal, cwd, runId }, callLlm, onProgress)
|
|
42
66
|
: await runReferencesParallel(
|
|
43
67
|
adaptiveRefs,
|
|
@@ -59,6 +83,7 @@ export async function stageCandidateRound1({
|
|
|
59
83
|
callLlm,
|
|
60
84
|
onProgress
|
|
61
85
|
)
|
|
86
|
+
}
|
|
62
87
|
|
|
63
88
|
if (signal?.aborted) {
|
|
64
89
|
await cleanMoaWorkspaces(cwd, { runId })
|
|
@@ -104,5 +129,20 @@ export async function stageCandidateRound1({
|
|
|
104
129
|
onProgress,
|
|
105
130
|
})
|
|
106
131
|
|
|
107
|
-
|
|
132
|
+
let adversarialTddResult = null
|
|
133
|
+
if (preset?.adversarial_tdd_enabled) {
|
|
134
|
+
adversarialTddResult = await runAdversarialTddLoop({
|
|
135
|
+
cwd,
|
|
136
|
+
runId,
|
|
137
|
+
userPrompt: typeof lastMsgText === "string" ? lastMsgText : "",
|
|
138
|
+
referenceOutputs,
|
|
139
|
+
preset,
|
|
140
|
+
callLlm,
|
|
141
|
+
execFn,
|
|
142
|
+
onProgress,
|
|
143
|
+
signal,
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { ok: true, referenceOutputs, successfulRefs, adversarialTddResult }
|
|
108
148
|
}
|
package/lib/pipeline/context.js
CHANGED
|
@@ -38,6 +38,9 @@ export async function stageProjectContext({
|
|
|
38
38
|
const isBlindEvaluation = Boolean(preset?.blind_evaluation)
|
|
39
39
|
const isPeerCritiqueEnabled = Boolean(preset?.peer_critique_enabled)
|
|
40
40
|
const allowCandidateOverride = Boolean(preset?.allow_candidate_override)
|
|
41
|
+
const localPreflightCheck = preset?.local_preflight_check !== false
|
|
42
|
+
const earlyExitConsensusEnabled = Boolean(preset?.early_exit_consensus_enabled)
|
|
43
|
+
const earlyExitThreshold = typeof preset?.early_exit_threshold === 'number' ? preset.early_exit_threshold : 0.95
|
|
41
44
|
|
|
42
45
|
const collectedCtx = await collectProjectContext(cwd, 16000)
|
|
43
46
|
const isRefinement = Boolean(collectedCtx?.files?.length > 0 && isRefinementTask(userPrompt, collectedCtx.files))
|
|
@@ -123,5 +126,7 @@ export async function stageProjectContext({
|
|
|
123
126
|
allowCandidateOverride, collectedCtx, isRefinement, projectContext,
|
|
124
127
|
effectiveReferences, quotaSubstitutions, priorTurnBaseline, candidateSystemPrompt,
|
|
125
128
|
needsQuestions, enrichedMessages,
|
|
129
|
+
localPreflightCheck, earlyExitConsensusEnabled, earlyExitThreshold,
|
|
126
130
|
}
|
|
127
131
|
}
|
|
132
|
+
|