@goodandready/dsh-moa 0.2.66 → 0.2.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,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,9 @@ 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'
@@ -147,6 +147,7 @@ export async function runReferencesParallel(references, messages, options = {},
147
147
  costUsd: costInfo.costUsd,
148
148
  durationMs: Date.now() - candidateStart,
149
149
  model: slot?.model ? (slot.provider ? (slot.provider + ":" + slot.model) : slot.model) : label,
150
+ ...(res?.logprobs ? { logprobs: res.logprobs } : {}),
150
151
  ok: true,
151
152
  }
152
153
 
package/lib/moa-schema.js CHANGED
@@ -63,12 +63,17 @@ 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),
66
67
  })
67
68
 
68
69
  export const Config = z.object({
69
70
  enabled: z.boolean().default(true).volatile(),
70
71
  default_preset: z.string().default('default').volatile(),
71
72
  smart_routing_enabled: z.boolean().default(false).volatile(),
73
+ interactive_workbench_enabled: z.boolean().default(false).volatile(),
74
+ ask_clarifying_questions: z.boolean().default(true).volatile(),
75
+ test_gate_enabled: z.boolean().default(false).volatile(),
76
+ uncertainty_scoring_enabled: z.boolean().default(false).volatile(),
72
77
  smart_routing_model: ModelSlotSchema.default({ provider: '', model: '' }).volatile(),
73
78
  prices: z.dict(PriceRow).default({}).volatile(),
74
79
  presets: z.array(PresetSchema).default([
@@ -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,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
- const synthesisPrompt = buildSynthesisPrompt(userPrompt, referenceOutputs, judgeCriteria, {
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.66",
3
+ "version": "0.2.67",
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",