@goodandready/dsh-moa 0.2.73 → 0.2.75
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 +15 -0
- package/README.ru.md +15 -0
- package/README.zh.md +15 -0
- package/lib/client.js +185 -2
- package/lib/events.js +4 -0
- package/lib/moa-schema.js +16 -0
- package/lib/pipeline/context.js +70 -2
- package/lib/pipeline/developer-preferences.js +196 -0
- package/lib/pipeline/escalation.js +160 -0
- package/lib/pipeline/manifest-stack.js +287 -0
- package/lib/pipeline/runner.js +60 -6
- package/lib/pipeline/self-consistency.js +135 -0
- package/lib/routes/preferences.js +108 -0
- package/lib/routes.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic Consensus Threshold & Multi-Judge Escalation (#300)
|
|
3
|
+
*
|
|
4
|
+
* Detects divergence among candidate outputs (Disagreement Metric).
|
|
5
|
+
* When candidate consensus is weak or contradictory, automatically escalates:
|
|
6
|
+
* - auto_upgrade: upgrades from lightweight/fast judge to flagship/senior judge
|
|
7
|
+
* - council_majority: convenes a 3-judge panel with majority voting
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { calculateSimilarity } from './early-consensus.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Calculates the Disagreement Metric (0.0 to 1.0) across candidate outputs.
|
|
14
|
+
* 0.0 means perfect consensus (identical solutions).
|
|
15
|
+
* 1.0 means total divergence (completely distinct solutions).
|
|
16
|
+
*
|
|
17
|
+
* @param {Array<object>} candidateOutputs
|
|
18
|
+
* @returns {number} Disagreement metric between 0.0 and 1.0
|
|
19
|
+
*/
|
|
20
|
+
export function calculateDisagreementMetric(candidateOutputs = []) {
|
|
21
|
+
if (!Array.isArray(candidateOutputs) || candidateOutputs.length < 2) {
|
|
22
|
+
return 0.0
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const validTexts = candidateOutputs
|
|
26
|
+
.filter((c) => c && typeof c.text === 'string' && c.text.trim())
|
|
27
|
+
.map((c) => c.text)
|
|
28
|
+
|
|
29
|
+
if (validTexts.length < 2) return 0.0
|
|
30
|
+
|
|
31
|
+
let totalSim = 0
|
|
32
|
+
let pairs = 0
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < validTexts.length; i++) {
|
|
35
|
+
for (let j = i + 1; j < validTexts.length; j++) {
|
|
36
|
+
totalSim += calculateSimilarity(validTexts[i], validTexts[j])
|
|
37
|
+
pairs++
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (pairs === 0) return 0.0
|
|
42
|
+
const avgSim = totalSim / pairs
|
|
43
|
+
const disagreement = Math.max(0.0, Math.min(1.0, 1.0 - avgSim))
|
|
44
|
+
return Number(disagreement.toFixed(4))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Evaluates whether candidate divergence warrants judge escalation.
|
|
49
|
+
*
|
|
50
|
+
* @param {object} params
|
|
51
|
+
* @param {Array<object>} params.candidateOutputs - Array of successful candidate results
|
|
52
|
+
* @param {object} params.primaryJudge - Primary configured judge slot
|
|
53
|
+
* @param {Array<object>} [params.fallbackJudges=[]] - Chain of backup judges
|
|
54
|
+
* @param {object} [params.escalationJudge] - Explicit flagship upgrade judge
|
|
55
|
+
* @param {string} [params.policy='single'] - 'single' | 'auto_upgrade' | 'council_majority'
|
|
56
|
+
* @param {number} [params.threshold=0.40] - Disagreement threshold to trigger escalation
|
|
57
|
+
* @returns {object} Escalation decision and effective judge(s)
|
|
58
|
+
*/
|
|
59
|
+
export function evaluateJudgeEscalation({
|
|
60
|
+
candidateOutputs = [],
|
|
61
|
+
primaryJudge = { provider: '', model: '' },
|
|
62
|
+
fallbackJudges = [],
|
|
63
|
+
escalationJudge = null,
|
|
64
|
+
policy = 'single',
|
|
65
|
+
threshold = 0.40,
|
|
66
|
+
} = {}) {
|
|
67
|
+
const normPolicy = String(policy || 'single').toLowerCase()
|
|
68
|
+
|
|
69
|
+
if (normPolicy === 'single' || !Array.isArray(candidateOutputs) || candidateOutputs.length < 2) {
|
|
70
|
+
return {
|
|
71
|
+
escalated: false,
|
|
72
|
+
escalatedType: 'none',
|
|
73
|
+
judge: primaryJudge,
|
|
74
|
+
councilJudges: [primaryJudge],
|
|
75
|
+
disagreement: 0.0,
|
|
76
|
+
threshold,
|
|
77
|
+
reason: 'Standard single judge evaluation',
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const disagreement = calculateDisagreementMetric(candidateOutputs)
|
|
82
|
+
const isTriggered = disagreement >= threshold
|
|
83
|
+
|
|
84
|
+
if (!isTriggered) {
|
|
85
|
+
return {
|
|
86
|
+
escalated: false,
|
|
87
|
+
escalatedType: 'none',
|
|
88
|
+
judge: primaryJudge,
|
|
89
|
+
councilJudges: [primaryJudge],
|
|
90
|
+
disagreement,
|
|
91
|
+
threshold,
|
|
92
|
+
reason: `Consensus adequate: disagreement ${(disagreement * 100).toFixed(1)}% < threshold ${(threshold * 100).toFixed(1)}%`,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (normPolicy === 'auto_upgrade') {
|
|
97
|
+
// Pick designated escalationJudge, or the first available fallbackJudge
|
|
98
|
+
let targetJudge = null
|
|
99
|
+
if (escalationJudge && escalationJudge.model) {
|
|
100
|
+
targetJudge = escalationJudge
|
|
101
|
+
} else if (Array.isArray(fallbackJudges) && fallbackJudges.length > 0 && fallbackJudges[0]?.model) {
|
|
102
|
+
targetJudge = fallbackJudges[0]
|
|
103
|
+
} else {
|
|
104
|
+
targetJudge = primaryJudge
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const upgraded = targetJudge.model !== primaryJudge.model || targetJudge.provider !== primaryJudge.provider
|
|
108
|
+
return {
|
|
109
|
+
escalated: upgraded,
|
|
110
|
+
escalatedType: upgraded ? 'auto_upgrade' : 'none',
|
|
111
|
+
judge: targetJudge,
|
|
112
|
+
originalJudge: primaryJudge,
|
|
113
|
+
councilJudges: [targetJudge],
|
|
114
|
+
disagreement,
|
|
115
|
+
threshold,
|
|
116
|
+
reason: upgraded
|
|
117
|
+
? `High candidate divergence (${(disagreement * 100).toFixed(1)}% >= ${(threshold * 100).toFixed(1)}%): upgraded judge to ${targetJudge.provider ? targetJudge.provider + ':' : ''}${targetJudge.model}`
|
|
118
|
+
: `High divergence detected (${(disagreement * 100).toFixed(1)}%), but no senior fallback judge configured`,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (normPolicy === 'council_majority') {
|
|
123
|
+
const council = [primaryJudge]
|
|
124
|
+
if (escalationJudge && escalationJudge.model && escalationJudge.model !== primaryJudge.model) {
|
|
125
|
+
council.push(escalationJudge)
|
|
126
|
+
}
|
|
127
|
+
if (Array.isArray(fallbackJudges)) {
|
|
128
|
+
for (const fb of fallbackJudges) {
|
|
129
|
+
if (council.length >= 3) break
|
|
130
|
+
if (fb && fb.model && !council.some((c) => c.model === fb.model && c.provider === fb.provider)) {
|
|
131
|
+
council.push(fb)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const hasMulti = council.length > 1
|
|
137
|
+
return {
|
|
138
|
+
escalated: hasMulti,
|
|
139
|
+
escalatedType: hasMulti ? 'council_majority' : 'none',
|
|
140
|
+
judge: council[0],
|
|
141
|
+
originalJudge: primaryJudge,
|
|
142
|
+
councilJudges: council,
|
|
143
|
+
disagreement,
|
|
144
|
+
threshold,
|
|
145
|
+
reason: hasMulti
|
|
146
|
+
? `High candidate divergence (${(disagreement * 100).toFixed(1)}% >= ${(threshold * 100).toFixed(1)}%): assembled ${council.length}-judge council`
|
|
147
|
+
: `High divergence detected, but fewer than 2 judges available for council`,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
escalated: false,
|
|
153
|
+
escalatedType: 'none',
|
|
154
|
+
judge: primaryJudge,
|
|
155
|
+
councilJudges: [primaryJudge],
|
|
156
|
+
disagreement,
|
|
157
|
+
threshold,
|
|
158
|
+
reason: 'Standard single judge',
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stack-Aware Dynamic Persona Evolution from Project Manifests (#308)
|
|
3
|
+
*
|
|
4
|
+
* Scans project manifests (package.json, Cargo.toml, pyproject.toml, requirements.txt, go.mod)
|
|
5
|
+
* in the workspace root to infer primary programming languages, frameworks, ORMs,
|
|
6
|
+
* and test suites. Evolves candidate proposers into specialized domain experts
|
|
7
|
+
* (e.g. React Optimization, TypeScript Strict Safety, Accessibility, FastAPI Async)
|
|
8
|
+
* to maximize architectural hypothesis diversity.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Scans workspace root manifests to detect project languages and dependencies.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} cwd - Workspace root directory
|
|
18
|
+
* @returns {object} Detected stack information
|
|
19
|
+
*/
|
|
20
|
+
export function detectProjectStack(cwd = process.cwd()) {
|
|
21
|
+
const stack = {
|
|
22
|
+
primaryLanguage: 'unknown',
|
|
23
|
+
frameworks: [],
|
|
24
|
+
libraries: [],
|
|
25
|
+
manifestFound: false,
|
|
26
|
+
stackSummary: '',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!cwd || typeof cwd !== 'string') return stack
|
|
30
|
+
|
|
31
|
+
// 1. Node / JavaScript / TypeScript (package.json)
|
|
32
|
+
const pkgPath = path.join(cwd, 'package.json')
|
|
33
|
+
if (fs.existsSync(pkgPath)) {
|
|
34
|
+
try {
|
|
35
|
+
const raw = fs.readFileSync(pkgPath, 'utf8')
|
|
36
|
+
const pkg = JSON.parse(raw)
|
|
37
|
+
stack.manifestFound = true
|
|
38
|
+
stack.primaryLanguage = 'javascript'
|
|
39
|
+
|
|
40
|
+
const allDeps = {
|
|
41
|
+
...(pkg.dependencies || {}),
|
|
42
|
+
...(pkg.devDependencies || {}),
|
|
43
|
+
...(pkg.peerDependencies || {}),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (allDeps.typescript || fs.existsSync(path.join(cwd, 'tsconfig.json'))) {
|
|
47
|
+
stack.primaryLanguage = 'typescript'
|
|
48
|
+
stack.libraries.push('typescript')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Frameworks
|
|
52
|
+
if (allDeps.next) stack.frameworks.push('next.js')
|
|
53
|
+
else if (allDeps.react) stack.frameworks.push('react')
|
|
54
|
+
|
|
55
|
+
if (allDeps.nuxt) stack.frameworks.push('nuxt')
|
|
56
|
+
else if (allDeps.vue) stack.frameworks.push('vue')
|
|
57
|
+
|
|
58
|
+
if (allDeps['@sveltejs/kit']) stack.frameworks.push('sveltekit')
|
|
59
|
+
else if (allDeps.svelte) stack.frameworks.push('svelte')
|
|
60
|
+
|
|
61
|
+
if (allDeps['@angular/core']) stack.frameworks.push('angular')
|
|
62
|
+
if (allDeps['@nestjs/core']) stack.frameworks.push('nestjs')
|
|
63
|
+
if (allDeps.fastify) stack.frameworks.push('fastify')
|
|
64
|
+
if (allDeps.express) stack.frameworks.push('express')
|
|
65
|
+
|
|
66
|
+
// Styling & UI
|
|
67
|
+
if (allDeps.tailwindcss) stack.libraries.push('tailwindcss')
|
|
68
|
+
|
|
69
|
+
// ORM & Data
|
|
70
|
+
if (allDeps.prisma || allDeps['@prisma/client']) stack.libraries.push('prisma')
|
|
71
|
+
if (allDeps['drizzle-orm']) stack.libraries.push('drizzle')
|
|
72
|
+
if (allDeps.typeorm) stack.libraries.push('typeorm')
|
|
73
|
+
if (allDeps.mongoose) stack.libraries.push('mongoose')
|
|
74
|
+
|
|
75
|
+
// Validation
|
|
76
|
+
if (allDeps.zod) stack.libraries.push('zod')
|
|
77
|
+
if (allDeps.valibot) stack.libraries.push('valibot')
|
|
78
|
+
|
|
79
|
+
// Testing
|
|
80
|
+
if (allDeps.vitest) stack.libraries.push('vitest')
|
|
81
|
+
if (allDeps.jest) stack.libraries.push('jest')
|
|
82
|
+
if (allDeps.playwright || allDeps['@playwright/test']) stack.libraries.push('playwright')
|
|
83
|
+
} catch {
|
|
84
|
+
// safe fallback on corrupted package.json
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2. Rust (Cargo.toml)
|
|
89
|
+
const cargoPath = path.join(cwd, 'Cargo.toml')
|
|
90
|
+
if (fs.existsSync(cargoPath)) {
|
|
91
|
+
try {
|
|
92
|
+
const content = fs.readFileSync(cargoPath, 'utf8')
|
|
93
|
+
stack.manifestFound = true
|
|
94
|
+
stack.primaryLanguage = 'rust'
|
|
95
|
+
|
|
96
|
+
if (content.includes('axum')) stack.frameworks.push('axum')
|
|
97
|
+
if (content.includes('actix-web')) stack.frameworks.push('actix-web')
|
|
98
|
+
if (content.includes('tokio')) stack.libraries.push('tokio')
|
|
99
|
+
if (content.includes('serde')) stack.libraries.push('serde')
|
|
100
|
+
if (content.includes('sqlx')) stack.libraries.push('sqlx')
|
|
101
|
+
if (content.includes('diesel')) stack.libraries.push('diesel')
|
|
102
|
+
} catch {
|
|
103
|
+
// safe fallback
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 3. Python (pyproject.toml, requirements.txt)
|
|
108
|
+
const pyprojectPath = path.join(cwd, 'pyproject.toml')
|
|
109
|
+
const reqPath = path.join(cwd, 'requirements.txt')
|
|
110
|
+
if (fs.existsSync(pyprojectPath) || fs.existsSync(reqPath)) {
|
|
111
|
+
try {
|
|
112
|
+
const pyContent = (fs.existsSync(pyprojectPath) ? fs.readFileSync(pyprojectPath, 'utf8') : '') +
|
|
113
|
+
'\n' + (fs.existsSync(reqPath) ? fs.readFileSync(reqPath, 'utf8') : '')
|
|
114
|
+
stack.manifestFound = true
|
|
115
|
+
if (stack.primaryLanguage === 'unknown') stack.primaryLanguage = 'python'
|
|
116
|
+
|
|
117
|
+
if (pyContent.includes('fastapi')) stack.frameworks.push('fastapi')
|
|
118
|
+
if (pyContent.includes('django')) stack.frameworks.push('django')
|
|
119
|
+
if (pyContent.includes('flask')) stack.frameworks.push('flask')
|
|
120
|
+
if (pyContent.includes('pydantic')) stack.libraries.push('pydantic')
|
|
121
|
+
if (pyContent.includes('sqlalchemy')) stack.libraries.push('sqlalchemy')
|
|
122
|
+
if (pyContent.includes('pytest')) stack.libraries.push('pytest')
|
|
123
|
+
if (pyContent.includes('torch')) stack.libraries.push('pytorch')
|
|
124
|
+
if (pyContent.includes('pandas')) stack.libraries.push('pandas')
|
|
125
|
+
} catch {
|
|
126
|
+
// safe fallback
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 4. Go (go.mod)
|
|
131
|
+
const goModPath = path.join(cwd, 'go.mod')
|
|
132
|
+
if (fs.existsSync(goModPath)) {
|
|
133
|
+
try {
|
|
134
|
+
const goContent = fs.readFileSync(goModPath, 'utf8')
|
|
135
|
+
stack.manifestFound = true
|
|
136
|
+
if (stack.primaryLanguage === 'unknown') stack.primaryLanguage = 'go'
|
|
137
|
+
|
|
138
|
+
if (goContent.includes('gin-gonic/gin')) stack.frameworks.push('gin')
|
|
139
|
+
if (goContent.includes('gofiber/fiber')) stack.frameworks.push('fiber')
|
|
140
|
+
if (goContent.includes('labstack/echo')) stack.frameworks.push('echo')
|
|
141
|
+
if (goContent.includes('gorm.io/gorm')) stack.libraries.push('gorm')
|
|
142
|
+
if (goContent.includes('stretchr/testify')) stack.libraries.push('testify')
|
|
143
|
+
} catch {
|
|
144
|
+
// safe fallback
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Build summary string
|
|
149
|
+
const parts = []
|
|
150
|
+
if (stack.primaryLanguage !== 'unknown') parts.push(stack.primaryLanguage)
|
|
151
|
+
if (stack.frameworks.length > 0) parts.push(`framework: ${stack.frameworks.join(', ')}`)
|
|
152
|
+
if (stack.libraries.length > 0) parts.push(`libs: ${stack.libraries.slice(0, 4).join(', ')}`)
|
|
153
|
+
stack.stackSummary = parts.join(' | ')
|
|
154
|
+
|
|
155
|
+
return stack
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Returns specialized domain personas based on detected stack.
|
|
160
|
+
*
|
|
161
|
+
* @param {object} stackInfo
|
|
162
|
+
* @returns {Array<string>} Array of specialized persona role descriptions
|
|
163
|
+
*/
|
|
164
|
+
export function getStackPersonaSpecialties(stackInfo = {}) {
|
|
165
|
+
const lang = stackInfo.primaryLanguage || 'unknown'
|
|
166
|
+
const frameworks = Array.isArray(stackInfo.frameworks) ? stackInfo.frameworks : []
|
|
167
|
+
const libs = Array.isArray(stackInfo.libraries) ? stackInfo.libraries : []
|
|
168
|
+
|
|
169
|
+
// React / Next.js / Frontend
|
|
170
|
+
if (frameworks.includes('react') || frameworks.includes('next.js') || frameworks.includes('vue') || frameworks.includes('svelte')) {
|
|
171
|
+
return [
|
|
172
|
+
'Component Architecture, Hooks & Re-render Optimization Specialist',
|
|
173
|
+
libs.includes('typescript') || lang === 'typescript'
|
|
174
|
+
? 'TypeScript Strict Typing, Generic Invariants & Defensive Schema Expert'
|
|
175
|
+
: 'State Management, Data Flow & Edge-Case Specialist',
|
|
176
|
+
'Accessibility (a11y), Semantic UI & Modern CSS Performance Reviewer',
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Python / FastAPI / Django
|
|
181
|
+
if (lang === 'python') {
|
|
182
|
+
return [
|
|
183
|
+
frameworks.includes('fastapi')
|
|
184
|
+
? 'FastAPI Async Concurrency & High-Throughput REST Architect'
|
|
185
|
+
: 'Pythonic Clean Architecture & Idiomatic Object Design Specialist',
|
|
186
|
+
libs.includes('pydantic')
|
|
187
|
+
? 'Pydantic V2 Schema Validation & Type-Safe Domain Modeler'
|
|
188
|
+
: 'Defensive Data Integrity & Error Boundary Engineer',
|
|
189
|
+
libs.includes('pytest')
|
|
190
|
+
? 'Pytest Fixtures, Mocking & Comprehensive Edge-Case Test Engineer'
|
|
191
|
+
: 'Performance Profiling & Memory Leak Auditor',
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Rust
|
|
196
|
+
if (lang === 'rust') {
|
|
197
|
+
return [
|
|
198
|
+
'Rust Zero-Cost Abstractions & Concurrency Architect',
|
|
199
|
+
'Rust Memory Safety, Error Handling & Lifetime Specialist',
|
|
200
|
+
'Rust Idiomatic API & Performance Specialist',
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Go
|
|
205
|
+
if (lang === 'go') {
|
|
206
|
+
return [
|
|
207
|
+
'Go Idiomatic Concurrency & Channel Synchronization Architect',
|
|
208
|
+
'Go Interface Design, Error Wrapping & Zero-Allocation Specialist',
|
|
209
|
+
'Go Production Reliability & Table-Driven Unit Testing Specialist',
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// General TypeScript / Node
|
|
214
|
+
if (lang === 'typescript') {
|
|
215
|
+
return [
|
|
216
|
+
'TypeScript Strict Type Safety & Clean Architecture Specialist',
|
|
217
|
+
'Async Flow, Micro-Optimization & Error Boundary Engineer',
|
|
218
|
+
'Maintainability, Modularity & Test Coverage Auditor',
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Universal Default
|
|
223
|
+
return [
|
|
224
|
+
'System Architecture & Core Correctness Engineer',
|
|
225
|
+
'Edge-Case Analysis & Defensive Error Handling Specialist',
|
|
226
|
+
'Clean Code, Maintainability & Documentation Specialist',
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Assigns stack-aware specialized personas to candidate proposer slots.
|
|
232
|
+
*
|
|
233
|
+
* @param {Array<object>} references - Proposer slots
|
|
234
|
+
* @param {object} stackInfo - Output of detectProjectStack
|
|
235
|
+
* @param {object} [options]
|
|
236
|
+
* @param {boolean} [options.enabled=true] - Whether stack-aware personas are enabled
|
|
237
|
+
* @returns {{ references: Array<object>, isSpecialized: boolean, stackSummary: string }}
|
|
238
|
+
*/
|
|
239
|
+
export function assignStackAwarePersonas(references = [], stackInfo = {}, options = {}) {
|
|
240
|
+
const { enabled = true } = options
|
|
241
|
+
|
|
242
|
+
if (!enabled || !Array.isArray(references) || references.length === 0) {
|
|
243
|
+
return {
|
|
244
|
+
references: Array.isArray(references) ? references : [],
|
|
245
|
+
isSpecialized: false,
|
|
246
|
+
stackSummary: stackInfo.stackSummary || '',
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const specialties = getStackPersonaSpecialties(stackInfo)
|
|
251
|
+
const evolved = references.map((slot, idx) => {
|
|
252
|
+
// If user already specified a custom non-default persona, preserve it
|
|
253
|
+
const hasCustom = slot.role_persona && slot.role_persona !== 'general' && slot.role_persona !== 'proposer'
|
|
254
|
+
if (hasCustom) {
|
|
255
|
+
return { ...slot }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const specialty = specialties[idx % specialties.length]
|
|
259
|
+
return {
|
|
260
|
+
...slot,
|
|
261
|
+
role_persona: specialty,
|
|
262
|
+
stackSpecialization: specialty,
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
references: evolved,
|
|
268
|
+
isSpecialized: Boolean(stackInfo.manifestFound),
|
|
269
|
+
stackSummary: stackInfo.stackSummary || '',
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Formats a directive summarizing the project stack for injection into judge or synthesis prompts.
|
|
275
|
+
*
|
|
276
|
+
* @param {object} stackInfo
|
|
277
|
+
* @returns {string|null}
|
|
278
|
+
*/
|
|
279
|
+
export function formatStackAwareDirective(stackInfo = {}) {
|
|
280
|
+
if (!stackInfo || !stackInfo.manifestFound || !stackInfo.stackSummary) {
|
|
281
|
+
return null
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return `### Project Stack & Architecture Context
|
|
285
|
+
- Detected Environment: ${stackInfo.stackSummary}
|
|
286
|
+
- Verification Requirement: All proposals must adhere to the conventions and idiomatic idioms of this technology stack.`
|
|
287
|
+
}
|
package/lib/pipeline/runner.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { evaluateJudgeEscalation } from './escalation.js'
|
|
2
|
+
import { calculateSelfConsistencyAgreement } from './self-consistency.js'
|
|
1
3
|
import { pruneCandidateEnsemble, formatPruningDirective } from './context-pruner.js'
|
|
2
4
|
import { buildAstCompositeMergeDirective } from '../moa-ast-merge.js'
|
|
3
5
|
import { runVisualMoaConsensus } from '../moa-visual-consensus.js'
|
|
@@ -106,6 +108,13 @@ export async function runMoAPipeline({
|
|
|
106
108
|
})
|
|
107
109
|
const syntaxDirective = syntaxResult.filterApplied ? formatSyntaxDirective(referenceOutputs, { blind: ctxStage.isBlindEvaluation }) : null
|
|
108
110
|
|
|
111
|
+
if (ctxStage.selfConsistencyExpanded && successfulRefs.length > 1) {
|
|
112
|
+
const scAgreement = calculateSelfConsistencyAgreement(successfulRefs)
|
|
113
|
+
if (typeof onProgress === 'function') {
|
|
114
|
+
onProgress(`🎯 *Self-Consistency Agreement: ${(scAgreement.agreementRatio * 100).toFixed(0)}% (${scAgreement.isStrongConsensus ? 'Strong consensus' : 'Divergent sampling'}) across ${scAgreement.sampleCount} samples*\n`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
109
118
|
// Stage 3: Clarifying Questions Branch
|
|
110
119
|
if (ctxStage.needsQuestions) {
|
|
111
120
|
return await stageClarifyingQuestions({
|
|
@@ -190,11 +199,56 @@ export async function runMoAPipeline({
|
|
|
190
199
|
return { error: new Error('Turn aborted') }
|
|
191
200
|
}
|
|
192
201
|
|
|
202
|
+
// Pre-Stage 6: Dynamic Consensus Threshold & Multi-Judge Escalation (#300)
|
|
203
|
+
const escalationResult = evaluateJudgeEscalation({
|
|
204
|
+
candidateOutputs: syntaxResult.validCandidates.length > 0 ? syntaxResult.validCandidates : successfulRefs,
|
|
205
|
+
primaryJudge: ctxStage.primaryJudge,
|
|
206
|
+
fallbackJudges: ctxStage.fallbackJudges,
|
|
207
|
+
escalationJudge: ctxStage.escalationJudge,
|
|
208
|
+
policy: ctxStage.escalationPolicy,
|
|
209
|
+
threshold: ctxStage.escalationDisagreementThreshold,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
let effectivePrimaryJudge = ctxStage.primaryJudge
|
|
213
|
+
let effectiveFallbackJudges = ctxStage.fallbackJudges
|
|
214
|
+
let effectiveJudgesChain = ctxStage.judgesChain
|
|
215
|
+
let effectivePreset = preset
|
|
216
|
+
|
|
217
|
+
if (escalationResult.escalated) {
|
|
218
|
+
emitEvent(ctx, MOA_EVENTS.JUDGE_ESCALATION, {
|
|
219
|
+
runId,
|
|
220
|
+
policy: ctxStage.escalationPolicy,
|
|
221
|
+
disagreement: escalationResult.disagreement,
|
|
222
|
+
threshold: escalationResult.threshold,
|
|
223
|
+
escalatedType: escalationResult.escalatedType,
|
|
224
|
+
originalJudge: escalationResult.originalJudge,
|
|
225
|
+
escalatedJudge: escalationResult.judge,
|
|
226
|
+
councilJudges: escalationResult.councilJudges,
|
|
227
|
+
})
|
|
228
|
+
if (typeof onProgress === 'function') {
|
|
229
|
+
onProgress(`⚖️ *Judge Escalation (${escalationResult.escalatedType}): ${escalationResult.reason}*\n`)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (escalationResult.escalatedType === 'auto_upgrade') {
|
|
233
|
+
effectivePrimaryJudge = escalationResult.judge
|
|
234
|
+
effectiveFallbackJudges = ctxStage.fallbackJudges.filter(
|
|
235
|
+
(j) => !(j.provider === escalationResult.judge.provider && j.model === escalationResult.judge.model)
|
|
236
|
+
)
|
|
237
|
+
effectiveJudgesChain = [effectivePrimaryJudge, ...effectiveFallbackJudges]
|
|
238
|
+
} else if (escalationResult.escalatedType === 'council_majority') {
|
|
239
|
+
effectivePreset = {
|
|
240
|
+
...preset,
|
|
241
|
+
multi_judge_enabled: true,
|
|
242
|
+
judge_models: escalationResult.councilJudges,
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
193
247
|
// Stage 6: Multi-Judge Panel
|
|
194
248
|
const mjRes = await stageMultiJudge({
|
|
195
|
-
preset,
|
|
249
|
+
preset: effectivePreset,
|
|
196
250
|
successfulRefs: syntaxResult.validCandidates.length > 0 ? syntaxResult.validCandidates : successfulRefs,
|
|
197
|
-
referenceOutputs, primaryJudge:
|
|
251
|
+
referenceOutputs, primaryJudge: effectivePrimaryJudge,
|
|
198
252
|
userPrompt, judgeCriteria: ctxStage.judgeCriteria, prices, maxTokens: ctxStage.maxTokens,
|
|
199
253
|
callLlm, signal, onProgress, cwd, runId, isRefinement: ctxStage.isRefinement,
|
|
200
254
|
isFastMode: ctxStage.isFastMode, collectedCtx: ctxStage.collectedCtx, startTime,
|
|
@@ -253,7 +307,7 @@ export async function runMoAPipeline({
|
|
|
253
307
|
}
|
|
254
308
|
}
|
|
255
309
|
|
|
256
|
-
const fileScoringDirective = [rawFileDirective, visualResult?.directive, adversarialTddResult?.directive, syntaxDirective, pruningDirective].filter(Boolean).join("\n\n") || null
|
|
310
|
+
const fileScoringDirective = [rawFileDirective, visualResult?.directive, adversarialTddResult?.directive, syntaxDirective, pruningDirective, ctxStage.stackAwareDirective, ctxStage.developerPreferencesDirective].filter(Boolean).join("\n\n") || null
|
|
257
311
|
|
|
258
312
|
// Stage 7: Synthesis Phase
|
|
259
313
|
const synthRes = await stageSynthesis({
|
|
@@ -263,9 +317,9 @@ export async function runMoAPipeline({
|
|
|
263
317
|
judgeCriteria: ctxStage.judgeCriteria,
|
|
264
318
|
isCuratorSynthesis: ctxStage.isCuratorSynthesis, isBlindEvaluation: ctxStage.isBlindEvaluation,
|
|
265
319
|
priorTurnBaseline: ctxStage.priorTurnBaseline, compositeMergeDirective, fileScoringDirective,
|
|
266
|
-
multiJudgeResult, preset, prices, maxTokens: ctxStage.maxTokens, isFastMode: ctxStage.isFastMode,
|
|
267
|
-
primaryJudge:
|
|
268
|
-
judgesChain:
|
|
320
|
+
multiJudgeResult, preset: effectivePreset, prices, maxTokens: ctxStage.maxTokens, isFastMode: ctxStage.isFastMode,
|
|
321
|
+
primaryJudge: effectivePrimaryJudge, fallbackJudges: effectiveFallbackJudges,
|
|
322
|
+
judgesChain: effectiveJudgesChain, aggTemp: ctxStage.aggTemp,
|
|
269
323
|
aggTimeoutSec: ctxStage.aggTimeoutSec, isStreamAggregator: ctxStage.isStreamAggregator,
|
|
270
324
|
onStreamDelta, signal, callLlm, onProgress, cwd, runId, isRefinement: ctxStage.isRefinement,
|
|
271
325
|
collectedCtx: ctxStage.collectedCtx, startTime,
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-Consistency Temperature Sampling for Single Models (#301)
|
|
3
|
+
*
|
|
4
|
+
* Allows users with a single model slot (e.g., local Ollama/vLLM or a single API key)
|
|
5
|
+
* to benefit from Mixture of Agents ensemble diversity by generating parallel samples
|
|
6
|
+
* at varying temperatures and complementary system personas, followed by consistency voting.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ROLE_PERSONA_PROMPTS } from '../moa-prompts.js'
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_SELF_CONSISTENCY_TEMPERATURES = Object.freeze([0.2, 0.6, 0.9])
|
|
12
|
+
export const DEFAULT_SELF_CONSISTENCY_PERSONAS = Object.freeze(['proposer', 'challenger', 'minimalist'])
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Expands a single reference model into multiple parallel proposer slots
|
|
16
|
+
* with distinct temperatures and complementary reasoning personas.
|
|
17
|
+
*
|
|
18
|
+
* @param {Array<object>} referenceModels - Original reference model slots
|
|
19
|
+
* @param {object} [options]
|
|
20
|
+
* @param {boolean} [options.enabled=false] - Whether self-consistency expansion is enabled
|
|
21
|
+
* @param {number} [options.samples=3] - Number of candidate samples to generate (2-10)
|
|
22
|
+
* @param {Array<number>} [options.temperatures] - Array of temperatures for samples
|
|
23
|
+
* @param {Array<string>} [options.personas] - Array of persona role names
|
|
24
|
+
* @returns {{ references: Array<object>, isExpanded: boolean, sampleCount: number }}
|
|
25
|
+
*/
|
|
26
|
+
export function expandSelfConsistencySlots(referenceModels = [], options = {}) {
|
|
27
|
+
const {
|
|
28
|
+
enabled = false,
|
|
29
|
+
samples = 3,
|
|
30
|
+
temperatures = DEFAULT_SELF_CONSISTENCY_TEMPERATURES,
|
|
31
|
+
personas = DEFAULT_SELF_CONSISTENCY_PERSONAS,
|
|
32
|
+
} = options
|
|
33
|
+
|
|
34
|
+
// Expansion applies only when explicitly enabled or requested
|
|
35
|
+
if (!enabled) {
|
|
36
|
+
return {
|
|
37
|
+
references: Array.isArray(referenceModels) ? referenceModels : [],
|
|
38
|
+
isExpanded: false,
|
|
39
|
+
sampleCount: Array.isArray(referenceModels) ? referenceModels.length : 0,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const rawList = Array.isArray(referenceModels) && referenceModels.length > 0
|
|
44
|
+
? referenceModels
|
|
45
|
+
: [{ provider: '', model: '' }]
|
|
46
|
+
|
|
47
|
+
// Only expand when a single model slot is configured
|
|
48
|
+
if (rawList.length !== 1) {
|
|
49
|
+
return {
|
|
50
|
+
references: rawList,
|
|
51
|
+
isExpanded: false,
|
|
52
|
+
sampleCount: rawList.length,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const baseSlot = rawList[0] || {}
|
|
57
|
+
const count = Math.min(10, Math.max(2, Number(samples) || 3))
|
|
58
|
+
const expandedList = []
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < count; i++) {
|
|
61
|
+
const temp = Array.isArray(temperatures) && typeof temperatures[i] === 'number'
|
|
62
|
+
? temperatures[i]
|
|
63
|
+
: (DEFAULT_SELF_CONSISTENCY_TEMPERATURES[i % DEFAULT_SELF_CONSISTENCY_TEMPERATURES.length] || 0.6)
|
|
64
|
+
|
|
65
|
+
const role = Array.isArray(personas) && personas[i]
|
|
66
|
+
? personas[i]
|
|
67
|
+
: (DEFAULT_SELF_CONSISTENCY_PERSONAS[i % DEFAULT_SELF_CONSISTENCY_PERSONAS.length] || 'proposer')
|
|
68
|
+
|
|
69
|
+
expandedList.push({
|
|
70
|
+
...baseSlot,
|
|
71
|
+
label: baseSlot.label
|
|
72
|
+
? `${baseSlot.label} [T=${temp}, ${role}]`
|
|
73
|
+
: `Sample ${i + 1} (${baseSlot.model || 'model'}, T=${temp})`,
|
|
74
|
+
temperature: temp,
|
|
75
|
+
role_persona: role,
|
|
76
|
+
sampleIndex: i + 1,
|
|
77
|
+
isSelfConsistencySample: true,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
references: expandedList,
|
|
83
|
+
isExpanded: true,
|
|
84
|
+
sampleCount: expandedList.length,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Calculates majority agreement and self-consistency metrics across candidate outputs.
|
|
90
|
+
*
|
|
91
|
+
* @param {Array<object>} candidateOutputs - Array of successful candidate results
|
|
92
|
+
* @returns {{ agreementRatio: number, isStrongConsensus: boolean, sampleCount: number }}
|
|
93
|
+
*/
|
|
94
|
+
export function calculateSelfConsistencyAgreement(candidateOutputs = []) {
|
|
95
|
+
if (!Array.isArray(candidateOutputs) || candidateOutputs.length < 2) {
|
|
96
|
+
return {
|
|
97
|
+
agreementRatio: 1.0,
|
|
98
|
+
isStrongConsensus: true,
|
|
99
|
+
sampleCount: Array.isArray(candidateOutputs) ? candidateOutputs.length : 0,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const texts = candidateOutputs
|
|
104
|
+
.filter((c) => c && typeof c.text === 'string' && c.text.trim())
|
|
105
|
+
.map((c) => c.text.trim().toLowerCase().replace(/\s+/g, ' '))
|
|
106
|
+
|
|
107
|
+
if (texts.length < 2) {
|
|
108
|
+
return {
|
|
109
|
+
agreementRatio: 1.0,
|
|
110
|
+
isStrongConsensus: true,
|
|
111
|
+
sampleCount: texts.length,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Count exact and near-match clusters
|
|
116
|
+
let maxClusterSize = 1
|
|
117
|
+
for (let i = 0; i < texts.length; i++) {
|
|
118
|
+
let clusterSize = 1
|
|
119
|
+
for (let j = 0; j < texts.length; j++) {
|
|
120
|
+
if (i === j) continue
|
|
121
|
+
// If texts are very similar (> 85% overlap)
|
|
122
|
+
if (texts[i] === texts[j] || (texts[i].length > 20 && texts[j].includes(texts[i].slice(0, 40)))) {
|
|
123
|
+
clusterSize++
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (clusterSize > maxClusterSize) maxClusterSize = clusterSize
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const agreementRatio = Number((maxClusterSize / texts.length).toFixed(2))
|
|
130
|
+
return {
|
|
131
|
+
agreementRatio,
|
|
132
|
+
isStrongConsensus: agreementRatio >= 0.66,
|
|
133
|
+
sampleCount: texts.length,
|
|
134
|
+
}
|
|
135
|
+
}
|