@goodandready/dsh-moa 0.2.78 → 0.2.80
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 +2 -0
- package/README.ru.md +2 -0
- package/README.zh.md +2 -0
- package/lib/client.js +204 -3
- package/lib/events.js +4 -0
- package/lib/moa-schema.js +22 -0
- package/lib/pipeline/budget-guard.js +158 -0
- package/lib/pipeline/embedding-clustering.js +456 -0
- package/lib/pipeline/pareto-frontier.js +167 -0
- package/lib/pipeline/rebuttal-debate.js +335 -0
- package/lib/pipeline/runner.js +90 -5
- package/lib/pricing.js +8 -3
- package/package.json +1 -1
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blind Embedding Clustering for Zero-LLM Fast Consensus (#312)
|
|
3
|
+
*
|
|
4
|
+
* Clusters candidate responses using local high-dimensional n-gram semantic embeddings.
|
|
5
|
+
* When a dominant cluster exhibits high semantic convergence (cosine similarity >= threshold),
|
|
6
|
+
* the plugin automatically promotes the cluster centroid candidate without invoking
|
|
7
|
+
* the expensive LLM Judge, saving significant tokens and latency.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { logger } from '../logger.js'
|
|
11
|
+
import { promoteCandidateWorkspace, cleanMoaWorkspaces } from '../workspace/fs.js'
|
|
12
|
+
import { createPromotedPreview } from '../live-canvas.js'
|
|
13
|
+
import { recordMoaRunAsync, candidatesForHistory } from '../history.js'
|
|
14
|
+
import { emitEvent, MOA_EVENTS } from '../events.js'
|
|
15
|
+
import { estimateTokenCost } from '../pricing.js'
|
|
16
|
+
|
|
17
|
+
const EMBEDDING_DIMENSIONS = 1024
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 32-bit FNV-1a hash function for strings
|
|
21
|
+
* @param {string} str
|
|
22
|
+
* @returns {number}
|
|
23
|
+
*/
|
|
24
|
+
export function fnv1a32(str) {
|
|
25
|
+
let hash = 2166136261
|
|
26
|
+
for (let i = 0; i < str.length; i++) {
|
|
27
|
+
hash ^= str.charCodeAt(i)
|
|
28
|
+
hash = Math.imul(hash, 16777619)
|
|
29
|
+
}
|
|
30
|
+
return hash >>> 0
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Normalizes text/code for embedding extraction.
|
|
35
|
+
* @param {string} text
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeText(text = '') {
|
|
39
|
+
if (typeof text !== 'string') return ''
|
|
40
|
+
return text
|
|
41
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
42
|
+
.replace(/\/\/.*$/gm, '')
|
|
43
|
+
.replace(/(^|\s)#.*$/gm, '$1')
|
|
44
|
+
.replace(/\r?\n/g, ' ')
|
|
45
|
+
.replace(/[ \t]+/g, ' ')
|
|
46
|
+
.trim()
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generates a unit-normalized Float32Array embedding vector from text
|
|
52
|
+
* using word tokens, code operators, and character n-grams.
|
|
53
|
+
* @param {string} text
|
|
54
|
+
* @param {number} [dimensions=1024]
|
|
55
|
+
* @returns {Float32Array}
|
|
56
|
+
*/
|
|
57
|
+
export function generateEmbedding(text = '', dimensions = EMBEDDING_DIMENSIONS) {
|
|
58
|
+
const vector = new Float32Array(dimensions)
|
|
59
|
+
const normalized = normalizeText(text)
|
|
60
|
+
if (!normalized) return vector
|
|
61
|
+
|
|
62
|
+
// 1. Tokens including keywords, identifiers, and syntax operators
|
|
63
|
+
const tokens = normalized.match(/[a-z0-9_$]+|===|!==|<=|>=|=>|\+\+|--|&&|\|\||\.\.\.|[{}()[\];,+\-*\/%=<>!&|^~?:.]/g) || []
|
|
64
|
+
const counts = new Map()
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
67
|
+
const t = tokens[i]
|
|
68
|
+
const h = fnv1a32(t) % dimensions
|
|
69
|
+
counts.set(h, (counts.get(h) || 0) + 1.2)
|
|
70
|
+
|
|
71
|
+
// Token bigrams
|
|
72
|
+
if (i < tokens.length - 1) {
|
|
73
|
+
const bigram = `${t}_${tokens[i + 1]}`
|
|
74
|
+
const h2 = fnv1a32(bigram) % dimensions
|
|
75
|
+
counts.set(h2, (counts.get(h2) || 0) + 0.8)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 2. Character 3-grams for morphology and syntax
|
|
80
|
+
const charSpan = Math.min(normalized.length, 12000)
|
|
81
|
+
for (let i = 0; i <= charSpan - 3; i++) {
|
|
82
|
+
const trigram = normalized.slice(i, i + 3)
|
|
83
|
+
const h3 = fnv1a32(trigram) % dimensions
|
|
84
|
+
counts.set(h3, (counts.get(h3) || 0) + 0.3)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 3. Sublinear TF weighting: 1 + ln(count)
|
|
88
|
+
let sumSq = 0
|
|
89
|
+
for (const [bucket, count] of counts.entries()) {
|
|
90
|
+
const val = 1 + Math.log(count)
|
|
91
|
+
vector[bucket] = val
|
|
92
|
+
sumSq += val * val
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 4. L2 Normalization
|
|
96
|
+
if (sumSq > 0) {
|
|
97
|
+
const norm = Math.sqrt(sumSq)
|
|
98
|
+
for (let i = 0; i < dimensions; i++) {
|
|
99
|
+
vector[i] /= norm
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return vector
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Computes cosine similarity between two unit-normalized vectors.
|
|
108
|
+
* Dot product equals cosine similarity for unit vectors.
|
|
109
|
+
* @param {Float32Array} vecA
|
|
110
|
+
* @param {Float32Array} vecB
|
|
111
|
+
* @returns {number}
|
|
112
|
+
*/
|
|
113
|
+
export function computeCosineSimilarity(vecA, vecB) {
|
|
114
|
+
if (!vecA || !vecB || vecA.length !== vecB.length) return 0.0
|
|
115
|
+
let dot = 0
|
|
116
|
+
for (let i = 0; i < vecA.length; i++) {
|
|
117
|
+
dot += vecA[i] * vecB[i]
|
|
118
|
+
}
|
|
119
|
+
const clamped = Math.max(0.0, Math.min(1.0, dot))
|
|
120
|
+
return Number(clamped.toFixed(4))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Builds pairwise cosine similarity matrix for candidate solutions.
|
|
125
|
+
* @param {Array<object>} candidates
|
|
126
|
+
* @returns {{ vectors: Float32Array[], matrix: number[][] }}
|
|
127
|
+
*/
|
|
128
|
+
export function buildSimilarityMatrix(candidates = []) {
|
|
129
|
+
const n = candidates.length
|
|
130
|
+
const vectors = candidates.map((c) => generateEmbedding(c.text || ''))
|
|
131
|
+
const matrix = Array.from({ length: n }, () => new Array(n).fill(0))
|
|
132
|
+
|
|
133
|
+
for (let i = 0; i < n; i++) {
|
|
134
|
+
matrix[i][i] = 1.0
|
|
135
|
+
for (let j = i + 1; j < n; j++) {
|
|
136
|
+
const sim = computeCosineSimilarity(vectors[i], vectors[j])
|
|
137
|
+
matrix[i][j] = sim
|
|
138
|
+
matrix[j][i] = sim
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { vectors, matrix }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Clusters candidate embeddings and determines if consensus was reached.
|
|
147
|
+
* @param {Array<object>} candidates
|
|
148
|
+
* @param {object} [options={}]
|
|
149
|
+
* @returns {object}
|
|
150
|
+
*/
|
|
151
|
+
export function clusterCandidateEmbeddings(candidates = [], options = {}) {
|
|
152
|
+
const threshold = typeof options.threshold === 'number' ? options.threshold : 0.88
|
|
153
|
+
const fallbackToJudgeOnSplit = options.fallbackToJudgeOnSplit !== false
|
|
154
|
+
const minMajorityRatio = typeof options.minMajorityRatio === 'number' ? options.minMajorityRatio : 0.60
|
|
155
|
+
|
|
156
|
+
if (!Array.isArray(candidates) || candidates.length < 2) {
|
|
157
|
+
return {
|
|
158
|
+
consensusReached: false,
|
|
159
|
+
threshold,
|
|
160
|
+
clusters: [],
|
|
161
|
+
dominantCluster: null,
|
|
162
|
+
centroidCandidate: null,
|
|
163
|
+
centroidIndex: 0,
|
|
164
|
+
similarityMatrix: [],
|
|
165
|
+
fallbackToJudge: fallbackToJudgeOnSplit,
|
|
166
|
+
reason: 'Insufficient candidates (need at least 2)',
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const n = candidates.length
|
|
171
|
+
const { vectors, matrix } = buildSimilarityMatrix(candidates)
|
|
172
|
+
|
|
173
|
+
// Threshold clustering: connect candidates with pairwise similarity >= threshold
|
|
174
|
+
const clusters = []
|
|
175
|
+
const assigned = new Set()
|
|
176
|
+
|
|
177
|
+
for (let i = 0; i < n; i++) {
|
|
178
|
+
if (assigned.has(i)) continue
|
|
179
|
+
const clusterMembers = [i]
|
|
180
|
+
assigned.add(i)
|
|
181
|
+
|
|
182
|
+
// Find all peers reachable with similarity >= threshold
|
|
183
|
+
for (let j = 0; j < n; j++) {
|
|
184
|
+
if (assigned.has(j)) continue
|
|
185
|
+
// Qualifies if similarity to cluster seed or average to cluster >= threshold
|
|
186
|
+
const simToSeed = matrix[i][j]
|
|
187
|
+
const avgSim = clusterMembers.reduce((sum, idx) => sum + matrix[j][idx], 0) / clusterMembers.length
|
|
188
|
+
if (simToSeed >= threshold || avgSim >= threshold) {
|
|
189
|
+
clusterMembers.push(j)
|
|
190
|
+
assigned.add(j)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
clusters.push(clusterMembers)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Find dominant cluster (maximum members)
|
|
197
|
+
clusters.sort((a, b) => b.length - a.length)
|
|
198
|
+
const largestCluster = clusters[0] || []
|
|
199
|
+
|
|
200
|
+
// Compute intra-cluster average similarity
|
|
201
|
+
let intraClusterSimSum = 0
|
|
202
|
+
let pairCount = 0
|
|
203
|
+
for (let a = 0; a < largestCluster.length; a++) {
|
|
204
|
+
for (let b = a + 1; b < largestCluster.length; b++) {
|
|
205
|
+
intraClusterSimSum += matrix[largestCluster[a]][largestCluster[b]]
|
|
206
|
+
pairCount++
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const avgIntraSimilarity = pairCount > 0 ? Number((intraClusterSimSum / pairCount).toFixed(4)) : 1.0
|
|
210
|
+
|
|
211
|
+
// Majority condition: cluster has >= required members and high internal similarity
|
|
212
|
+
const requiredCount = Math.max(2, Math.ceil(n * minMajorityRatio))
|
|
213
|
+
const consensusReached = largestCluster.length >= requiredCount && (avgIntraSimilarity >= threshold || largestCluster.length === n)
|
|
214
|
+
|
|
215
|
+
let centroidIndex = -1
|
|
216
|
+
let centroidCandidate = null
|
|
217
|
+
|
|
218
|
+
if (consensusReached) {
|
|
219
|
+
// Select centroid: member with highest average similarity to peers in the dominant cluster
|
|
220
|
+
let bestAvgSim = -1
|
|
221
|
+
for (const idx of largestCluster) {
|
|
222
|
+
let simTotal = 0
|
|
223
|
+
for (const peerIdx of largestCluster) {
|
|
224
|
+
if (idx !== peerIdx) simTotal += matrix[idx][peerIdx]
|
|
225
|
+
}
|
|
226
|
+
const candidateAvg = largestCluster.length > 1 ? simTotal / (largestCluster.length - 1) : 1.0
|
|
227
|
+
|
|
228
|
+
// Favor syntactically valid candidates in tie/near-tie situations
|
|
229
|
+
const hasSyntaxError = candidates[idx].syntaxValidation?.valid === false
|
|
230
|
+
const adjustedScore = candidateAvg - (hasSyntaxError ? 0.05 : 0)
|
|
231
|
+
|
|
232
|
+
if (adjustedScore > bestAvgSim) {
|
|
233
|
+
bestAvgSim = adjustedScore
|
|
234
|
+
centroidIndex = idx
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
centroidCandidate = candidates[centroidIndex] || null
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const dominantClusterInfo = {
|
|
242
|
+
size: largestCluster.length,
|
|
243
|
+
memberIndices: largestCluster.map((i) => candidates[i].index ?? (i + 1)),
|
|
244
|
+
avgSimilarity: avgIntraSimilarity,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
consensusReached,
|
|
249
|
+
threshold,
|
|
250
|
+
dominantCluster: dominantClusterInfo,
|
|
251
|
+
clusters: clusters.map((c, id) => ({
|
|
252
|
+
id,
|
|
253
|
+
size: c.length,
|
|
254
|
+
indices: c.map((i) => candidates[i].index ?? (i + 1)),
|
|
255
|
+
})),
|
|
256
|
+
centroidCandidate,
|
|
257
|
+
centroidIndex: centroidCandidate ? (centroidCandidate.index ?? (centroidIndex + 1)) : 0,
|
|
258
|
+
similarityMatrix: matrix,
|
|
259
|
+
fallbackToJudge: !consensusReached && fallbackToJudgeOnSplit,
|
|
260
|
+
reason: consensusReached
|
|
261
|
+
? `Blind embedding consensus reached (${largestCluster.length}/${n} models converged with avg similarity ${avgIntraSimilarity} >= ${threshold})`
|
|
262
|
+
: `No dominant embedding cluster converged (largest has ${largestCluster.length}/${n} with avg sim ${avgIntraSimilarity} < ${threshold})`,
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Estimates saved tokens and USD cost from bypassing the judge round.
|
|
268
|
+
* @param {Array<object>} candidates
|
|
269
|
+
* @param {string} userPrompt
|
|
270
|
+
* @param {number} maxTokens
|
|
271
|
+
* @param {object} prices
|
|
272
|
+
* @returns {{ skippedTokens: number, skippedCostUsd: number }}
|
|
273
|
+
*/
|
|
274
|
+
export function estimateSkippedJudgeUsage(candidates = [], userPrompt = '', maxTokens = 4096, prices = {}) {
|
|
275
|
+
const candidateChars = candidates.reduce((acc, c) => acc + (c.text?.length || 0), 0)
|
|
276
|
+
const promptChars = userPrompt.length
|
|
277
|
+
const estimatedInputTokens = Math.round((candidateChars + promptChars) / 3.8)
|
|
278
|
+
const estimatedOutputTokens = Math.min(maxTokens, 1500)
|
|
279
|
+
const skippedTokens = estimatedInputTokens + estimatedOutputTokens
|
|
280
|
+
|
|
281
|
+
const costEst = estimateTokenCost({
|
|
282
|
+
provider: 'default',
|
|
283
|
+
model: 'judge',
|
|
284
|
+
inputTokens: estimatedInputTokens,
|
|
285
|
+
outputTokens: estimatedOutputTokens,
|
|
286
|
+
prices,
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
skippedTokens,
|
|
291
|
+
skippedCostUsd: Number((costEst?.costUsd || 0.005).toFixed(5)),
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Evaluates candidate responses for blind embedding clustering and executes fast zero-LLM consensus if reached.
|
|
297
|
+
* @param {object} params
|
|
298
|
+
* @returns {Promise<{ ok: boolean, consensusReached: boolean, payload?: object, error?: Error, abort?: boolean }>}
|
|
299
|
+
*/
|
|
300
|
+
export async function stageEmbeddingClustering({
|
|
301
|
+
successfulRefs = [],
|
|
302
|
+
referenceOutputs = [],
|
|
303
|
+
preset = {},
|
|
304
|
+
userPrompt = '',
|
|
305
|
+
prices = {},
|
|
306
|
+
maxTokens = 4096,
|
|
307
|
+
cwd = process.cwd(),
|
|
308
|
+
runId = '',
|
|
309
|
+
signal = null,
|
|
310
|
+
promoteOpts = {},
|
|
311
|
+
allowCandidateOverride = false,
|
|
312
|
+
isRefinement = false,
|
|
313
|
+
historyFilePath = null,
|
|
314
|
+
startTime = Date.now(),
|
|
315
|
+
collectedCtx = null,
|
|
316
|
+
liveCanvas = null,
|
|
317
|
+
ctx = null,
|
|
318
|
+
}) {
|
|
319
|
+
const enabled = Boolean(preset?.embedding_clustering_enabled || preset?.embeddingClusteringEnabled)
|
|
320
|
+
const threshold = typeof (preset?.embedding_consensus_threshold ?? preset?.embeddingConsensusThreshold) === 'number'
|
|
321
|
+
? (preset.embedding_consensus_threshold ?? preset.embeddingConsensusThreshold)
|
|
322
|
+
: 0.88
|
|
323
|
+
const fallbackToJudgeOnSplit = (preset?.fallback_to_judge_on_split ?? preset?.fallbackToJudgeOnSplit) !== false
|
|
324
|
+
|
|
325
|
+
if (!enabled || successfulRefs.length < 2) {
|
|
326
|
+
return { ok: true, consensusReached: false }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const result = clusterCandidateEmbeddings(successfulRefs, {
|
|
330
|
+
threshold,
|
|
331
|
+
fallbackToJudgeOnSplit,
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
emitEvent(ctx, MOA_EVENTS.EMBEDDING_CLUSTER, {
|
|
335
|
+
runId,
|
|
336
|
+
consensusReached: result.consensusReached,
|
|
337
|
+
threshold,
|
|
338
|
+
dominantCluster: result.dominantCluster,
|
|
339
|
+
clusters: result.clusters,
|
|
340
|
+
centroidIndex: result.centroidIndex,
|
|
341
|
+
fallbackToJudge: result.fallbackToJudge,
|
|
342
|
+
reason: result.reason,
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
if (!result.consensusReached || !result.centroidCandidate) {
|
|
346
|
+
logger.info(`[dsh-moa] Embedding clustering: ${result.reason}. Proceeding to arbitration.`)
|
|
347
|
+
return { ok: true, consensusReached: false, fallbackToJudge: result.fallbackToJudge }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const winner = result.centroidCandidate
|
|
351
|
+
logger.info(`[dsh-moa] Blind embedding fast consensus reached! Winner centroid: candidate ${result.centroidIndex}`)
|
|
352
|
+
|
|
353
|
+
// Promote centroid candidate files to project workspace
|
|
354
|
+
let promotedFiles = []
|
|
355
|
+
let promoteErr = null
|
|
356
|
+
if (winner.files?.length > 0) {
|
|
357
|
+
try {
|
|
358
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, winner.index, {
|
|
359
|
+
...promoteOpts,
|
|
360
|
+
runId,
|
|
361
|
+
keepMoa: allowCandidateOverride,
|
|
362
|
+
})
|
|
363
|
+
} catch (err) {
|
|
364
|
+
promoteErr = err
|
|
365
|
+
}
|
|
366
|
+
} else if (!allowCandidateOverride) {
|
|
367
|
+
await cleanMoaWorkspaces(cwd, { runId })
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (promoteErr) {
|
|
371
|
+
if (!allowCandidateOverride) await cleanMoaWorkspaces(cwd, { runId })
|
|
372
|
+
if (signal?.aborted || promoteErr?.message?.includes('aborted')) {
|
|
373
|
+
return { ok: false, consensusReached: true, abort: true, error: new Error('Turn aborted') }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const skippedUsage = estimateSkippedJudgeUsage(successfulRefs, userPrompt, maxTokens, prices)
|
|
378
|
+
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs, {
|
|
379
|
+
winningIndex: winner.index,
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
const totalTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0)
|
|
383
|
+
const totalCostUsd = Number(referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0).toFixed(5))
|
|
384
|
+
const durationMs = Date.now() - startTime
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
await recordMoaRunAsync({
|
|
388
|
+
id: runId,
|
|
389
|
+
cwd,
|
|
390
|
+
prompt: userPrompt,
|
|
391
|
+
preset: preset?.name || 'default',
|
|
392
|
+
isRefinement,
|
|
393
|
+
isEarlyConsensus: true,
|
|
394
|
+
isEmbeddingConsensus: true,
|
|
395
|
+
candidates: candidatesForHistory(referenceOutputs),
|
|
396
|
+
aggregator: 'Blind Embedding Consensus (Bypassed)',
|
|
397
|
+
winnerIndex: winner.index,
|
|
398
|
+
winnerModel: winner.label,
|
|
399
|
+
promotedFiles,
|
|
400
|
+
totalTokens,
|
|
401
|
+
totalCostUsd,
|
|
402
|
+
skippedJudgeTokens: skippedUsage.skippedTokens,
|
|
403
|
+
durationMs,
|
|
404
|
+
}, historyFilePath || undefined)
|
|
405
|
+
} catch (histErr) {
|
|
406
|
+
logger.warn('[dsh-moa] Failed to record embedding consensus in history:', histErr?.message || histErr)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
emitEvent(ctx, MOA_EVENTS.RUN_FINISH, {
|
|
410
|
+
runId,
|
|
411
|
+
durationMs,
|
|
412
|
+
totalTokens,
|
|
413
|
+
totalCostUsd,
|
|
414
|
+
promotedFiles,
|
|
415
|
+
winnerIndex: winner.index,
|
|
416
|
+
isEarlyConsensus: true,
|
|
417
|
+
isEmbeddingConsensus: true,
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
ok: true,
|
|
422
|
+
consensusReached: true,
|
|
423
|
+
payload: {
|
|
424
|
+
kind: 'synthesis',
|
|
425
|
+
content: winner.text,
|
|
426
|
+
aggregator: 'Blind Embedding Consensus (Bypassed)',
|
|
427
|
+
references: referenceOutputs,
|
|
428
|
+
presetName: preset?.name || 'default',
|
|
429
|
+
isRefinement,
|
|
430
|
+
isEarlyConsensus: true,
|
|
431
|
+
isEmbeddingConsensus: true,
|
|
432
|
+
winningIndex: winner.index,
|
|
433
|
+
winnerModel: winner.label,
|
|
434
|
+
promotedFiles,
|
|
435
|
+
runId,
|
|
436
|
+
embeddingInfo: {
|
|
437
|
+
dominantCluster: result.dominantCluster,
|
|
438
|
+
threshold,
|
|
439
|
+
clusters: result.clusters,
|
|
440
|
+
centroidIndex: result.centroidIndex,
|
|
441
|
+
},
|
|
442
|
+
...(livePreview ? { liveCanvas: livePreview } : {}),
|
|
443
|
+
usage: {
|
|
444
|
+
totalTokens,
|
|
445
|
+
totalCostUsd,
|
|
446
|
+
skippedJudgeTokens: skippedUsage.skippedTokens,
|
|
447
|
+
skippedJudgeCostUsd: skippedUsage.skippedCostUsd,
|
|
448
|
+
candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
|
|
449
|
+
aggregator: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
450
|
+
},
|
|
451
|
+
skippedFiles: collectedCtx?.skippedFiles || 0,
|
|
452
|
+
skippedList: collectedCtx?.skippedList || [],
|
|
453
|
+
durationMs,
|
|
454
|
+
},
|
|
455
|
+
}
|
|
456
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek Harness Mixture of Agents (MoA) — Cost/Quality Pareto Frontier Slider (#307)
|
|
3
|
+
*
|
|
4
|
+
* Provides reactive calculation of the optimal ensemble trade-off between Cost ($),
|
|
5
|
+
* Quality (Rigorous Consensus), and Latency (seconds) along the Pareto Frontier.
|
|
6
|
+
* Dynamically maps a single slider value (0–100%) to curated model configurations:
|
|
7
|
+
* - 0–25%: Local Free Edge Tier ($0.00 cost, ultra-low latency)
|
|
8
|
+
* - 26–50%: Economic Tier (Low-cost cloud models + deepseek-chat)
|
|
9
|
+
* - 51–75%: Balanced Production Tier (Diverse candidates + deepseek-reasoner / Sonnet)
|
|
10
|
+
* - 76–100%: Ultra-Rigorous Consensus Tier (Flagship ensemble + Multi-Judge panel)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { MOA_EVENTS, emitEvent } from '../events.js'
|
|
14
|
+
import { estimateCandidateRunCost, estimateDownstreamCost } from '../moa-budget.js'
|
|
15
|
+
|
|
16
|
+
export const PARETO_TIERS = [
|
|
17
|
+
{
|
|
18
|
+
maxPos: 25,
|
|
19
|
+
name: 'local_free',
|
|
20
|
+
label: '⚡ 100% Free / Local Edge',
|
|
21
|
+
description: 'Zero API costs. Local models on MiniPC (Qwen 2.5 Coder / Ollama) for maximum speed and privacy.',
|
|
22
|
+
defaultReferences: [
|
|
23
|
+
{ provider: 'ollama', model: 'qwen2.5-coder:7b', role_persona: 'proposer' },
|
|
24
|
+
{ provider: 'ollama', model: 'deepseek-r1:8b', role_persona: 'challenger' },
|
|
25
|
+
],
|
|
26
|
+
defaultAggregator: { provider: 'ollama', model: 'qwen2.5-coder:7b' },
|
|
27
|
+
estimatedCostUsd: 0.0,
|
|
28
|
+
estimatedLatencySec: 4,
|
|
29
|
+
multiJudge: false,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
maxPos: 50,
|
|
33
|
+
name: 'economic',
|
|
34
|
+
label: '💡 Economic Tier',
|
|
35
|
+
description: 'Low-cost fast models. Local edge proposers verified by an economical cloud aggregator.',
|
|
36
|
+
defaultReferences: [
|
|
37
|
+
{ provider: 'ollama', model: 'qwen2.5-coder:7b', role_persona: 'proposer' },
|
|
38
|
+
{ provider: 'deepseek', model: 'deepseek-chat', role_persona: 'challenger' },
|
|
39
|
+
],
|
|
40
|
+
defaultAggregator: { provider: 'deepseek', model: 'deepseek-chat' },
|
|
41
|
+
estimatedCostUsd: 0.002,
|
|
42
|
+
estimatedLatencySec: 7,
|
|
43
|
+
multiJudge: false,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
maxPos: 75,
|
|
47
|
+
name: 'balanced',
|
|
48
|
+
label: '⚖️ Optimal Balanced Tier',
|
|
49
|
+
description: 'High hypothesis diversity with reasoning models and flagship aggregator.',
|
|
50
|
+
defaultReferences: [
|
|
51
|
+
{ provider: 'deepseek', model: 'deepseek-chat', role_persona: 'proposer' },
|
|
52
|
+
{ provider: 'deepseek', model: 'deepseek-reasoner', role_persona: 'challenger' },
|
|
53
|
+
{ provider: 'ollama', model: 'qwen2.5-coder:7b', role_persona: 'minimalist' },
|
|
54
|
+
],
|
|
55
|
+
defaultAggregator: { provider: 'deepseek', model: 'deepseek-reasoner' },
|
|
56
|
+
estimatedCostUsd: 0.009,
|
|
57
|
+
estimatedLatencySec: 12,
|
|
58
|
+
multiJudge: false,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
maxPos: 100,
|
|
62
|
+
name: 'ultra_rigorous',
|
|
63
|
+
label: '🏆 Ultra-Rigorous Consensus',
|
|
64
|
+
description: 'Maximum accuracy. Flagship reasoning models with multi-judge panel and adversarial peer critique.',
|
|
65
|
+
defaultReferences: [
|
|
66
|
+
{ provider: 'deepseek', model: 'deepseek-reasoner', role_persona: 'proposer' },
|
|
67
|
+
{ provider: 'anthropic', model: 'claude-3-5-sonnet', role_persona: 'challenger' },
|
|
68
|
+
{ provider: 'openai', model: 'gpt-4o', role_persona: 'adversary' },
|
|
69
|
+
],
|
|
70
|
+
defaultAggregator: { provider: 'deepseek', model: 'deepseek-reasoner' },
|
|
71
|
+
estimatedCostUsd: 0.038,
|
|
72
|
+
estimatedLatencySec: 22,
|
|
73
|
+
multiJudge: true,
|
|
74
|
+
},
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Returns the Pareto tier definition for a given slider position (0-100).
|
|
79
|
+
*
|
|
80
|
+
* @param {number} position
|
|
81
|
+
* @returns {object} Tier configuration
|
|
82
|
+
*/
|
|
83
|
+
export function getParetoProfile(position = 50) {
|
|
84
|
+
const clamped = Math.max(0, Math.min(100, Number(position) || 0))
|
|
85
|
+
for (const tier of PARETO_TIERS) {
|
|
86
|
+
if (clamped <= tier.maxPos) {
|
|
87
|
+
return { ...tier, position: clamped }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { ...PARETO_TIERS[PARETO_TIERS.length - 1], position: clamped }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Calculates dynamically predicted cost and latency for an ensemble along the Pareto Frontier.
|
|
95
|
+
*
|
|
96
|
+
* @param {number} sliderPos
|
|
97
|
+
* @param {Array<object>} [availableModels]
|
|
98
|
+
* @param {object} [options]
|
|
99
|
+
* @returns {object} Ensemble recommendation
|
|
100
|
+
*/
|
|
101
|
+
export function calculateParetoEnsemble(sliderPos = 50, availableModels = [], options = {}) {
|
|
102
|
+
const profile = getParetoProfile(sliderPos)
|
|
103
|
+
const { prices = {}, promptTokens = 1500, expectedOutputTokens = 2048 } = options
|
|
104
|
+
|
|
105
|
+
// Calculate upfront cost
|
|
106
|
+
const candCost = estimateCandidateRunCost(profile.defaultReferences, prices, promptTokens, expectedOutputTokens)
|
|
107
|
+
const downstreamCost = estimateDownstreamCost(
|
|
108
|
+
profile.defaultAggregator,
|
|
109
|
+
profile.multiJudge ? profile.defaultReferences : [],
|
|
110
|
+
profile.defaultReferences.length,
|
|
111
|
+
prices,
|
|
112
|
+
expectedOutputTokens,
|
|
113
|
+
promptTokens
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
const calculatedCostUsd = profile.name === 'local_free'
|
|
117
|
+
? 0.0
|
|
118
|
+
: Number((candCost.totalEstimatedUsd + downstreamCost).toFixed(5))
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
tier: profile.name,
|
|
122
|
+
label: profile.label,
|
|
123
|
+
description: profile.description,
|
|
124
|
+
sliderPosition: profile.position,
|
|
125
|
+
estimatedCostUsd: calculatedCostUsd > 0 ? calculatedCostUsd : profile.estimatedCostUsd,
|
|
126
|
+
estimatedLatencySec: profile.estimatedLatencySec,
|
|
127
|
+
recommendedReferences: profile.defaultReferences,
|
|
128
|
+
recommendedAggregator: profile.defaultAggregator,
|
|
129
|
+
multiJudgeEnabled: profile.multiJudge,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Adapts preset configuration based on Pareto slider position if enabled.
|
|
135
|
+
*
|
|
136
|
+
* @param {object} preset
|
|
137
|
+
* @param {object} [options]
|
|
138
|
+
* @returns {object} Adjusted preset
|
|
139
|
+
*/
|
|
140
|
+
export function applyParetoAdjustment(preset, options = {}) {
|
|
141
|
+
if (!preset?.pareto_slider_enabled) return preset
|
|
142
|
+
|
|
143
|
+
const { ctx, runId, onProgress } = options
|
|
144
|
+
const position = preset.pareto_slider_position ?? 50
|
|
145
|
+
const recommendation = calculateParetoEnsemble(position, options.availableModels || [], options)
|
|
146
|
+
|
|
147
|
+
if (typeof onProgress === 'function') {
|
|
148
|
+
onProgress(`📈 *Pareto Frontier*: Active profile ${recommendation.label} (~$${recommendation.estimatedCostUsd}, ~${recommendation.estimatedLatencySec}s)\n`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (ctx) {
|
|
152
|
+
emitEvent(ctx, MOA_EVENTS.PARETO_ADJUSTED, {
|
|
153
|
+
runId,
|
|
154
|
+
position,
|
|
155
|
+
tier: recommendation.tier,
|
|
156
|
+
estimatedCostUsd: recommendation.estimatedCostUsd,
|
|
157
|
+
estimatedLatencySec: recommendation.estimatedLatencySec,
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
...preset,
|
|
163
|
+
reference_models: recommendation.recommendedReferences,
|
|
164
|
+
aggregator: recommendation.recommendedAggregator,
|
|
165
|
+
multi_judge_enabled: recommendation.multiJudgeEnabled || preset.multi_judge_enabled,
|
|
166
|
+
}
|
|
167
|
+
}
|