vibeoscore 1.0.2 → 1.0.9

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.
Files changed (45) hide show
  1. package/client.js +1 -0
  2. package/client.ts +2 -0
  3. package/lib/logger.js +27 -0
  4. package/mcp-server.js +5 -4
  5. package/mcp-server.ts +4 -3
  6. package/package.json +12 -10
  7. package/dashboard/dist/assets/index-BnPt1Fii.js +0 -1
  8. package/dashboard/dist/assets/index-CfH00tOL.css +0 -1
  9. package/dashboard/dist/index.html +0 -3
  10. package/lib/blackbox-rf.js +0 -1099
  11. package/lib/blackbox.js +0 -137
  12. package/lib/compression.js +0 -119
  13. package/lib/db.js +0 -106
  14. package/lib/db.ts +0 -113
  15. package/lib/delegation.js +0 -137
  16. package/lib/meta-controller.js +0 -418
  17. package/lib/meta-controller.mjs +0 -499
  18. package/lib/patterns.js +0 -150
  19. package/lib/resolution-tracker.js +0 -486
  20. package/lib/stress.js +0 -84
  21. package/lib/tdd.js +0 -218
  22. package/lib/tier-routing.js +0 -48
  23. package/middleware/auth.js +0 -75
  24. package/middleware/auth.ts +0 -87
  25. package/middleware/usage-logging.js +0 -29
  26. package/middleware/usage-logging.ts +0 -41
  27. package/nginx-vibetheog-api.conf +0 -64
  28. package/routes/admin.js +0 -93
  29. package/routes/admin.ts +0 -107
  30. package/routes/blackbox.js +0 -463
  31. package/routes/compression.js +0 -12
  32. package/routes/delegation.js +0 -30
  33. package/routes/patterns.js +0 -53
  34. package/routes/pricing.js +0 -62
  35. package/routes/stress.js +0 -30
  36. package/routes/tdd.js +0 -68
  37. package/routes/tier-routing.js +0 -31
  38. package/scripts/dashboard-server.mjs +0 -246
  39. package/scripts/deploy-zero-downtime.sh +0 -77
  40. package/scripts/deploy.sh +0 -68
  41. package/scripts/release.mjs +0 -30
  42. package/scripts/seed-master-token.js +0 -29
  43. package/scripts/start-all.mjs +0 -34
  44. package/server.js +0 -88
  45. package/vibeos-api.service +0 -19
