vibeoscore 1.0.2
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/.env.example +5 -0
- package/README.md +29 -0
- package/client.js +257 -0
- package/client.ts +334 -0
- package/dashboard/dist/assets/index-BnPt1Fii.js +1 -0
- package/dashboard/dist/assets/index-CfH00tOL.css +1 -0
- package/dashboard/dist/index.html +3 -0
- package/lib/blackbox-rf.js +1099 -0
- package/lib/blackbox.js +137 -0
- package/lib/compression.js +119 -0
- package/lib/db.js +106 -0
- package/lib/db.ts +113 -0
- package/lib/delegation.js +137 -0
- package/lib/meta-controller.js +418 -0
- package/lib/meta-controller.mjs +499 -0
- package/lib/patterns.js +150 -0
- package/lib/resolution-tracker.js +486 -0
- package/lib/stress.js +84 -0
- package/lib/tdd.js +218 -0
- package/lib/tier-routing.js +48 -0
- package/mcp-server.js +370 -0
- package/mcp-server.ts +364 -0
- package/middleware/auth.js +75 -0
- package/middleware/auth.ts +87 -0
- package/middleware/usage-logging.js +29 -0
- package/middleware/usage-logging.ts +41 -0
- package/nginx-vibetheog-api.conf +64 -0
- package/package.json +66 -0
- package/routes/admin.js +93 -0
- package/routes/admin.ts +107 -0
- package/routes/blackbox.js +463 -0
- package/routes/compression.js +12 -0
- package/routes/delegation.js +30 -0
- package/routes/patterns.js +53 -0
- package/routes/pricing.js +62 -0
- package/routes/stress.js +30 -0
- package/routes/tdd.js +68 -0
- package/routes/tier-routing.js +31 -0
- package/scripts/dashboard-server.mjs +246 -0
- package/scripts/deploy-zero-downtime.sh +77 -0
- package/scripts/deploy.sh +68 -0
- package/scripts/release.mjs +30 -0
- package/scripts/seed-master-token.js +29 -0
- package/scripts/start-all.mjs +34 -0
- package/server.js +88 -0
- package/vibeos-api.service +19 -0
|
@@ -0,0 +1,1099 @@
|
|
|
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
|
+
}
|