@goodandready/dsh-moa 0.2.66 → 0.2.68
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 +22 -0
- package/lib/moa-adaptive-personas.js +176 -0
- package/lib/moa-candidates.js +6 -4
- package/lib/moa-parser.js +10 -1
- package/lib/moa-schema.js +9 -0
- package/lib/moa-swarm.js +219 -0
- package/lib/moa-uncertainty.js +154 -0
- package/lib/pipeline/candidates.js +11 -2
- package/lib/pipeline/synthesis.js +10 -1
- package/lib/tui-settings.js +146 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applyTuiSettings, buildTuiSection } from './tui-settings.js'
|
|
1
2
|
import { registerMoaCommands } from './commands.js'
|
|
2
3
|
import { service, getWebServerPort } from './services.js'
|
|
3
4
|
import crypto from 'node:crypto'
|
|
@@ -189,6 +190,9 @@ export function apply(ctx, config) {
|
|
|
189
190
|
// Official slash command /moa registration via dsh-commands (#275)
|
|
190
191
|
registerMoaCommands(ctx, { live, moaService })
|
|
191
192
|
|
|
193
|
+
// Official TUI settings section registration (#277)
|
|
194
|
+
applyTuiSettings(ctx, { live })
|
|
195
|
+
|
|
192
196
|
// Session turn interceptor: route turn to real aggregator model & stream MoA synthesis
|
|
193
197
|
ctx.effect(() => {
|
|
194
198
|
return () => {
|
|
@@ -391,3 +395,21 @@ export {
|
|
|
391
395
|
getActiveLlmProviders,
|
|
392
396
|
getActiveLlmModels,
|
|
393
397
|
} from './moa-model-validator.js'
|
|
398
|
+
export { applyTuiSettings, buildTuiSection } from './tui-settings.js'
|
|
399
|
+
export {
|
|
400
|
+
calculateTokenEntropy,
|
|
401
|
+
analyzeCandidateUncertainty,
|
|
402
|
+
formatUncertaintyAnnotations,
|
|
403
|
+
} from './moa-uncertainty.js'
|
|
404
|
+
export {
|
|
405
|
+
classifyPromptDomain,
|
|
406
|
+
assignAdaptivePersonas,
|
|
407
|
+
DOMAIN_PERSONA_POOLS,
|
|
408
|
+
} from './moa-adaptive-personas.js'
|
|
409
|
+
export {
|
|
410
|
+
isGitRepository,
|
|
411
|
+
createSwarmWorktree,
|
|
412
|
+
collectSwarmWorktreeDiff,
|
|
413
|
+
cleanupSwarmWorktrees,
|
|
414
|
+
runSubagentSwarm,
|
|
415
|
+
} from './moa-swarm.js'
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt-Adaptive Dynamic Persona Synthesis for Reference Models (#254)
|
|
3
|
+
* Classifies prompt technical domain and synthesizes complementary engineering personas
|
|
4
|
+
* dynamically so each candidate approaches the task from a distinct expert angle.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const DOMAIN_PERSONA_POOLS = {
|
|
8
|
+
database: [
|
|
9
|
+
{
|
|
10
|
+
id: 'dba_index_specialist',
|
|
11
|
+
title: 'DBA Index Specialist',
|
|
12
|
+
prompt: 'You are a Principal DBA Index Specialist. Focus on query execution plans, composite indexing, lock contention prevention, and optimal read/write throughput.',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 'zero_downtime_migration_guard',
|
|
16
|
+
title: 'Zero-Downtime Migration Guard',
|
|
17
|
+
prompt: 'You are a Zero-Downtime Migration Guard. Ensure backward-compatible schema evolutions, safe expand-and-contract patterns, and rollback safety.',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'orm_query_architect',
|
|
21
|
+
title: 'ORM & Query Architect',
|
|
22
|
+
prompt: 'You are an ORM & Query Architect. Focus on eliminating N+1 queries, clean repository abstractions, transaction isolation levels, and strict data integrity.',
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
frontend: [
|
|
26
|
+
{
|
|
27
|
+
id: 'a11y_ux_specialist',
|
|
28
|
+
title: 'Accessibility & UX Specialist',
|
|
29
|
+
prompt: 'You are an Accessibility & UX Specialist. Ensure WCAG 2.1 AA compliance, keyboard navigation, ARIA semantics, and responsive user feedback.',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'css_design_systems_architect',
|
|
33
|
+
title: 'CSS & Design Systems Architect',
|
|
34
|
+
prompt: 'You are a CSS & Design Systems Architect. Focus on design tokens, layout resilience, fluid typography, zero layout shift, and clean component composition.',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'state_rendering_optimizer',
|
|
38
|
+
title: 'State & Rendering Optimizer',
|
|
39
|
+
prompt: 'You are a Frontend State & Rendering Optimizer. Focus on minimizing unnecessary re-renders, memoization, deterministic state transitions, and bundle efficiency.',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
security: [
|
|
43
|
+
{
|
|
44
|
+
id: 'offensive_security_auditor',
|
|
45
|
+
title: 'Offensive Security Auditor',
|
|
46
|
+
prompt: 'You are an Offensive Security Auditor. Proactively hunt for injection vectors, SSRF, path traversal, auth bypass, and privilege escalation.',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'defensive_hardening_specialist',
|
|
50
|
+
title: 'Defensive Hardening Specialist',
|
|
51
|
+
prompt: 'You are a Defensive Hardening Specialist. Enforce least-privilege access, strict input validation, fail-closed guards, and cryptographic best practices.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'runtime_sandbox_guardian',
|
|
55
|
+
title: 'Runtime & Sandbox Guardian',
|
|
56
|
+
prompt: 'You are a Runtime & Sandbox Guardian. Focus on resource limits, rate limiting, secret sanitization, and zero-trust boundary enforcement.',
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
performance: [
|
|
60
|
+
{
|
|
61
|
+
id: 'algorithmic_optimizer',
|
|
62
|
+
title: 'Algorithmic Complexity Optimizer',
|
|
63
|
+
prompt: 'You are an Algorithmic Complexity Optimizer. Replace O(N^2) bottlenecks with optimal data structures, streaming pipelines, and bounded memory usage.',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'concurrency_profiler',
|
|
67
|
+
title: 'Memory & Concurrency Profiler',
|
|
68
|
+
prompt: 'You are a Memory & Concurrency Profiler. Prevent event-loop blocking, race conditions, memory leaks, and unbounded promise queues.',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
testing: [
|
|
72
|
+
{
|
|
73
|
+
id: 'edge_case_fuzzer',
|
|
74
|
+
title: 'Edge-Case & Fuzzing Specialist',
|
|
75
|
+
prompt: 'You are an Edge-Case & Fuzzing Specialist. Target boundary conditions, malformed inputs, concurrency races, and failure recovery paths.',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'contract_test_architect',
|
|
79
|
+
title: 'Integration & Contract Test Architect',
|
|
80
|
+
prompt: 'You are an Integration & Contract Test Architect. Build deterministic, isolated, fast-running test suites with zero flakiness.',
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
architecture: [
|
|
84
|
+
{
|
|
85
|
+
id: 'ddd_architect',
|
|
86
|
+
title: 'Domain-Driven Design Architect',
|
|
87
|
+
prompt: 'You are a Domain-Driven Design Architect. Enforce clean module boundaries, single responsibility, explicit contracts, and low coupling.',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'resilience_engineer',
|
|
91
|
+
title: 'Resilience & Fault-Tolerance Engineer',
|
|
92
|
+
prompt: 'You are a Resilience & Fault-Tolerance Engineer. Design idempotent operations, graceful degradation, circuit breakers, and clear error taxonomy.',
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const DOMAIN_RULES = [
|
|
98
|
+
{ domain: 'database', pattern: /\b(sql|postgres|mysql|sqlite|prisma|drizzle|typeorm|migration|index|schema|table|query|database|db|transaction)\b/i },
|
|
99
|
+
{ domain: 'security', pattern: /\b(security|auth|jwt|oauth|csrf|xss|ssrf|injection|traversal|permission|encrypt|hash|token|secret|vulnerability|cve)\b/i },
|
|
100
|
+
{ domain: 'frontend', pattern: /\b(react|vue|svelte|css|tailwind|html|dom|component|ui|ux|button|modal|layout|responsive|aria|accessibility|style)\b/i },
|
|
101
|
+
{ domain: 'performance', pattern: /\b(performance|optimize|latency|throughput|cache|memory|leak|benchmark|bottleneck|fast|profil|concurrent|parallel)\b/i },
|
|
102
|
+
{ domain: 'testing', pattern: /\b(test|unit|integration|e2e|mock|assert|coverage|vitest|jest|playwright|tdd|regression)\b/i },
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Classifies a user prompt into a specialized technical domain.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} prompt
|
|
109
|
+
* @returns {{ domain: string, confidence: number, matchedKeywords: string[] }}
|
|
110
|
+
*/
|
|
111
|
+
export function classifyPromptDomain(prompt) {
|
|
112
|
+
const text = String(prompt || '').trim()
|
|
113
|
+
if (!text) return { domain: 'architecture', confidence: 0.5, matchedKeywords: [] }
|
|
114
|
+
|
|
115
|
+
let bestDomain = 'architecture'
|
|
116
|
+
let bestCount = 0
|
|
117
|
+
let bestMatches = []
|
|
118
|
+
|
|
119
|
+
for (const rule of DOMAIN_RULES) {
|
|
120
|
+
const matches = [...text.matchAll(new RegExp(rule.pattern.source, 'gi'))].map((m) => m[0].toLowerCase())
|
|
121
|
+
const unique = [...new Set(matches)]
|
|
122
|
+
if (unique.length > bestCount) {
|
|
123
|
+
bestCount = unique.length
|
|
124
|
+
bestDomain = rule.domain
|
|
125
|
+
bestMatches = unique
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
domain: bestDomain,
|
|
131
|
+
confidence: bestCount > 0 ? Math.min(0.98, Number((0.6 + bestCount * 0.12).toFixed(2))) : 0.55,
|
|
132
|
+
matchedKeywords: bestMatches,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Synthesizes and assigns complementary domain-specific personas to reference model slots.
|
|
138
|
+
*
|
|
139
|
+
* @param {Array<object>} references
|
|
140
|
+
* @param {string} prompt
|
|
141
|
+
* @returns {{ domain: string, references: Array<object>, personasAssigned: Array<object> }}
|
|
142
|
+
*/
|
|
143
|
+
export function assignAdaptivePersonas(references, prompt) {
|
|
144
|
+
if (!Array.isArray(references) || references.length === 0) {
|
|
145
|
+
return { domain: 'architecture', references: [], personasAssigned: [] }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const { domain } = classifyPromptDomain(prompt)
|
|
149
|
+
const pool = DOMAIN_PERSONA_POOLS[domain] || DOMAIN_PERSONA_POOLS.architecture
|
|
150
|
+
const fallbackPool = DOMAIN_PERSONA_POOLS.architecture
|
|
151
|
+
|
|
152
|
+
const personasAssigned = []
|
|
153
|
+
const enrichedRefs = references.map((slot, idx) => {
|
|
154
|
+
const chosen = pool[idx % pool.length] || fallbackPool[idx % fallbackPool.length]
|
|
155
|
+
personasAssigned.push({
|
|
156
|
+
candidateIndex: idx + 1,
|
|
157
|
+
model: `${slot.provider || ''}:${slot.model || ''}`,
|
|
158
|
+
domain,
|
|
159
|
+
personaId: chosen.id,
|
|
160
|
+
personaTitle: chosen.title,
|
|
161
|
+
})
|
|
162
|
+
return {
|
|
163
|
+
...slot,
|
|
164
|
+
adaptive_domain: domain,
|
|
165
|
+
adaptive_persona_id: chosen.id,
|
|
166
|
+
adaptive_persona_title: chosen.title,
|
|
167
|
+
adaptive_persona_prompt: `[ADAPTIVE EXPERT ROLE — ${chosen.title} (${domain.toUpperCase()})]: ${chosen.prompt}`,
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
domain,
|
|
173
|
+
references: enrichedRefs,
|
|
174
|
+
personasAssigned,
|
|
175
|
+
}
|
|
176
|
+
}
|
package/lib/moa-candidates.js
CHANGED
|
@@ -85,9 +85,9 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
85
85
|
references.forEach((slot, i) => {
|
|
86
86
|
const label = slotLabel(slot)
|
|
87
87
|
const role = slot?.role_persona || slot?.role || 'general'
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
const basePersona = (role && ROLE_PERSONA_PROMPTS[role]) ? `\n\n${ROLE_PERSONA_PROMPTS[role]}` : ''
|
|
89
|
+
const adaptiveExtra = slot?.adaptive_persona_prompt ? `\n\n${slot.adaptive_persona_prompt}` : ''
|
|
90
|
+
const persona = basePersona + adaptiveExtra
|
|
91
91
|
const slotMessages = [{ role: 'system', content: systemPrompt + persona }, ...advisoryMessages]
|
|
92
92
|
|
|
93
93
|
// Feature 7: Per-candidate temperature or temperature gradient
|
|
@@ -140,13 +140,15 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
140
140
|
index: i + 1,
|
|
141
141
|
slot,
|
|
142
142
|
label,
|
|
143
|
-
role_persona: role,
|
|
143
|
+
role_persona: slot?.adaptive_persona_title || role,
|
|
144
|
+
...(slot?.adaptive_domain ? { adaptive_domain: slot.adaptive_domain } : {}),
|
|
144
145
|
temperature: effectiveTemperature,
|
|
145
146
|
text,
|
|
146
147
|
usage: costInfo,
|
|
147
148
|
costUsd: costInfo.costUsd,
|
|
148
149
|
durationMs: Date.now() - candidateStart,
|
|
149
150
|
model: slot?.model ? (slot.provider ? (slot.provider + ":" + slot.model) : slot.model) : label,
|
|
151
|
+
...(res?.logprobs ? { logprobs: res.logprobs } : {}),
|
|
150
152
|
ok: true,
|
|
151
153
|
}
|
|
152
154
|
|
package/lib/moa-parser.js
CHANGED
|
@@ -56,12 +56,18 @@ export function parseMoACommand(text, presets = [], defaultPreset = undefined) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
let remainder = text.slice(4).trim()
|
|
60
|
+
let isSwarm = false
|
|
61
|
+
if (/^--swarm(?:\s+|$)/i.test(remainder)) {
|
|
62
|
+
isSwarm = true
|
|
63
|
+
remainder = remainder.replace(/^--swarm\s*/i, '').trim()
|
|
64
|
+
}
|
|
60
65
|
if (!remainder) {
|
|
61
66
|
return {
|
|
62
67
|
presetName: defaultPreset,
|
|
63
68
|
prompt: '',
|
|
64
69
|
isExplicit: false,
|
|
70
|
+
...(isSwarm ? { isSwarm: true } : {}),
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
73
|
|
|
@@ -71,6 +77,7 @@ export function parseMoACommand(text, presets = [], defaultPreset = undefined) {
|
|
|
71
77
|
presetName: matchPreset[1],
|
|
72
78
|
prompt: matchPreset[2] || '',
|
|
73
79
|
isExplicit: true,
|
|
80
|
+
...(isSwarm ? { isSwarm: true } : {}),
|
|
74
81
|
}
|
|
75
82
|
}
|
|
76
83
|
|
|
@@ -80,6 +87,7 @@ export function parseMoACommand(text, presets = [], defaultPreset = undefined) {
|
|
|
80
87
|
presetName: matchShortPreset[1],
|
|
81
88
|
prompt: matchShortPreset[2] || '',
|
|
82
89
|
isExplicit: true,
|
|
90
|
+
...(isSwarm ? { isSwarm: true } : {}),
|
|
83
91
|
}
|
|
84
92
|
}
|
|
85
93
|
|
|
@@ -100,6 +108,7 @@ export function parseMoACommand(text, presets = [], defaultPreset = undefined) {
|
|
|
100
108
|
presetName: defaultPreset,
|
|
101
109
|
prompt: remainder,
|
|
102
110
|
isExplicit: false,
|
|
111
|
+
...(isSwarm ? { isSwarm: true } : {}),
|
|
103
112
|
}
|
|
104
113
|
}
|
|
105
114
|
|
package/lib/moa-schema.js
CHANGED
|
@@ -63,12 +63,21 @@ export const PresetSchema = z.object({
|
|
|
63
63
|
local_fallback_enabled: z.boolean().default(false),
|
|
64
64
|
local_fallback_models: z.array(ModelSlotSchema).default([]),
|
|
65
65
|
interactive_workbench_enabled: z.boolean().default(false),
|
|
66
|
+
uncertainty_scoring_enabled: z.boolean().default(false),
|
|
67
|
+
subagent_swarm_enabled: z.boolean().default(false),
|
|
68
|
+
adaptive_personas_enabled: z.boolean().default(false),
|
|
66
69
|
})
|
|
67
70
|
|
|
68
71
|
export const Config = z.object({
|
|
69
72
|
enabled: z.boolean().default(true).volatile(),
|
|
70
73
|
default_preset: z.string().default('default').volatile(),
|
|
71
74
|
smart_routing_enabled: z.boolean().default(false).volatile(),
|
|
75
|
+
interactive_workbench_enabled: z.boolean().default(false).volatile(),
|
|
76
|
+
ask_clarifying_questions: z.boolean().default(true).volatile(),
|
|
77
|
+
test_gate_enabled: z.boolean().default(false).volatile(),
|
|
78
|
+
uncertainty_scoring_enabled: z.boolean().default(false).volatile(),
|
|
79
|
+
subagent_swarm_enabled: z.boolean().default(false).volatile(),
|
|
80
|
+
adaptive_personas_enabled: z.boolean().default(false).volatile(),
|
|
72
81
|
smart_routing_model: ModelSlotSchema.default({ provider: '', model: '' }).volatile(),
|
|
73
82
|
prices: z.dict(PriceRow).default({}).volatile(),
|
|
74
83
|
presets: z.array(PresetSchema).default([
|
package/lib/moa-swarm.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MoA-to-Subagents Swarm with Isolated Git Worktrees (#255)
|
|
3
|
+
* Provisions isolated git worktrees (or workspace sandboxes) per candidate,
|
|
4
|
+
* dispatches autonomous subagent/candidate execution, collects git diffs,
|
|
5
|
+
* and guarantees worktree cleanup in finally blocks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs/promises'
|
|
9
|
+
import { existsSync } from 'node:fs'
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
import { execFile } from 'node:child_process'
|
|
12
|
+
import { promisify } from 'node:util'
|
|
13
|
+
import { logger } from './logger.js'
|
|
14
|
+
import { slotLabel } from './moa-prompts.js'
|
|
15
|
+
import { extractFileBlocks } from './workspace/fs.js'
|
|
16
|
+
|
|
17
|
+
const execFileAsync = promisify(execFile)
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Checks if a directory is inside a valid git repository.
|
|
21
|
+
*/
|
|
22
|
+
export async function isGitRepository(cwd) {
|
|
23
|
+
try {
|
|
24
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { cwd, timeout: 5000 })
|
|
25
|
+
return stdout.trim() === 'true'
|
|
26
|
+
} catch {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates an isolated git worktree (or directory sandbox if not a git repo) for a swarm candidate.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} cwd
|
|
35
|
+
* @param {string} runId
|
|
36
|
+
* @param {number} candidateIndex
|
|
37
|
+
* @returns {Promise<{ worktreePath: string, branchName: string, isGitWorktree: boolean }>}
|
|
38
|
+
*/
|
|
39
|
+
export async function createSwarmWorktree(cwd, runId, candidateIndex) {
|
|
40
|
+
const safeRunId = String(runId || Date.now()).replace(/[^a-zA-Z0-9_-]/g, '')
|
|
41
|
+
const worktreePath = path.join(cwd, '.moa', `swarm-${safeRunId}`, `candidate-${candidateIndex}`)
|
|
42
|
+
const branchName = `moa-swarm/${safeRunId}/cand-${candidateIndex}`
|
|
43
|
+
|
|
44
|
+
await fs.mkdir(path.dirname(worktreePath), { recursive: true })
|
|
45
|
+
|
|
46
|
+
if (await isGitRepository(cwd)) {
|
|
47
|
+
try {
|
|
48
|
+
await execFileAsync('git', ['worktree', 'add', '--detach', worktreePath, 'HEAD'], { cwd, timeout: 10000 })
|
|
49
|
+
return { worktreePath, branchName, isGitWorktree: true }
|
|
50
|
+
} catch (err) {
|
|
51
|
+
logger.warn(`[dsh-moa] git worktree add failed for candidate-${candidateIndex}, using isolated directory sandbox: ${err?.message || err}`)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await fs.mkdir(worktreePath, { recursive: true })
|
|
56
|
+
return { worktreePath, branchName, isGitWorktree: false }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Collects git diff and modified files from a candidate's swarm worktree.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} worktreePath
|
|
63
|
+
* @param {boolean} isGitWorktree
|
|
64
|
+
* @returns {Promise<{ diffText: string, modifiedFiles: string[] }>}
|
|
65
|
+
*/
|
|
66
|
+
export async function collectSwarmWorktreeDiff(worktreePath, isGitWorktree = false) {
|
|
67
|
+
if (!existsSync(worktreePath)) {
|
|
68
|
+
return { diffText: '', modifiedFiles: [] }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (isGitWorktree) {
|
|
72
|
+
try {
|
|
73
|
+
const { stdout: statusOut } = await execFileAsync('git', ['status', '--porcelain'], { cwd: worktreePath, timeout: 5000 })
|
|
74
|
+
const modifiedFiles = statusOut
|
|
75
|
+
.split('\n')
|
|
76
|
+
.map((line) => line.slice(3).trim())
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
|
|
79
|
+
const { stdout: diffOut } = await execFileAsync('git', ['diff', 'HEAD'], { cwd: worktreePath, timeout: 5000 })
|
|
80
|
+
return { diffText: diffOut.trim(), modifiedFiles }
|
|
81
|
+
} catch {
|
|
82
|
+
// Fallback to directory scan
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const modifiedFiles = []
|
|
87
|
+
async function scanDir(dir, prefix = '') {
|
|
88
|
+
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => [])
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
if (entry.name === '.git') continue
|
|
91
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name
|
|
92
|
+
const full = path.join(dir, entry.name)
|
|
93
|
+
if (entry.isDirectory()) {
|
|
94
|
+
await scanDir(full, rel)
|
|
95
|
+
} else {
|
|
96
|
+
modifiedFiles.push(rel)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
await scanDir(worktreePath)
|
|
101
|
+
return { diffText: '', modifiedFiles }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Guarantees removal of all swarm worktrees and temporary directories.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} cwd
|
|
108
|
+
* @param {string} runId
|
|
109
|
+
* @param {Array<{ worktreePath: string, isGitWorktree: boolean }>} worktrees
|
|
110
|
+
*/
|
|
111
|
+
export async function cleanupSwarmWorktrees(cwd, runId, worktrees = []) {
|
|
112
|
+
for (const wt of worktrees) {
|
|
113
|
+
if (!wt?.worktreePath) continue
|
|
114
|
+
if (wt.isGitWorktree) {
|
|
115
|
+
try {
|
|
116
|
+
await execFileAsync('git', ['worktree', 'remove', '--force', wt.worktreePath], { cwd, timeout: 8000 })
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
await fs.rm(wt.worktreePath, { recursive: true, force: true })
|
|
121
|
+
} catch {}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (await isGitRepository(cwd)) {
|
|
125
|
+
try {
|
|
126
|
+
await execFileAsync('git', ['worktree', 'prune'], { cwd, timeout: 5000 })
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const safeRunId = String(runId || '').replace(/[^a-zA-Z0-9_-]/g, '')
|
|
131
|
+
if (safeRunId) {
|
|
132
|
+
const swarmRoot = path.join(cwd, '.moa', `swarm-${safeRunId}`)
|
|
133
|
+
try {
|
|
134
|
+
await fs.rm(swarmRoot, { recursive: true, force: true })
|
|
135
|
+
} catch {}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Runs candidates as a swarm with isolated worktrees.
|
|
141
|
+
* Uses ctx.subagent.run if available on Cordis context, falling back to callLlm + worktree materialization.
|
|
142
|
+
*/
|
|
143
|
+
export async function runSubagentSwarm(references, messages, options = {}, callLlm, onProgress) {
|
|
144
|
+
const cwd = options.cwd || process.cwd()
|
|
145
|
+
const runId = options.runId || `run-${Date.now()}`
|
|
146
|
+
const worktrees = []
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
if (typeof onProgress === 'function') {
|
|
150
|
+
onProgress(`🐝 *Launching MoA Subagent Swarm across ${references.length} isolated worktrees...*\n`)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const tasks = references.map(async (slot, idx) => {
|
|
154
|
+
const candidateIndex = idx + 1
|
|
155
|
+
const label = slotLabel(slot)
|
|
156
|
+
const wt = await createSwarmWorktree(cwd, runId, candidateIndex)
|
|
157
|
+
worktrees.push(wt)
|
|
158
|
+
|
|
159
|
+
const start = Date.now()
|
|
160
|
+
let responseText = ''
|
|
161
|
+
let usage = { inputTokens: 100, outputTokens: 100, totalTokens: 200, costUsd: 0 }
|
|
162
|
+
|
|
163
|
+
if (options.ctx?.subagent && typeof options.ctx.subagent.run === 'function') {
|
|
164
|
+
const subRes = await options.ctx.subagent.run({
|
|
165
|
+
provider: slot.provider,
|
|
166
|
+
model: slot.model,
|
|
167
|
+
cwd: wt.worktreePath,
|
|
168
|
+
messages,
|
|
169
|
+
signal: options.signal,
|
|
170
|
+
})
|
|
171
|
+
responseText = typeof subRes === 'string' ? subRes : (subRes?.content || subRes?.text || '')
|
|
172
|
+
if (subRes?.usage) usage = subRes.usage
|
|
173
|
+
} else {
|
|
174
|
+
const llmRes = await callLlm({
|
|
175
|
+
provider: slot.provider,
|
|
176
|
+
model: slot.model,
|
|
177
|
+
messages,
|
|
178
|
+
temperature: options.temperature ?? 0.6,
|
|
179
|
+
maxTokens: options.maxTokens ?? 4096,
|
|
180
|
+
signal: options.signal,
|
|
181
|
+
})
|
|
182
|
+
responseText = typeof llmRes === 'string' ? llmRes : (llmRes?.content || llmRes?.text || '')
|
|
183
|
+
if (llmRes?.usage) usage = llmRes.usage
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Materialize any extracted file blocks inside the isolated worktree
|
|
187
|
+
const files = extractFileBlocks(responseText)
|
|
188
|
+
for (const f of files) {
|
|
189
|
+
const targetFile = path.join(wt.worktreePath, f.relativePath || f.path || "")
|
|
190
|
+
await fs.mkdir(path.dirname(targetFile), { recursive: true })
|
|
191
|
+
await fs.writeFile(targetFile, f.content, 'utf8')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const diffInfo = await collectSwarmWorktreeDiff(wt.worktreePath, wt.isGitWorktree)
|
|
195
|
+
const enrichedText = diffInfo.diffText
|
|
196
|
+
? `${responseText}\n\n#### Git Worktree Diff (${wt.branchName}):\n\`\`\`diff\n${diffInfo.diffText}\n\`\`\``
|
|
197
|
+
: responseText
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
index: candidateIndex,
|
|
201
|
+
slot,
|
|
202
|
+
label,
|
|
203
|
+
text: enrichedText,
|
|
204
|
+
files,
|
|
205
|
+
swarmWorktree: wt.worktreePath,
|
|
206
|
+
swarmBranch: wt.branchName,
|
|
207
|
+
modifiedFiles: diffInfo.modifiedFiles,
|
|
208
|
+
usage,
|
|
209
|
+
costUsd: usage.costUsd || 0,
|
|
210
|
+
durationMs: Date.now() - start,
|
|
211
|
+
ok: true,
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
return await Promise.all(tasks)
|
|
216
|
+
} finally {
|
|
217
|
+
await cleanupSwarmWorktrees(cwd, runId, worktrees)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token-Level Uncertainty & Entropy Scoring for Hallucination Detection (#256)
|
|
3
|
+
* Calculates Shannon entropy across token logprob distributions, detects high-uncertainty
|
|
4
|
+
* identifier/method call spans, and builds diagnostic annotations for the Judge.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const LN2 = Math.LN2
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Computes token entropy in bits from a token logprob entry.
|
|
11
|
+
* Supports full top_logprobs distributions as well as scalar logprob surprisal.
|
|
12
|
+
*
|
|
13
|
+
* @param {object|number} entry
|
|
14
|
+
* @returns {number} Entropy in bits (>= 0)
|
|
15
|
+
*/
|
|
16
|
+
export function calculateTokenEntropy(entry) {
|
|
17
|
+
if (typeof entry === 'number') {
|
|
18
|
+
if (!Number.isFinite(entry) || entry >= 0) return 0
|
|
19
|
+
return Number((-entry / LN2).toFixed(4))
|
|
20
|
+
}
|
|
21
|
+
if (!entry || typeof entry !== 'object') return 0
|
|
22
|
+
|
|
23
|
+
const topList = Array.isArray(entry.top_logprobs) ? entry.top_logprobs : null
|
|
24
|
+
if (topList && topList.length > 0) {
|
|
25
|
+
let entropy = 0
|
|
26
|
+
let totalProb = 0
|
|
27
|
+
for (const item of topList) {
|
|
28
|
+
const lp = typeof item === 'number' ? item : Number(item?.logprob)
|
|
29
|
+
if (!Number.isFinite(lp)) continue
|
|
30
|
+
const p = Math.min(1, Math.max(1e-9, Math.exp(lp)))
|
|
31
|
+
totalProb += p
|
|
32
|
+
entropy -= p * (lp / LN2)
|
|
33
|
+
}
|
|
34
|
+
// Account for remaining probability mass if top_logprobs is truncated
|
|
35
|
+
if (totalProb < 0.999 && totalProb > 0) {
|
|
36
|
+
const rem = 1 - totalProb
|
|
37
|
+
entropy -= rem * Math.log2(rem)
|
|
38
|
+
}
|
|
39
|
+
return Number(Math.max(0, entropy).toFixed(4))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const lp = Number(entry.logprob)
|
|
43
|
+
if (Number.isFinite(lp) && lp < 0) {
|
|
44
|
+
return Number((-lp / LN2).toFixed(4))
|
|
45
|
+
}
|
|
46
|
+
return 0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Analyzes a candidate output's token logprobs (and code heuristics) to identify
|
|
51
|
+
* high-uncertainty zones where hallucinations are likely.
|
|
52
|
+
*
|
|
53
|
+
* @param {object} candidate
|
|
54
|
+
* @param {object} [options]
|
|
55
|
+
* @returns {object}
|
|
56
|
+
*/
|
|
57
|
+
export function analyzeCandidateUncertainty(candidate, options = {}) {
|
|
58
|
+
const threshold = typeof options.entropyThreshold === 'number' ? options.entropyThreshold : 1.2
|
|
59
|
+
const rawLogprobs = Array.isArray(candidate?.logprobs)
|
|
60
|
+
? candidate.logprobs
|
|
61
|
+
: (Array.isArray(candidate?.logprobs?.content) ? candidate.logprobs.content : [])
|
|
62
|
+
|
|
63
|
+
const highEntropySpans = []
|
|
64
|
+
let sumEntropy = 0
|
|
65
|
+
let maxEntropy = 0
|
|
66
|
+
let tokenCount = 0
|
|
67
|
+
|
|
68
|
+
if (rawLogprobs.length > 0) {
|
|
69
|
+
for (let i = 0; i < rawLogprobs.length; i++) {
|
|
70
|
+
const item = rawLogprobs[i]
|
|
71
|
+
const h = calculateTokenEntropy(item)
|
|
72
|
+
const tok = String(item?.token ?? '').trim()
|
|
73
|
+
sumEntropy += h
|
|
74
|
+
if (h > maxEntropy) maxEntropy = h
|
|
75
|
+
tokenCount++
|
|
76
|
+
|
|
77
|
+
if (h >= threshold && tok.length > 0 && !/^[\s{}()\[\];,.:'"`]+$/.test(tok)) {
|
|
78
|
+
// Reconstruct local context window around token
|
|
79
|
+
const prev = rawLogprobs.slice(Math.max(0, i - 2), i).map((t) => t?.token || '').join('')
|
|
80
|
+
const next = rawLogprobs.slice(i + 1, Math.min(rawLogprobs.length, i + 3)).map((t) => t?.token || '').join('')
|
|
81
|
+
const contextSpan = (prev + (item?.token || '') + next).replace(/\s+/g, ' ').trim()
|
|
82
|
+
const prob = Number.isFinite(Number(item?.logprob))
|
|
83
|
+
? Number(Math.exp(Number(item.logprob)).toFixed(3))
|
|
84
|
+
: Number(Math.pow(2, -h).toFixed(3))
|
|
85
|
+
|
|
86
|
+
highEntropySpans.push({
|
|
87
|
+
token: tok,
|
|
88
|
+
span: contextSpan || tok,
|
|
89
|
+
entropy: h,
|
|
90
|
+
probability: prob,
|
|
91
|
+
warning: `Внимание: высокая энтропия на участке "${contextSpan || tok}" (H=${h.toFixed(2)} bits, p=${prob})`,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const avgEntropy = tokenCount > 0 ? Number((sumEntropy / tokenCount).toFixed(4)) : 0
|
|
98
|
+
let riskLevel = 'low'
|
|
99
|
+
if (maxEntropy >= threshold * 1.6 || highEntropySpans.length >= 3) {
|
|
100
|
+
riskLevel = 'high'
|
|
101
|
+
} else if (maxEntropy >= threshold || highEntropySpans.length >= 1) {
|
|
102
|
+
riskLevel = 'medium'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
candidateIndex: candidate?.index ?? 0,
|
|
107
|
+
model: candidate?.label || candidate?.model || 'unknown',
|
|
108
|
+
tokenCount,
|
|
109
|
+
avgEntropy,
|
|
110
|
+
maxEntropy: Number(maxEntropy.toFixed(4)),
|
|
111
|
+
riskLevel,
|
|
112
|
+
highEntropySpans,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Formats uncertainty analysis across all candidates into a Judge prompt directive.
|
|
118
|
+
*
|
|
119
|
+
* @param {Array<object>} candidates
|
|
120
|
+
* @param {object} [options]
|
|
121
|
+
* @returns {{ reportText: string, analyses: Array<object>, hasHighUncertainty: boolean }}
|
|
122
|
+
*/
|
|
123
|
+
export function formatUncertaintyAnnotations(candidates, options = {}) {
|
|
124
|
+
if (!Array.isArray(candidates) || candidates.length === 0) {
|
|
125
|
+
return { reportText: '', analyses: [], hasHighUncertainty: false }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const analyses = candidates.map((c, idx) =>
|
|
129
|
+
analyzeCandidateUncertainty({ ...c, index: c.index ?? idx + 1 }, options)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const flagged = analyses.filter((a) => a.highEntropySpans.length > 0)
|
|
133
|
+
if (flagged.length === 0) {
|
|
134
|
+
return { reportText: '', analyses, hasHighUncertainty: false }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const lines = [
|
|
138
|
+
'\n### 🔬 Token-Level Uncertainty & Hallucination Warnings:',
|
|
139
|
+
'The following candidate segments exhibited abnormally high token entropy (low model confidence). Carefully verify API existence, method signatures, and edge cases before adopting these fragments:',
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
for (const a of flagged) {
|
|
143
|
+
lines.push(`- **Candidate ${a.candidateIndex} (${a.model})** [Risk: ${a.riskLevel.toUpperCase()}, Avg H=${a.avgEntropy}, Max H=${a.maxEntropy}]:`)
|
|
144
|
+
for (const span of a.highEntropySpans.slice(0, 5)) {
|
|
145
|
+
lines.push(` - ⚠️ ${span.warning}`)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
reportText: lines.join('\n') + '\n',
|
|
151
|
+
analyses,
|
|
152
|
+
hasHighUncertainty: true,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { assignAdaptivePersonas } from '../moa-adaptive-personas.js'
|
|
2
|
+
import { runSubagentSwarm } from '../moa-swarm.js'
|
|
1
3
|
import { runReferencesParallel } from '../moa-candidates.js'
|
|
2
4
|
import { extractFileBlocks, writeCandidateWorkspace, cleanMoaWorkspaces } from '../workspace/fs.js'
|
|
3
5
|
import { verifyFileSyntax } from '../workspace/diff.js'
|
|
@@ -30,8 +32,15 @@ export async function stageCandidateRound1({
|
|
|
30
32
|
testCommand,
|
|
31
33
|
execFn,
|
|
32
34
|
}) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
+
const lastMsgText = enrichedMessages[enrichedMessages.length - 1]?.content || ''
|
|
36
|
+
const adaptiveRefs = preset?.adaptive_personas_enabled
|
|
37
|
+
? assignAdaptivePersonas(effectiveReferences, typeof lastMsgText === 'string' ? lastMsgText : '').references
|
|
38
|
+
: effectiveReferences
|
|
39
|
+
|
|
40
|
+
const referenceOutputs = preset?.subagent_swarm_enabled
|
|
41
|
+
? await runSubagentSwarm(adaptiveRefs, enrichedMessages, { temperature: refTemp, maxTokens, signal, cwd, runId }, callLlm, onProgress)
|
|
42
|
+
: await runReferencesParallel(
|
|
43
|
+
adaptiveRefs,
|
|
35
44
|
enrichedMessages,
|
|
36
45
|
{
|
|
37
46
|
systemPrompt: candidateSystemPrompt,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatUncertaintyAnnotations } from '../moa-uncertainty.js'
|
|
1
2
|
import { logger } from '../logger.js'
|
|
2
3
|
import { buildSynthesisPrompt, slotLabel } from '../moa-prompts.js'
|
|
3
4
|
import { estimateDownstreamCost, checkPhaseBudget, createBudgetAbortPayload } from '../moa-budget.js'
|
|
@@ -36,7 +37,14 @@ export async function stageSynthesis({
|
|
|
36
37
|
collectedCtx,
|
|
37
38
|
startTime,
|
|
38
39
|
}) {
|
|
39
|
-
|
|
40
|
+
let uncertaintyDirective = ''
|
|
41
|
+
if (preset?.uncertainty_scoring_enabled) {
|
|
42
|
+
const uncReport = formatUncertaintyAnnotations(referenceOutputs)
|
|
43
|
+
if (uncReport.hasHighUncertainty) {
|
|
44
|
+
uncertaintyDirective = uncReport.reportText
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const baseSynthesisPrompt = buildSynthesisPrompt(userPrompt, referenceOutputs, judgeCriteria, {
|
|
40
48
|
curatorSynthesis: isCuratorSynthesis,
|
|
41
49
|
blindEvaluation: isBlindEvaluation,
|
|
42
50
|
priorTurnBaseline,
|
|
@@ -44,6 +52,7 @@ export async function stageSynthesis({
|
|
|
44
52
|
fileScoringDirective,
|
|
45
53
|
consensusReport: multiJudgeResult?.consensusReport,
|
|
46
54
|
})
|
|
55
|
+
const synthesisPrompt = uncertaintyDirective ? baseSynthesisPrompt + '\n' + uncertaintyDirective : baseSynthesisPrompt
|
|
47
56
|
const synthPromptTokens = Math.max(50, Math.ceil(synthesisPrompt.length / 4))
|
|
48
57
|
|
|
49
58
|
if (preset?.budget_guard_enabled && preset?.max_budget_usd > 0 && !isFastMode) {
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH Terminal User Interface (TUI) Settings Section for dsh-moa (#277)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { service } from './services.js'
|
|
6
|
+
|
|
7
|
+
function boolFormat(value) {
|
|
8
|
+
return value === false ? 'false' : 'true'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function boolParse(text) {
|
|
12
|
+
return { kind: 'set', value: String(text).trim() === 'true' }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Builds the TUI settings section descriptor from live configuration.
|
|
17
|
+
*
|
|
18
|
+
* @param {{ live: () => Record<string, any> }} deps
|
|
19
|
+
* @returns {object}
|
|
20
|
+
*/
|
|
21
|
+
export function buildTuiSection({ live }) {
|
|
22
|
+
const cfg = (typeof live === 'function' ? live() : live) || {}
|
|
23
|
+
const presets = Array.isArray(cfg.presets) ? cfg.presets : []
|
|
24
|
+
const presetOptions = presets.length > 0
|
|
25
|
+
? presets.map((p) => ({
|
|
26
|
+
value: String(p.name || p.id || ''),
|
|
27
|
+
label: `${p.name || p.id} (${(p.reference_models || p.references || []).length} models)`,
|
|
28
|
+
})).filter((o) => o.value !== '')
|
|
29
|
+
: [{ value: 'default', label: 'default' }]
|
|
30
|
+
|
|
31
|
+
const fields = [
|
|
32
|
+
{
|
|
33
|
+
path: ['enabled'],
|
|
34
|
+
group: 'general',
|
|
35
|
+
kind: 'boolean',
|
|
36
|
+
label: 'Enable MoA (/moa slash command)',
|
|
37
|
+
hint: 'Master switch for Mixture of Agents turn routing.',
|
|
38
|
+
format: boolFormat,
|
|
39
|
+
parse: boolParse,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
path: ['default_preset'],
|
|
43
|
+
group: 'general',
|
|
44
|
+
kind: 'text',
|
|
45
|
+
label: 'Default preset',
|
|
46
|
+
hint: 'Active consensus preset used when /moa is invoked without --preset.',
|
|
47
|
+
options: presetOptions,
|
|
48
|
+
format: (value) => (typeof value === 'string' && value.trim() !== '' ? value.trim() : 'default'),
|
|
49
|
+
parse: (text) => {
|
|
50
|
+
const value = String(text).trim()
|
|
51
|
+
return value === '' ? { kind: 'set', value: 'default' } : { kind: 'set', value }
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
path: ['interactive_workbench_enabled'],
|
|
56
|
+
group: 'pipeline',
|
|
57
|
+
kind: 'boolean',
|
|
58
|
+
label: 'Interactive Workbench',
|
|
59
|
+
hint: 'Enable interactive per-file candidate assembly workbench.',
|
|
60
|
+
format: (val) => (val === true ? 'true' : 'false'),
|
|
61
|
+
parse: boolParse,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
path: ['ask_clarifying_questions'],
|
|
65
|
+
group: 'pipeline',
|
|
66
|
+
kind: 'boolean',
|
|
67
|
+
label: 'Ask clarifying questions',
|
|
68
|
+
hint: 'Prompt user for clarification on broad or ambiguous requests before running ensemble.',
|
|
69
|
+
format: boolFormat,
|
|
70
|
+
parse: boolParse,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
path: ['test_gate_enabled'],
|
|
74
|
+
group: 'pipeline',
|
|
75
|
+
kind: 'boolean',
|
|
76
|
+
label: 'Test Execution Gate',
|
|
77
|
+
hint: 'Run syntax and unit test checks against candidate solutions before judge ranking.',
|
|
78
|
+
format: (val) => (val === true ? 'true' : 'false'),
|
|
79
|
+
parse: boolParse,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
path: ['smart_routing_enabled'],
|
|
83
|
+
group: 'advanced',
|
|
84
|
+
kind: 'boolean',
|
|
85
|
+
label: 'Smart Routing',
|
|
86
|
+
hint: 'Automatically select the best preset based on prompt complexity.',
|
|
87
|
+
format: (val) => (val === true ? 'true' : 'false'),
|
|
88
|
+
parse: boolParse,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
path: ['uncertainty_scoring_enabled'],
|
|
92
|
+
group: 'advanced',
|
|
93
|
+
kind: 'boolean',
|
|
94
|
+
label: 'Token Uncertainty Scoring',
|
|
95
|
+
hint: 'Calculate token-level entropy from logprobs to highlight potential hallucinations for the judge.',
|
|
96
|
+
format: (val) => (val === true ? 'true' : 'false'),
|
|
97
|
+
parse: boolParse,
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
ns: 'dsh-moa',
|
|
103
|
+
title: 'Mixture of Agents (MoA)',
|
|
104
|
+
descriptions: {
|
|
105
|
+
en: 'Multi-model ensemble synthesis, judge consensus, and pipeline verification controls.',
|
|
106
|
+
zh: '多模型集成合成、裁判共识与流水线验证设置。',
|
|
107
|
+
},
|
|
108
|
+
groups: [
|
|
109
|
+
{ id: 'general', title: 'General', descriptions: { zh: '常规' } },
|
|
110
|
+
{ id: 'pipeline', title: 'Pipeline & Gates', descriptions: { zh: '流水线与验证' } },
|
|
111
|
+
{ id: 'advanced', title: 'Advanced', descriptions: { zh: '高级' } },
|
|
112
|
+
],
|
|
113
|
+
fields,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Registers the TUI settings section when the host provides tuiSettingsSections.
|
|
119
|
+
*
|
|
120
|
+
* @param {object} ctx
|
|
121
|
+
* @param {{ live: () => Record<string, any> }} deps
|
|
122
|
+
*/
|
|
123
|
+
export function applyTuiSettings(ctx, { live }) {
|
|
124
|
+
const registerWithService = (tuiCtx) => {
|
|
125
|
+
if (!tuiCtx) return
|
|
126
|
+
const tuiService = service(tuiCtx, 'tuiSettingsSections')
|
|
127
|
+
if (!tuiService || typeof tuiService.register !== 'function') return
|
|
128
|
+
try {
|
|
129
|
+
const dispose = tuiService.register(buildTuiSection({ live }))
|
|
130
|
+
const eff = typeof tuiCtx.effect === 'function' ? tuiCtx.effect : (typeof ctx?.effect === 'function' ? ctx.effect : null)
|
|
131
|
+
if (eff) {
|
|
132
|
+
eff(() => () => {
|
|
133
|
+
try { if (typeof dispose === 'function') dispose() } catch {}
|
|
134
|
+
}, 'dsh-moa: tui settings')
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
ctx?.logger?.warn?.('[dsh-moa] Could not register TUI settings: ' + (err?.message || err))
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (service(ctx, 'tuiSettingsSections')?.register) {
|
|
142
|
+
registerWithService(ctx)
|
|
143
|
+
} else if (typeof ctx?.inject === 'function') {
|
|
144
|
+
ctx.inject(['tuiSettingsSections'], registerWithService)
|
|
145
|
+
}
|
|
146
|
+
}
|