@@ -1,1099 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"
2
- import { dirname, resolve } from "node:path"
3
- import { fileURLToPath } from "node:url"
4
- import { getDb } from "./db.js"
5
- import { ResolutionTracker, extractFeatures } from "./blackbox.js"
6
- import { scoreStress } from "./stress.js"
7
- import { autoSelectMode } from "./meta-controller.js"
8
-
9
- const __dirname = dirname(fileURLToPath(import.meta.url))
10
- const MODEL_PATH = process.env.VIBEOS_BLACKBOX_ROUTING_MODEL_PATH || resolve(__dirname, "..", "data", "blackbox-routing-model.json")
11
- const MODE_CLASSES = ["budget", "audit", "speed", "quality", "longrun"]
12
- const DEFAULT_CONFIDENCE_THRESHOLD = 0.55
13
- const DEFAULT_TREE_COUNT = 29
14
- const DEFAULT_MAX_DEPTH = 7
15
- const DEFAULT_MIN_SAMPLES_SPLIT = 6
16
- const DEFAULT_MIN_SAMPLES_LEAF = 2
17
- const DEFAULT_FEATURE_SUBSAMPLE = 6
18
-
19
- let cachedModel = null
20
- const cachedGateModels = new Map()
21
-
22
- function num(v, fallback = 0) {
23
- const n = Number(v)
24
- return Number.isFinite(n) ? n : fallback
25
- }
26
-
27
- function modePriority(mode) {
28
- const normalized = String(mode || "").toLowerCase()
29
- const priorities = { budget: 0, audit: 1, speed: 2, longrun: 3, quality: 4 }
30
- return priorities[normalized] ?? 0
31
- }
32
-
33
- function clamp(v, min = 0, max = 1) {
34
- return Math.min(max, Math.max(min, v))
35
- }
36
-
37
- function loadJsonSafe(raw) {
38
- try {
39
- if (!raw) return null
40
- return typeof raw === "string" ? JSON.parse(raw) : raw
41
- } catch {
42
- return null
43
- }
44
- }
45
-
46
- function loadRecentSessionSignals(projectId, sessionId, lookbackHours = 48, limit = 31) {
47
- try {
48
- const db = getDb()
49
- const cutoff = new Date(Date.now() - lookbackHours * 60 * 60 * 1000).toISOString()
50
- const rows = projectId
51
- ? db.prepare(
52
- "SELECT session_id, state_json, outcome, updated_at FROM blackbox_sessions WHERE updated_at >= ? AND project_id = ? ORDER BY updated_at DESC LIMIT ?"
53
- ).all(cutoff, projectId, limit)
54
- : db.prepare(
55
- "SELECT session_id, state_json, outcome, updated_at FROM blackbox_sessions WHERE updated_at >= ? ORDER BY updated_at DESC LIMIT ?"
56
- ).all(cutoff, limit)
57
- const sessions = []
58
- for (const row of rows) {
59
- if (sessionId && row.session_id === sessionId) continue
60
- const parsed = loadJsonSafe(row.state_json)
61
- const telemetry = parsed?.telemetry || parsed?.history?.slice(-1)[0]?.telemetry || null
62
- const signals = telemetry?.signals || {}
63
- const input = telemetry?.input || {}
64
- const controlVector = telemetry?.control_vector || {}
65
- sessions.push({
66
- stress: num(input.latest_stress_multiplier ?? input.stress_multiplier ?? input.stress ?? controlVector.stress_multiplier ?? 0),
67
- repeatRatio: num(signals.action_consistency ?? 0),
68
- loopConsecutive: num(signals.loop_consecutive ?? parsed?.loopCount ?? 0),
69
- isLooping: Boolean(signals.is_looping || signals.sub_regime === "LOOPING"),
70
- outcome: String(signals.outcome || row.outcome || parsed?.outcome || "").toLowerCase(),
71
- })
72
- }
73
- if (!sessions.length) return null
74
- const recent = sessions.slice(0, Math.min(5, sessions.length))
75
- const avg = (items, key) => items.reduce((sum, item) => sum + num(item[key]), 0) / Math.max(items.length, 1)
76
- const peak = (items, key) => items.reduce((max, item) => Math.max(max, num(item[key])), 0)
77
- return {
78
- sampleCount: sessions.length,
79
- recentStressAvg: avg(recent, "stress"),
80
- recentStressPeak: peak(recent, "stress"),
81
- repeatRatio: avg(sessions, "repeatRatio"),
82
- loopRate: sessions.filter((item) => item.isLooping || item.loopConsecutive >= 3).length / sessions.length,
83
- negativeRate: sessions.filter((item) => item.outcome === "negative").length / sessions.length,
84
- anomalyRate: sessions.filter((item) => item.stress >= 0.45 || item.isLooping || item.outcome === "negative").length / sessions.length,
85
- }
86
- } catch {
87
- return null
88
- }
89
- }
90
-
91
- function stableHash(str) {
92
- const s = String(str || "")
93
- let hash = 2166136261
94
- for (let i = 0; i < s.length; i++) {
95
- hash ^= s.charCodeAt(i)
96
- hash = Math.imul(hash, 16777619)
97
- }
98
- return hash >>> 0
99
- }
100
-
101
- function createRng(seed) {
102
- let state = seed >>> 0
103
- return () => {
104
- state = (state + 0x6D2B79F5) | 0
105
- let t = Math.imul(state ^ (state >>> 15), 1 | state)
106
- t ^= t + Math.imul(t ^ (t >>> 7), 61 | t)
107
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296
108
- }
109
- }
110
-
111
- function shuffleInPlace(items, rng) {
112
- for (let i = items.length - 1; i > 0; i--) {
113
- const j = Math.floor(rng() * (i + 1))
114
- ;[items[i], items[j]] = [items[j], items[i]]
115
- }
116
- return items
117
- }
118
-
119
- function ensureModelDir() {
120
- mkdirSync(dirname(MODEL_PATH), { recursive: true })
121
- }
122
-
123
- function normalizeGateMode(targetMode) {
124
- return String(targetMode || "").toLowerCase().trim()
125
- }
126
-
127
- function getModeGatePath(targetMode) {
128
- const gateMode = normalizeGateMode(targetMode)
129
- return resolve(__dirname, "..", "data", "mode-gates", `${gateMode || "mode"}-gate.json`)
130
- }
131
-
132
- function ensureModeGateDir(targetMode) {
133
- mkdirSync(dirname(getModeGatePath(targetMode)), { recursive: true })
134
- }
135
-
136
- function oneHotIndex(value) {
137
- const idx = MODE_CLASSES.indexOf(String(value || "").toLowerCase())
138
- return idx >= 0 ? idx : 0
139
- }
140
-
141
- function regimeIndex(value) {
142
- const map = {
143
- INIT: 0,
144
- DIVERGENT: 1,
145
- EXPLORING: 2,
146
- REFINING: 3,
147
- CONVERGING: 4,
148
- CLOSED: 5,
149
- LOOPING: 6,
150
- }
151
- return map[String(value || "").toUpperCase()] ?? 0
152
- }
153
-
154
- function resolutionIndex(value) {
155
- const map = {
156
- unresolved: 0,
157
- looping: 1,
158
- converging: 2,
159
- solved: 3,
160
- }
161
- return map[String(value || "").toLowerCase()] ?? 0
162
- }
163
-
164
- function continuityIndex(value) {
165
- const map = {
166
- HIGH: 1,
167
- MEDIUM: 0.6,
168
- LOW: 0.2,
169
- }
170
- return map[String(value || "").toUpperCase()] ?? 0.5
171
- }
172
-
173
- function loopInterventionIndex(value) {
174
- const map = {
175
- none: 0,
176
- gentle: 1,
177
- suggestive: 2,
178
- assertive: 3,
179
- escalated: 4,
180
- }
181
- return map[String(value || "").toLowerCase()] ?? 0
182
- }
183
-
184
- function buildPromptSignals(text, explicit = {}) {
185
- const raw = extractFeatures(text)
186
- const lower = String(text || "").toLowerCase()
187
- const words = lower.split(/\s+/).filter(Boolean)
188
- const fileMentions = (lower.match(/(?:^|[\s"'(])\.{0,2}\/[a-z0-9._/-]+|\.(?:js|ts|tsx|jsx|py|rs|go|java|cpp|c|h|json|yaml|yml|toml|sql|css|html|md)\b|package\.json|tsconfig\.json|dockerfile|makefile|docker-compose/gi) || []).length
189
- const errorSignals = (lower.match(/bug|error|fail|crash|broken|wrong|incorrect|issue|problem|exception|traceback|segfault|race|deadlock|leak|corrupt/gi) || []).length
190
- const actionWords = new Set(["check", "find", "list", "search", "look", "count", "show", "get", "read", "grep", "scan", "detect", "inspect", "implement", "refactor", "migrate", "redesign", "optimize", "debug", "fix", "resolve", "patch", "build", "deploy", "integrate", "benchmark", "profile", "secure", "harden", "audit", "design", "create", "generate", "transform", "convert", "setup", "configure", "provision", "bootstrap"])
191
- let actionDensity = 0
192
- for (const word of words.slice(0, 8)) {
193
- if (actionWords.has(word)) actionDensity += 0.15
194
- }
195
- actionDensity = clamp(actionDensity)
196
- const complexityPattern = /multi.*(?:file|module|step|stage|phase|tenant|region|thread|process)|concurrent|async|parallel|distributed|replicated|shard|cluster|microservice|framework|database|schema|migration|backward.*compat|breaking.*change|api.*(?:version|breaking)|protocol|encoding|serializ/i
197
- let complexityCount = 0
198
- for (const word of words) {
199
- if (complexityPattern.test(word)) complexityCount++
200
- }
201
- const prompt = {
202
- length: num(raw.length),
203
- word_count: num(raw.word_count),
204
- sentence_count: num(raw.sentence_count),
205
- avg_word_length: num(raw.avg_word_length),
206
- question_ratio: num(raw.question_ratio),
207
- code_blocks: num(raw.code_blocks),
208
- urgency: num(raw.urgency),
209
- repetition: num(raw.repetition),
210
- sentiment: num(raw.sentiment),
211
- complexity: num(raw.complexity),
212
- instruction_density: num(raw.instruction_density),
213
- file_mentions: num(explicit.file_mentions ?? fileMentions),
214
- error_signals: num(explicit.error_signals ?? errorSignals),
215
- action_density: num(explicit.action_density ?? actionDensity),
216
- complexity_words: num(explicit.complexity_words ?? complexityCount),
217
- stress: num(explicit.stress ?? scoreStress(text)),
218
- }
219
- return prompt
220
- }
221
-
222
- function buildFeatureVector(text, context = {}) {
223
- const prompt = buildPromptSignals(text, context.prompt_features || context.features || {})
224
- const state = context.state || {}
225
- return [
226
- prompt.length,
227
- prompt.word_count,
228
- prompt.sentence_count,
229
- prompt.avg_word_length,
230
- prompt.question_ratio,
231
- prompt.code_blocks,
232
- prompt.urgency,
233
- prompt.repetition,
234
- prompt.sentiment,
235
- prompt.complexity,
236
- prompt.instruction_density,
237
- prompt.file_mentions,
238
- prompt.error_signals,
239
- prompt.action_density,
240
- prompt.complexity_words,
241
- prompt.stress,
242
- num(state.momentum),
243
- num(state.signals?.action_consistency),
244
- num(state.signals?.entropy_trend),
245
- num(state.signals?.feature_contradiction),
246
- num(state.signals?.embedding_delta),
247
- num(state.pivot_score),
248
- num(state.n_interactions),
249
- num(state.loop_consecutive ?? state.loop_count),
250
- regimeIndex(state.sub_regime),
251
- resolutionIndex(state.resolution),
252
- continuityIndex(state.continuity_state),
253
- loopInterventionIndex(state.loop_intervention_level),
254
- state.is_looping ? 1 : 0,
255
- num(state.outcome === "positive" ? 1 : state.outcome === "negative" ? -1 : 0),
256
- ]
257
- }
258
-
259
- function bootstrapSamples() {
260
- const scenarios = [
261
- { text: "hi", state: { sub_regime: "INIT", resolution: "unresolved", continuity_state: "HIGH", momentum: 0.05, n_interactions: 1 }, mode: "budget" },
262
- { text: "just give me the quick answer", state: { sub_regime: "INIT", resolution: "unresolved", continuity_state: "HIGH", momentum: 0.06, n_interactions: 1 }, mode: "budget" },
263
- { text: "show me the current status", state: { sub_regime: "EXPLORING", resolution: "unresolved", continuity_state: "HIGH", momentum: 0.18, n_interactions: 2 }, mode: "audit" },
264
- { text: "inspect the repo and list files", state: { sub_regime: "EXPLORING", resolution: "unresolved", continuity_state: "HIGH", momentum: 0.14, n_interactions: 2 }, mode: "audit" },
265
- { text: "can you inspect this error trace and explain what is wrong?", state: { sub_regime: "REFINING", resolution: "unresolved", continuity_state: "MEDIUM", momentum: 0.42, n_interactions: 3 }, mode: "audit" },
266
- { text: "please review the config and compare the current behavior", state: { sub_regime: "EXPLORING", resolution: "unresolved", continuity_state: "HIGH", momentum: 0.16, n_interactions: 2 }, mode: "audit" },
267
- { text: "this is broken now fix it immediately", state: { sub_regime: "REFINING", resolution: "unresolved", continuity_state: "MEDIUM", momentum: 0.38, n_interactions: 4 }, mode: "quality" },
268
- { text: "help me debug this failing test", state: { sub_regime: "REFINING", resolution: "unresolved", continuity_state: "MEDIUM", momentum: 0.31, n_interactions: 4 }, mode: "quality" },
269
- { text: "let's wrap this up and ship the final change", state: { sub_regime: "CONVERGING", resolution: "converging", continuity_state: "MEDIUM", momentum: 0.67, n_interactions: 8 }, mode: "quality" },
270
- { text: "finalize the release and verify the patch before closing", state: { sub_regime: "CLOSED", resolution: "solved", continuity_state: "MEDIUM", momentum: 0.74, n_interactions: 9 }, mode: "quality" },
271
- { text: "we are repeating the same solution again and again", state: { sub_regime: "LOOPING", resolution: "looping", continuity_state: "LOW", momentum: 0.12, loop_consecutive: 3, is_looping: true, n_interactions: 6 }, mode: "speed" },
272
- { text: "why does this keep looping?", state: { sub_regime: "LOOPING", resolution: "looping", continuity_state: "LOW", momentum: 0.09, loop_consecutive: 2, is_looping: true, n_interactions: 5 }, mode: "speed" },
273
- { text: "we already tried that, give me a shorter path", state: { sub_regime: "LOOPING", resolution: "looping", continuity_state: "LOW", momentum: 0.11, loop_consecutive: 4, is_looping: true, n_interactions: 7 }, mode: "speed" },
274
- { text: "I need a complete investigation with careful reasoning", state: { sub_regime: "RESEARCH", resolution: "unresolved", continuity_state: "HIGH", momentum: 0.24, n_interactions: 5 }, mode: "longrun" },
275
- { text: "research the right documentation and compare the API behavior", state: { sub_regime: "RESEARCH", resolution: "unresolved", continuity_state: "HIGH", momentum: 0.22, n_interactions: 5 }, mode: "longrun" },
276
- { text: "trace the issue and gather evidence from the logs", state: { sub_regime: "RESEARCH", resolution: "unresolved", continuity_state: "HIGH", momentum: 0.27, n_interactions: 6 }, mode: "longrun" },
277
- ]
278
- return scenarios.map((scenario) => ({
279
- features: buildFeatureVector(scenario.text, { state: scenario.state }),
280
- label: scenario.mode,
281
- source: "bootstrap",
282
- }))
283
- }
284
-
285
- function expandSyntheticSamples(baseSamples, seed = 42, repeats = 4) {
286
- const variants = []
287
- const rng = createRng(seed)
288
- const prefixes = [
289
- "",
290
- "please ",
291
- "can you ",
292
- "we need to ",
293
- "now ",
294
- ]
295
- const suffixes = [
296
- "",
297
- " with the same intent",
298
- " and keep the scope narrow",
299
- " without changing unrelated files",
300
- " with a careful review",
301
- ]
302
- const actionHints = {
303
- budget: ["quickly", "briefly", "lightweight", "simple"],
304
- audit: ["inspect", "review", "check", "list"],
305
- quality: ["carefully", "thoroughly", "fix", "validate"],
306
- speed: ["fast", "short", "concise", "minimal"],
307
- longrun: ["investigate", "analyze", "research", "trace"],
308
- }
309
-
310
- for (const sample of baseSamples) {
311
- variants.push(sample)
312
- const state = sample.state || {}
313
- const label = sample.label
314
- const hints = actionHints[label] || ["review"]
315
- for (let i = 0; i < repeats; i++) {
316
- const prefix = prefixes[Math.floor(rng() * prefixes.length)]
317
- const suffix = suffixes[Math.floor(rng() * suffixes.length)]
318
- const hint = hints[Math.floor(rng() * hints.length)]
319
- const text = `${prefix}${hint} the task${suffix}`.trim()
320
- const syntheticState = {
321
- ...state,
322
- momentum: clamp(num(state.momentum) + (rng() - 0.5) * 0.08, 0, 1),
323
- n_interactions: Math.max(1, Math.round(num(state.n_interactions || 1) + (rng() - 0.5) * 2)),
324
- loop_consecutive: Math.max(0, Math.round(num(state.loop_consecutive || 0) + (rng() - 0.5) * 2)),
325
- }
326
- if (label === "speed") {
327
- syntheticState.is_looping = true
328
- syntheticState.resolution = "looping"
329
- syntheticState.continuity_state = "LOW"
330
- } else if (label === "quality") {
331
- syntheticState.resolution = "converging"
332
- syntheticState.continuity_state = "MEDIUM"
333
- } else if (label === "longrun") {
334
- syntheticState.resolution = "unresolved"
335
- syntheticState.continuity_state = "HIGH"
336
- }
337
- variants.push({
338
- features: buildFeatureVector(text, { state: syntheticState }),
339
- label,
340
- source: "synthetic",
341
- })
342
- }
343
- }
344
- return variants
345
- }
346
-
347
- function splitMetricsBySource(model, samples) {
348
- const grouped = {}
349
- for (const sample of samples) {
350
- const source = sample.source || "unknown"
351
- if (!grouped[source]) {
352
- grouped[source] = { total: 0, correct: 0 }
353
- }
354
- const pred = predictBlackboxRoutingModel(model, sample.features)
355
- grouped[source].total++
356
- if (pred.label === sample.label) grouped[source].correct++
357
- }
358
- const out = {}
359
- for (const [source, bucket] of Object.entries(grouped)) {
360
- out[source] = {
361
- accuracy: bucket.total > 0 ? bucket.correct / bucket.total : 0,
362
- samples: bucket.total,
363
- }
364
- }
365
- return out
366
- }
367
-
368
- function loadJson(filePath) {
369
- try {
370
- if (!existsSync(filePath)) return null
371
- return JSON.parse(readFileSync(filePath, "utf-8"))
372
- } catch {
373
- return null
374
- }
375
- }
376
-
377
- function saveJson(filePath, data) {
378
- ensureModelDir()
379
- const tmp = `${filePath}.tmp.${Date.now()}`
380
- writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n")
381
- renameSync(tmp, filePath)
382
- }
383
-
384
- function gini(counts) {
385
- const total = counts.reduce((a, b) => a + b, 0)
386
- if (total <= 0) return 0
387
- let sum = 0
388
- for (const c of counts) {
389
- const p = c / total
390
- sum += p * p
391
- }
392
- return 1 - sum
393
- }
394
-
395
- function majorityLabel(samples, classes) {
396
- const counts = new Array(classes.length).fill(0)
397
- for (const sample of samples) {
398
- const idx = classes.indexOf(sample.label)
399
- if (idx >= 0) counts[idx]++
400
- }
401
- let best = 0
402
- for (let i = 1; i < counts.length; i++) {
403
- if (counts[i] > counts[best]) best = i
404
- }
405
- return classes[best] || classes[0] || "budget"
406
- }
407
-
408
- function classCounts(samples, classes) {
409
- const counts = new Array(classes.length).fill(0)
410
- for (const sample of samples) {
411
- const idx = classes.indexOf(sample.label)
412
- if (idx >= 0) counts[idx]++
413
- }
414
- return counts
415
- }
416
-
417
- function pickFeatureSubset(featureCount, subsetSize, rng) {
418
- const indices = [...Array(featureCount).keys()]
419
- shuffleInPlace(indices, rng)
420
- return indices.slice(0, Math.max(1, Math.min(subsetSize, featureCount)))
421
- }
422
-
423
- function bestSplit(samples, featureIndices, classes) {
424
- const parentCounts = classCounts(samples, classes)
425
- const parentGini = gini(parentCounts)
426
- let best = null
427
- for (const featureIndex of featureIndices) {
428
- const sorted = samples
429
- .map((sample) => ({ value: num(sample.features[featureIndex]), label: sample.label, sample }))
430
- .filter((row) => Number.isFinite(row.value))
431
- .sort((a, b) => a.value - b.value)
432
- if (sorted.length < 2) continue
433
- const leftCounts = new Array(classes.length).fill(0)
434
- const rightCounts = parentCounts.slice()
435
- for (let i = 0; i < sorted.length - 1; i++) {
436
- const current = sorted[i]
437
- const classIndex = classes.indexOf(current.label)
438
- if (classIndex >= 0) {
439
- leftCounts[classIndex]++
440
- rightCounts[classIndex]--
441
- }
442
- if (sorted[i].value === sorted[i + 1].value) continue
443
- const leftSize = i + 1
444
- const rightSize = sorted.length - leftSize
445
- if (leftSize < DEFAULT_MIN_SAMPLES_LEAF || rightSize < DEFAULT_MIN_SAMPLES_LEAF) continue
446
- const threshold = (sorted[i].value + sorted[i + 1].value) / 2
447
- const leftGini = gini(leftCounts)
448
- const rightGini = gini(rightCounts)
449
- const weighted = (leftSize / sorted.length) * leftGini + (rightSize / sorted.length) * rightGini
450
- const gain = parentGini - weighted
451
- if (!best || gain > best.gain) {
452
- best = {
453
- gain,
454
- featureIndex,
455
- threshold,
456
- leftLabels: leftCounts.slice(),
457
- rightLabels: rightCounts.slice(),
458
- }
459
- }
460
- }
461
- }
462
- return best
463
- }
464
-
465
- function buildTree(samples, classes, rng, depth = 0, options = {}) {
466
- const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH
467
- const minSamplesSplit = options.minSamplesSplit ?? DEFAULT_MIN_SAMPLES_SPLIT
468
- const minSamplesLeaf = options.minSamplesLeaf ?? DEFAULT_MIN_SAMPLES_LEAF
469
- const featureSubsample = options.featureSubsample ?? DEFAULT_FEATURE_SUBSAMPLE
470
- const counts = classCounts(samples, classes)
471
- const label = majorityLabel(samples, classes)
472
- const total = samples.length
473
- const isPure = counts.some((c) => c === total)
474
- if (depth >= maxDepth || total < minSamplesSplit || isPure) {
475
- return { leaf: true, label, counts }
476
- }
477
- const features = pickFeatureSubset(samples[0].features.length, featureSubsample, rng)
478
- const split = bestSplit(samples, features, classes)
479
- if (!split || split.gain <= 1e-8) {
480
- return { leaf: true, label, counts }
481
- }
482
- const left = []
483
- const right = []
484
- for (const sample of samples) {
485
- const value = num(sample.features[split.featureIndex])
486
- if (value <= split.threshold) left.push(sample)
487
- else right.push(sample)
488
- }
489
- if (left.length < minSamplesLeaf || right.length < minSamplesLeaf) {
490
- return { leaf: true, label, counts }
491
- }
492
- return {
493
- leaf: false,
494
- label,
495
- counts,
496
- featureIndex: split.featureIndex,
497
- threshold: split.threshold,
498
- left: buildTree(left, classes, rng, depth + 1, options),
499
- right: buildTree(right, classes, rng, depth + 1, options),
500
- }
501
- }
502
-
503
- function predictTree(tree, features, classes) {
504
- let node = tree
505
- while (!node.leaf) {
506
- const value = num(features[node.featureIndex])
507
- node = value <= node.threshold ? node.left : node.right
508
- if (!node) break
509
- }
510
- const counts = node?.counts || new Array(classes.length).fill(0)
511
- const total = counts.reduce((a, b) => a + b, 0)
512
- const probs = counts.map((c) => (total > 0 ? c / total : 1 / classes.length))
513
- let bestIndex = 0
514
- for (let i = 1; i < probs.length; i++) {
515
- if (probs[i] > probs[bestIndex]) bestIndex = i
516
- }
517
- return { label: classes[bestIndex] || classes[0] || "budget", probs }
518
- }
519
-
520
- function toClassLabel(mode) {
521
- const normalized = String(mode || "").toLowerCase()
522
- if (MODE_CLASSES.includes(normalized)) return normalized
523
- return "budget"
524
- }
525
-
526
- function deriveLabelFromState(state, stress) {
527
- return toClassLabel(autoSelectMode(state?.sub_regime || "INIT", stress))
528
- }
529
-
530
- function deriveLabelFromTelemetry(telemetry, state, stress) {
531
- const candidates = [
532
- telemetry?.selection?.optimization_mode,
533
- telemetry?.optimization_mode,
534
- telemetry?.control_vector?.optimization_mode,
535
- telemetry?.selected_mode,
536
- telemetry?.mode,
537
- telemetry?.label,
538
- ]
539
- for (const candidate of candidates) {
540
- const normalized = String(candidate || "").toLowerCase()
541
- if (!normalized || normalized === "auto") continue
542
- if (MODE_CLASSES.includes(normalized)) {
543
- return toClassLabel(normalized)
544
- }
545
- }
546
- return deriveLabelFromState(state, stress)
547
- }
548
-
549
- function deriveSamplesFromSessionRow(row) {
550
- const parsed = loadJsonSafe(row.state_json)
551
- if (!parsed || !Array.isArray(parsed.history)) return []
552
- const replay = new ResolutionTracker(row.session_id || "session", parsed.maxHistory || 50)
553
- const samples = []
554
- for (const entry of parsed.history) {
555
- const text = String(entry?.text || "")
556
- const features = entry?.features && typeof entry.features === "object" && !Array.isArray(entry.features)
557
- ? entry.features
558
- : extractFeatures(text)
559
- const state = replay.update({
560
- userText: text,
561
- features,
562
- action: entry?.action || "explore",
563
- entropy: num(entry?.entropy, 1.0),
564
- uncertainty: num(entry?.uncertainty, 50),
565
- embedding: entry?.embedding || null,
566
- })
567
- const telemetry = entry?.telemetry || parsed.telemetry || null
568
- const stress = scoreStress(text)
569
- samples.push({
570
- features: buildFeatureVector(text, { state, prompt_features: features, stress }),
571
- label: deriveLabelFromTelemetry(telemetry, state, stress),
572
- source: "db",
573
- })
574
- }
575
- return samples
576
- }
577
-
578
- export function extractRoutingFeatures(input = {}) {
579
- const text = String(input.user_text || input.prompt || "")
580
- const features = buildFeatureVector(text, {
581
- state: input.state || input,
582
- prompt_features: input.features || input.prompt_features || {},
583
- stress: input.stress_multiplier ?? input.stress_score ?? input.stress ?? scoreStress(text),
584
- })
585
- return features
586
- }
587
-
588
- function evaluateModel(model, samples) {
589
- if (!samples.length) {
590
- return { accuracy: 0, per_class_accuracy: {}, samples: 0, baseline_accuracy: 0 }
591
- }
592
- let correct = 0
593
- const classTotals = Object.fromEntries(model.classes.map((c) => [c, 0]))
594
- const classCorrect = Object.fromEntries(model.classes.map((c) => [c, 0]))
595
- const majority = model.classes.reduce((best, cls) => (model.classCounts?.[cls] || 0) > (model.classCounts?.[best] || 0) ? cls : best, model.classes[0] || "budget")
596
- let baselineCorrect = 0
597
- for (const sample of samples) {
598
- const pred = predictBlackboxRoutingModel(model, sample.features)
599
- if (pred.label === sample.label) {
600
- correct++
601
- classCorrect[sample.label] = (classCorrect[sample.label] || 0) + 1
602
- }
603
- classTotals[sample.label] = (classTotals[sample.label] || 0) + 1
604
- if (majority === sample.label) baselineCorrect++
605
- }
606
- const perClassAccuracy = {}
607
- for (const cls of model.classes) {
608
- perClassAccuracy[cls] = classTotals[cls] ? classCorrect[cls] / classTotals[cls] : 0
609
- }
610
- return {
611
- accuracy: correct / samples.length,
612
- per_class_accuracy: perClassAccuracy,
613
- samples: samples.length,
614
- baseline_accuracy: baselineCorrect / samples.length,
615
- }
616
- }
617
-
618
- function trainRandomForestModel(samples, classes, options = {}, labelMapper = (label) => label) {
619
- const valid = (Array.isArray(samples) ? samples : [])
620
- .filter((sample) => sample && Array.isArray(sample.features) && sample.features.length > 0)
621
- .map((sample) => ({
622
- features: sample.features.map((v) => num(v)),
623
- label: labelMapper(sample.label),
624
- source: sample.source || "unknown",
625
- }))
626
- .filter((sample) => classes.includes(sample.label))
627
-
628
- const rng = createRng(options.seed ?? 42)
629
- const shuffled = shuffleInPlace(valid.slice(), rng)
630
- const holdoutSize = Math.max(1, Math.floor(shuffled.length * 0.2))
631
- const holdout = shuffled.slice(0, holdoutSize)
632
- const train = shuffled.slice(holdoutSize)
633
- const trees = []
634
- const treeCount = options.treeCount ?? DEFAULT_TREE_COUNT
635
- const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH
636
- const minSamplesSplit = options.minSamplesSplit ?? DEFAULT_MIN_SAMPLES_SPLIT
637
- const minSamplesLeaf = options.minSamplesLeaf ?? DEFAULT_MIN_SAMPLES_LEAF
638
- const featureSubsample = options.featureSubsample ?? DEFAULT_FEATURE_SUBSAMPLE
639
-
640
- if (!train.length) {
641
- return {
642
- trained: false,
643
- reason: "insufficient training samples",
644
- classes,
645
- feature_names: [],
646
- trees: [],
647
- accuracy: 0,
648
- per_class_accuracy: {},
649
- samples: 0,
650
- baseline_accuracy: 0,
651
- }
652
- }
653
-
654
- const sourceCounts = valid.reduce((acc, sample) => {
655
- acc[sample.source] = (acc[sample.source] || 0) + 1
656
- return acc
657
- }, {})
658
-
659
- for (let i = 0; i < treeCount; i++) {
660
- const treeRng = createRng((options.seed ?? 42) + i * 97 + 1)
661
- const bootstrap = []
662
- for (let j = 0; j < train.length; j++) {
663
- bootstrap.push(train[Math.floor(treeRng() * train.length)])
664
- }
665
- trees.push(buildTree(bootstrap, classes, treeRng, 0, { maxDepth, minSamplesSplit, minSamplesLeaf, featureSubsample }))
666
- }
667
-
668
- const model = {
669
- version: 1,
670
- trained: true,
671
- trained_at: new Date().toISOString(),
672
- classes,
673
- feature_names: [
674
- "length",
675
- "word_count",
676
- "sentence_count",
677
- "avg_word_length",
678
- "question_ratio",
679
- "code_blocks",
680
- "urgency",
681
- "repetition",
682
- "sentiment",
683
- "complexity",
684
- "instruction_density",
685
- "file_mentions",
686
- "error_signals",
687
- "action_density",
688
- "complexity_words",
689
- "stress",
690
- "momentum",
691
- "action_consistency",
692
- "entropy_trend",
693
- "feature_contradiction",
694
- "embedding_delta",
695
- "pivot_score",
696
- "n_interactions",
697
- "loop_consecutive",
698
- "sub_regime",
699
- "resolution",
700
- "continuity_state",
701
- "loop_intervention_level",
702
- "is_looping",
703
- "outcome",
704
- ],
705
- params: {
706
- tree_count: treeCount,
707
- max_depth: maxDepth,
708
- min_samples_split: minSamplesSplit,
709
- min_samples_leaf: minSamplesLeaf,
710
- feature_subsample: featureSubsample,
711
- confidence_threshold: options.confidenceThreshold ?? DEFAULT_CONFIDENCE_THRESHOLD,
712
- seed: options.seed ?? 42,
713
- },
714
- trees,
715
- classCounts: Object.fromEntries(classes.map((cls) => [cls, 0])),
716
- }
717
-
718
- for (const sample of train) {
719
- model.classCounts[sample.label] = (model.classCounts[sample.label] || 0) + 1
720
- }
721
-
722
- const metrics = evaluateModel(model, holdout)
723
- model.metrics = {
724
- ...metrics,
725
- holdout_accuracy_by_source: splitMetricsBySource(model, holdout),
726
- holdout_samples: holdout.length,
727
- train_samples: train.length,
728
- total_samples: valid.length,
729
- source_counts: sourceCounts,
730
- }
731
- return model
732
- }
733
-
734
- export function trainBlackboxRoutingModel(samples, options = {}) {
735
- return trainRandomForestModel(samples, [...MODE_CLASSES], options, (label) => toClassLabel(label))
736
- }
737
-
738
- export function trainModeGateModel(samples, targetMode, options = {}) {
739
- const positive = normalizeGateMode(targetMode)
740
- const classes = [positive || "target", "other"]
741
- return trainRandomForestModel(samples, classes, options, (label) => (normalizeGateMode(label) === positive ? positive : "other"))
742
- }
743
-
744
- export function predictBlackboxRoutingModel(model, features) {
745
- if (!model || !Array.isArray(model.trees) || model.trees.length === 0) {
746
- return { label: "budget", confidence: 0, probabilities: { budget: 1 }, source: "fallback" }
747
- }
748
- const vector = Array.isArray(features) ? features.map((v) => num(v)) : extractRoutingFeatures(features)
749
- const votes = Object.fromEntries(model.classes.map((cls) => [cls, 0]))
750
- for (const tree of model.trees) {
751
- const pred = predictTree(tree, vector, model.classes)
752
- for (let i = 0; i < model.classes.length; i++) {
753
- const cls = model.classes[i]
754
- votes[cls] += pred.probs[i] || 0
755
- }
756
- }
757
- const probabilities = {}
758
- let bestClass = model.classes[0] || "budget"
759
- let bestProb = -1
760
- for (const cls of model.classes) {
761
- const prob = votes[cls] / model.trees.length
762
- probabilities[cls] = prob
763
- if (prob > bestProb) {
764
- bestProb = prob
765
- bestClass = cls
766
- }
767
- }
768
- return {
769
- label: bestClass,
770
- confidence: clamp(bestProb, 0, 1),
771
- probabilities,
772
- source: "random_forest",
773
- }
774
- }
775
-
776
- export function loadBlackboxRoutingModel({ refresh = false } = {}) {
777
- if (!refresh && cachedModel) return cachedModel
778
- const loaded = loadJson(MODEL_PATH)
779
- if (loaded && loaded.trained && Array.isArray(loaded.trees)) {
780
- cachedModel = loaded
781
- return cachedModel
782
- }
783
- return null
784
- }
785
-
786
- export function loadModeGateModel(targetMode, { refresh = false } = {}) {
787
- const gateMode = normalizeGateMode(targetMode)
788
- if (!gateMode) return null
789
- if (!refresh && cachedGateModels.has(gateMode)) return cachedGateModels.get(gateMode)
790
- const loaded = loadJson(getModeGatePath(gateMode))
791
- if (loaded && loaded.trained && Array.isArray(loaded.trees)) {
792
- cachedGateModels.set(gateMode, loaded)
793
- return loaded
794
- }
795
- return null
796
- }
797
-
798
- export function selectModeGate(targetMode, input = {}) {
799
- const gateMode = normalizeGateMode(targetMode)
800
- if (!gateMode) return null
801
-
802
- const stress = num(input.stress_multiplier ?? input.stress_score ?? input.stress ?? 0)
803
- const state = {
804
- sub_regime: String(input.sub_regime || input.state?.sub_regime || "INIT").toUpperCase(),
805
- resolution: input.resolution || input.resolution_state || input.state?.resolution || null,
806
- continuity_state: input.continuity_state || input.state?.continuity_state || null,
807
- loop_intervention_level: input.loop_intervention_level || input.state?.loop_intervention_level || null,
808
- momentum: num(input.momentum ?? input.state?.momentum ?? 0),
809
- n_interactions: num(input.n_interactions ?? input.turn_count ?? input.state?.n_interactions ?? 0),
810
- loop_consecutive: num(input.loop_consecutive ?? input.loop_count ?? input.state?.loop_consecutive ?? 0),
811
- pivot_score: num(input.pivot_score ?? input.state?.pivot_score ?? 0),
812
- is_looping: Boolean(input.is_looping ?? input.state?.is_looping ?? false),
813
- outcome: input.outcome || input.state?.outcome || null,
814
- signals: input.state?.signals || input.signals || {},
815
- user_text: String(input.user_text || input.prompt || input.state?.user_text || ""),
816
- prompt: String(input.user_text || input.prompt || input.state?.prompt || ""),
817
- }
818
- const prompt = String(input.user_text || input.prompt || "")
819
- const features = Array.isArray(input.features)
820
- ? input.features
821
- : extractRoutingFeatures({
822
- user_text: prompt,
823
- prompt,
824
- stress_multiplier: stress,
825
- state,
826
- features: input.features || input.prompt_features || {},
827
- })
828
- const model = loadModeGateModel(gateMode)
829
- if (!model) return null
830
-
831
- const prediction = predictBlackboxRoutingModel(model, features)
832
- const confidenceThreshold = model.params?.confidence_threshold ?? DEFAULT_CONFIDENCE_THRESHOLD
833
- if (!prediction || prediction.label !== gateMode || prediction.confidence < confidenceThreshold) {
834
- return null
835
- }
836
-
837
- return {
838
- mode: gateMode,
839
- source: "mode_gate",
840
- confidence: prediction.confidence,
841
- probabilities: prediction.probabilities,
842
- prediction,
843
- model: getModeGateModelMeta(gateMode),
844
- features,
845
- }
846
- }
847
-
848
- export function getBlackboxRoutingModelMeta() {
849
- const model = loadBlackboxRoutingModel()
850
- if (!model) {
851
- return {
852
- available: false,
853
- trained: false,
854
- path: MODEL_PATH,
855
- message: "no routing model saved yet",
856
- confidence_threshold: DEFAULT_CONFIDENCE_THRESHOLD,
857
- }
858
- }
859
- return {
860
- available: true,
861
- trained: true,
862
- path: MODEL_PATH,
863
- trained_at: model.trained_at,
864
- classes: model.classes,
865
- metrics: model.metrics || {},
866
- params: model.params || {},
867
- confidence_threshold: model.params?.confidence_threshold ?? DEFAULT_CONFIDENCE_THRESHOLD,
868
- }
869
- }
870
-
871
- export function getModeGateModelMeta(targetMode) {
872
- const gateMode = normalizeGateMode(targetMode)
873
- const model = loadModeGateModel(gateMode)
874
- const path = getModeGatePath(gateMode)
875
- if (!model) {
876
- return {
877
- available: false,
878
- trained: false,
879
- path,
880
- target_mode: gateMode,
881
- message: `no ${gateMode || "mode"} gate model saved yet`,
882
- confidence_threshold: DEFAULT_CONFIDENCE_THRESHOLD,
883
- }
884
- }
885
- return {
886
- available: true,
887
- trained: true,
888
- path,
889
- target_mode: gateMode,
890
- trained_at: model.trained_at,
891
- classes: model.classes,
892
- metrics: model.metrics || {},
893
- params: model.params || {},
894
- confidence_threshold: model.params?.confidence_threshold ?? DEFAULT_CONFIDENCE_THRESHOLD,
895
- }
896
- }
897
-
898
- export function saveBlackboxRoutingModel(model) {
899
- ensureModelDir()
900
- const payload = {
901
- ...model,
902
- saved_at: new Date().toISOString(),
903
- }
904
- writeFileSync(MODEL_PATH, JSON.stringify(payload, null, 2) + "\n", "utf-8")
905
- cachedModel = payload
906
- return payload
907
- }
908
-
909
- export function saveModeGateModel(model, targetMode) {
910
- const gateMode = normalizeGateMode(targetMode)
911
- ensureModeGateDir(gateMode)
912
- const payload = {
913
- ...model,
914
- saved_at: new Date().toISOString(),
915
- target_mode: gateMode,
916
- }
917
- writeFileSync(getModeGatePath(gateMode), JSON.stringify(payload, null, 2) + "\n", "utf-8")
918
- cachedGateModels.set(gateMode, payload)
919
- return payload
920
- }
921
-
922
- export function buildBlackboxRoutingSamplesFromDb({ projectId = null, limit = 5000 } = {}) {
923
- const db = getDb()
924
- let rows
925
- if (projectId) {
926
- rows = db.prepare(
927
- "SELECT session_id, project_id, state_json FROM blackbox_sessions WHERE project_id = ? ORDER BY updated_at DESC LIMIT ?"
928
- ).all(projectId, limit)
929
- } else {
930
- rows = db.prepare(
931
- "SELECT session_id, project_id, state_json FROM blackbox_sessions ORDER BY updated_at DESC LIMIT ?"
932
- ).all(limit)
933
- }
934
- const samples = []
935
- for (const row of rows) {
936
- samples.push(...deriveSamplesFromSessionRow(row))
937
- }
938
- return samples
939
- }
940
-
941
- export function rebuildBlackboxRoutingModel({ projectId = null, limit = 5000, includeBootstrap = true, seed = 42, treeCount = DEFAULT_TREE_COUNT, maxDepth = DEFAULT_MAX_DEPTH, minSamplesSplit = DEFAULT_MIN_SAMPLES_SPLIT, minSamplesLeaf = DEFAULT_MIN_SAMPLES_LEAF, featureSubsample = DEFAULT_FEATURE_SUBSAMPLE, confidenceThreshold = DEFAULT_CONFIDENCE_THRESHOLD } = {}) {
942
- const dbSamples = buildBlackboxRoutingSamplesFromDb({ projectId, limit })
943
- const bootstrap = includeBootstrap ? expandSyntheticSamples(bootstrapSamples(), seed) : []
944
- const samples = dbSamples.concat(bootstrap)
945
- const model = trainBlackboxRoutingModel(samples, {
946
- seed,
947
- treeCount,
948
- maxDepth,
949
- minSamplesSplit,
950
- minSamplesLeaf,
951
- featureSubsample,
952
- confidenceThreshold,
953
- })
954
- if (!model.trained) {
955
- return {
956
- ok: false,
957
- model,
958
- samples: samples.length,
959
- path: MODEL_PATH,
960
- message: model.reason || "training failed",
961
- }
962
- }
963
- const saved = saveBlackboxRoutingModel(model)
964
- return {
965
- ok: true,
966
- model: saved,
967
- samples: samples.length,
968
- synthetic_samples: bootstrap.length,
969
- production_samples: dbSamples.length,
970
- path: MODEL_PATH,
971
- accuracy: model.metrics?.accuracy ?? 0,
972
- baseline_accuracy: model.metrics?.baseline_accuracy ?? 0,
973
- source_counts: model.metrics?.source_counts || {},
974
- holdout_accuracy_by_source: model.metrics?.holdout_accuracy_by_source || {},
975
- }
976
- }
977
-
978
- export function rebuildModeGateModel({ targetMode, projectId = null, limit = 5000, includeBootstrap = true, seed = 42, treeCount = DEFAULT_TREE_COUNT, maxDepth = DEFAULT_MAX_DEPTH, minSamplesSplit = DEFAULT_MIN_SAMPLES_SPLIT, minSamplesLeaf = DEFAULT_MIN_SAMPLES_LEAF, featureSubsample = DEFAULT_FEATURE_SUBSAMPLE, confidenceThreshold = DEFAULT_CONFIDENCE_THRESHOLD } = {}) {
979
- const gateMode = normalizeGateMode(targetMode)
980
- if (!gateMode) {
981
- return { ok: false, message: "targetMode is required", path: null, target_mode: null }
982
- }
983
- const dbSamples = buildBlackboxRoutingSamplesFromDb({ projectId, limit })
984
- const bootstrap = includeBootstrap ? expandSyntheticSamples(bootstrapSamples(), seed) : []
985
- const samples = dbSamples.concat(bootstrap)
986
- const model = trainModeGateModel(samples, gateMode, {
987
- seed,
988
- treeCount,
989
- maxDepth,
990
- minSamplesSplit,
991
- minSamplesLeaf,
992
- featureSubsample,
993
- confidenceThreshold,
994
- })
995
- if (!model.trained) {
996
- return {
997
- ok: false,
998
- model,
999
- samples: samples.length,
1000
- path: getModeGatePath(gateMode),
1001
- target_mode: gateMode,
1002
- message: model.reason || "training failed",
1003
- }
1004
- }
1005
- const saved = saveModeGateModel(model, gateMode)
1006
- return {
1007
- ok: true,
1008
- model: saved,
1009
- samples: samples.length,
1010
- synthetic_samples: bootstrap.length,
1011
- production_samples: dbSamples.length,
1012
- path: getModeGatePath(gateMode),
1013
- target_mode: gateMode,
1014
- accuracy: model.metrics?.accuracy ?? 0,
1015
- baseline_accuracy: model.metrics?.baseline_accuracy ?? 0,
1016
- source_counts: model.metrics?.source_counts || {},
1017
- holdout_accuracy_by_source: model.metrics?.holdout_accuracy_by_source || {},
1018
- }
1019
- }
1020
-
1021
- export function selectBlackboxMode(input = {}) {
1022
- const stress = num(input.stress_multiplier ?? input.stress_score ?? input.stress ?? 0)
1023
- const state = {
1024
- sub_regime: String(input.sub_regime || "INIT").toUpperCase(),
1025
- resolution: input.resolution || input.resolution_state || null,
1026
- continuity_state: input.continuity_state || null,
1027
- loop_intervention_level: input.loop_intervention_level || null,
1028
- momentum: num(input.momentum ?? input.state?.momentum ?? 0),
1029
- n_interactions: num(input.n_interactions ?? input.turn_count ?? input.state?.n_interactions ?? 0),
1030
- loop_consecutive: num(input.loop_consecutive ?? input.loop_count ?? input.state?.loop_consecutive ?? 0),
1031
- pivot_score: num(input.pivot_score ?? input.state?.pivot_score ?? 0),
1032
- is_looping: Boolean(input.is_looping ?? input.state?.is_looping ?? false),
1033
- outcome: input.outcome || null,
1034
- signals: input.state?.signals || input.signals || {},
1035
- user_text: String(input.user_text || input.prompt || ""),
1036
- prompt: String(input.user_text || input.prompt || ""),
1037
- }
1038
- const prompt = String(input.user_text || input.prompt || "")
1039
- const features = Array.isArray(input.features) ? input.features : buildFeatureVector(prompt, { state, prompt_features: input.features || input.prompt_features || {}, stress })
1040
- const recentSignals = loadRecentSessionSignals(input.project_id || null, input.session_id || null)
1041
- const heuristic = autoSelectMode(state.sub_regime || "INIT", stress, { state, recentSignals: recentSignals || undefined })
1042
- const budgetGate = selectModeGate("budget", { ...input, state, features })
1043
- if (budgetGate) {
1044
- return {
1045
- ...budgetGate,
1046
- model: getBlackboxRoutingModelMeta(),
1047
- gate_model: budgetGate.model,
1048
- features,
1049
- }
1050
- }
1051
- const model = loadBlackboxRoutingModel()
1052
- if (!model) {
1053
- return {
1054
- mode: heuristic,
1055
- source: "heuristic",
1056
- confidence: 0,
1057
- probabilities: {},
1058
- model: getBlackboxRoutingModelMeta(),
1059
- features,
1060
- }
1061
- }
1062
- const prediction = predictBlackboxRoutingModel(model, features)
1063
- const confidenceThreshold = model.params?.confidence_threshold ?? DEFAULT_CONFIDENCE_THRESHOLD
1064
- if (!prediction || prediction.confidence < confidenceThreshold) {
1065
- return {
1066
- mode: heuristic,
1067
- source: "heuristic",
1068
- confidence: prediction?.confidence ?? 0,
1069
- probabilities: prediction?.probabilities || {},
1070
- prediction,
1071
- model: getBlackboxRoutingModelMeta(),
1072
- features,
1073
- }
1074
- }
1075
- if (modePriority(prediction.label) < modePriority(heuristic)) {
1076
- return {
1077
- mode: heuristic,
1078
- source: "heuristic",
1079
- confidence: prediction.confidence,
1080
- probabilities: prediction.probabilities,
1081
- prediction,
1082
- model: getBlackboxRoutingModelMeta(),
1083
- features,
1084
- }
1085
- }
1086
- return {
1087
- mode: prediction.label,
1088
- source: "random_forest",
1089
- confidence: prediction.confidence,
1090
- probabilities: prediction.probabilities,
1091
- model: getBlackboxRoutingModelMeta(),
1092
- features,
1093
- }
1094
- }
1095
-
1096
- export function resetBlackboxRoutingModelCache() {
1097
- cachedModel = null
1098
- cachedGateModels.clear()
1099
- }