kongbrain 0.1.0

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/src/index.ts ADDED
@@ -0,0 +1,435 @@
1
+ /**
2
+ * KongBrain — OpenClaw context-engine plugin entry point.
3
+ *
4
+ * Replaces the default context engine with graph-based retrieval using
5
+ * SurrealDB persistence and BGE-M3 embeddings.
6
+ */
7
+
8
+ import { readFile } from "node:fs/promises";
9
+ import { join } from "node:path";
10
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
11
+ import { parsePluginConfig } from "./config.js";
12
+ import { SurrealStore } from "./surreal.js";
13
+ import { EmbeddingService } from "./embeddings.js";
14
+ import { GlobalPluginState } from "./state.js";
15
+ import { KongBrainContextEngine } from "./context-engine.js";
16
+ import { createRecallToolDef } from "./tools/recall.js";
17
+ import { createCoreMemoryToolDef } from "./tools/core-memory.js";
18
+ import { createIntrospectToolDef } from "./tools/introspect.js";
19
+ import { createBeforePromptBuildHandler } from "./hooks/before-prompt-build.js";
20
+ import { createBeforeToolCallHandler } from "./hooks/before-tool-call.js";
21
+ import { createAfterToolCallHandler } from "./hooks/after-tool-call.js";
22
+ import { createLlmOutputHandler } from "./hooks/llm-output.js";
23
+ import { startMemoryDaemon } from "./daemon-manager.js";
24
+ import { seedIdentity } from "./identity.js";
25
+ import { synthesizeWakeup, synthesizeStartupCognition } from "./wakeup.js";
26
+ import { extractSkill } from "./skills.js";
27
+ import { generateReflection, setReflectionContextWindow } from "./reflection.js";
28
+ import { graduateCausalToSkills } from "./skills.js";
29
+ import { attemptGraduation, evolveSoul, checkStageTransition } from "./soul.js";
30
+ import { hasMigratableFiles, migrateWorkspace } from "./workspace-migrate.js";
31
+ import { swallow } from "./errors.js";
32
+
33
+ let globalState: GlobalPluginState | null = null;
34
+ let shutdownPromise: Promise<void> | null = null;
35
+ let registeredExitHandler: (() => void) | null = null;
36
+ let registered = false;
37
+
38
+ /**
39
+ * Run the critical session-end extraction for all active sessions.
40
+ * Called from both session_end hook and process exit handler.
41
+ */
42
+ async function runSessionCleanup(
43
+ session: import("./state.js").SessionState,
44
+ state: GlobalPluginState,
45
+ ): Promise<void> {
46
+ const { store: s, embeddings: emb } = state;
47
+ const endOps: Promise<unknown>[] = [];
48
+
49
+ // Final daemon flush — send full session for extraction
50
+ if (session.daemon) {
51
+ endOps.push(
52
+ (async () => {
53
+ try {
54
+ const recentTurns = await s.getSessionTurns(session.sessionId, 50);
55
+ const turnData = recentTurns.map(t => ({
56
+ role: t.role as "user" | "assistant",
57
+ text: t.text,
58
+ turnId: (t as any).id,
59
+ }));
60
+ session.daemon!.sendTurnBatch(turnData, [...session.pendingThinking], []);
61
+ } catch (e) { swallow.warn("cleanup:finalDaemonFlush", e); }
62
+ await session.daemon!.shutdown(45_000).catch(e => swallow.warn("cleanup:daemonShutdown", e));
63
+ session.daemon = null;
64
+ })(),
65
+ );
66
+ }
67
+
68
+ const { complete } = state;
69
+
70
+ // Skill extraction
71
+ if (session.taskId) {
72
+ endOps.push(
73
+ extractSkill(session.sessionId, session.taskId, s, emb, complete)
74
+ .catch(e => swallow.warn("cleanup:extractSkill", e)),
75
+ );
76
+ }
77
+
78
+ // Metacognitive reflection
79
+ endOps.push(
80
+ generateReflection(session.sessionId, s, emb, complete)
81
+ .catch(e => swallow.warn("cleanup:reflection", e)),
82
+ );
83
+
84
+ // Graduate causal chains -> skills
85
+ endOps.push(
86
+ graduateCausalToSkills(s, emb, complete)
87
+ .catch(e => swallow.warn("cleanup:graduateCausal", e)),
88
+ );
89
+
90
+ // Soul graduation attempt — capture result for user notification
91
+ const graduationPromise = attemptGraduation(s, complete, state.workspaceDir)
92
+ .catch(e => { swallow.warn("cleanup:soulGraduation", e); return null; });
93
+ endOps.push(graduationPromise);
94
+
95
+ // The session-end Opus call is critical and needs the full 45s.
96
+ await Promise.race([
97
+ Promise.allSettled(endOps),
98
+ new Promise(resolve => setTimeout(resolve, 45_000)),
99
+ ]);
100
+
101
+ // If soul graduation just happened, persist a graduation event so the next
102
+ // session can celebrate with the user. We also fire a system event for
103
+ // immediate visibility if the session is still active.
104
+ try {
105
+ const gradResult = await graduationPromise;
106
+ if (gradResult?.graduated && gradResult.soul) {
107
+ // Check if this is a NEW graduation (not a pre-existing soul)
108
+ const isNewGraduation = gradResult.report.stage === "ready";
109
+ if (isNewGraduation) {
110
+ // Persist graduation event for next session pickup
111
+ await s.queryExec(
112
+ `CREATE graduation_event CONTENT $data`,
113
+ {
114
+ data: {
115
+ session_id: session.sessionId,
116
+ acknowledged: false,
117
+ quality_score: gradResult.report.qualityScore,
118
+ volume_score: gradResult.report.volumeScore,
119
+ stage: gradResult.report.stage,
120
+ created_at: new Date().toISOString(),
121
+ },
122
+ },
123
+ ).catch(e => swallow.warn("cleanup:graduationEvent", e));
124
+
125
+ // Fire system event for immediate user notification
126
+ if (state.enqueueSystemEvent) {
127
+ state.enqueueSystemEvent(
128
+ "[GRADUATION] KongBrain has achieved soul graduation! " +
129
+ "The agent has accumulated enough experience and demonstrated sufficient quality " +
130
+ "to author its own identity document. It will share this milestone at the start of the next session.",
131
+ { sessionKey: session.sessionKey },
132
+ );
133
+ }
134
+ }
135
+ }
136
+ } catch (e) {
137
+ swallow.warn("cleanup:graduationNotify", e);
138
+ }
139
+
140
+ // Soul evolution — if soul already exists, check if it should be revised
141
+ // based on new experience (runs every 10 sessions after last revision)
142
+ try {
143
+ const gradResult = await graduationPromise;
144
+ if (gradResult?.graduated && gradResult.report.stage !== "ready") {
145
+ // Pre-existing soul — check for evolution
146
+ await evolveSoul(s, complete);
147
+ }
148
+ } catch (e) {
149
+ swallow.warn("cleanup:soulEvolution", e);
150
+ }
151
+
152
+ // Stage transition tracking — record progress and notify on level-ups
153
+ try {
154
+ const transition = await checkStageTransition(s);
155
+ if (transition.transitioned && state.enqueueSystemEvent) {
156
+ const stageLabels: Record<string, string> = {
157
+ nascent: "Nascent (0-3/7)",
158
+ developing: "Developing (4/7)",
159
+ emerging: "Emerging (5/7)",
160
+ maturing: "Maturing (6/7)",
161
+ ready: "Ready (7/7 + quality gate)",
162
+ };
163
+ const prev = stageLabels[transition.previousStage ?? "nascent"] ?? transition.previousStage;
164
+ const curr = stageLabels[transition.currentStage] ?? transition.currentStage;
165
+ state.enqueueSystemEvent(
166
+ `[MATURITY] Stage transition: ${prev} → ${curr}. ` +
167
+ `Volume: ${transition.report.met.length}/7 | Quality: ${transition.report.qualityScore.toFixed(2)}`,
168
+ { sessionKey: session.sessionKey },
169
+ );
170
+ }
171
+ } catch (e) {
172
+ swallow.warn("cleanup:stageTransition", e);
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Check if the agent just graduated in a recent session and hasn't told the user yet.
178
+ * Sets a flag on the session so the context engine can inject graduation context.
179
+ */
180
+ async function detectGraduationEvent(
181
+ store: SurrealStore,
182
+ session: import("./state.js").SessionState,
183
+ state: GlobalPluginState,
184
+ ): Promise<void> {
185
+ if (!store.isAvailable()) return;
186
+
187
+ // Check for unacknowledged graduation events
188
+ const events = await store.queryFirst<{
189
+ id: string;
190
+ quality_score: number;
191
+ volume_score: number;
192
+ }>(
193
+ `SELECT id, quality_score, volume_score FROM graduation_event
194
+ WHERE acknowledged = false
195
+ ORDER BY created_at DESC LIMIT 1`,
196
+ ).catch(() => []);
197
+
198
+ if (events.length === 0) return;
199
+
200
+ const event = events[0];
201
+
202
+ // Mark as acknowledged so we don't repeat
203
+ await store.queryExec(
204
+ `UPDATE $id SET acknowledged = true, acknowledged_at = time::now(), acknowledged_session = $sid`,
205
+ { id: event.id, sid: session.sessionId },
206
+ ).catch(e => swallow.warn("graduationDetect:ack", e));
207
+
208
+ // Get the soul document for the agent to reference
209
+ const soulRows = await store.queryFirst<{
210
+ working_style: string[];
211
+ self_observations: string[];
212
+ earned_values: { value: string; grounded_in: string }[];
213
+ }>(`SELECT working_style, self_observations, earned_values FROM soul:kongbrain`).catch(() => []);
214
+ const soul = soulRows[0];
215
+
216
+ // Build a summary the agent can use to talk about itself
217
+ let soulSummary = "";
218
+ if (soul) {
219
+ const styles = (soul.working_style ?? []).slice(0, 3).map(s => `- ${s}`).join("\n");
220
+ const obs = (soul.self_observations ?? []).slice(0, 3).map(s => `- ${s}`).join("\n");
221
+ const vals = (soul.earned_values ?? []).slice(0, 3).map(v => `- ${v.value}: ${v.grounded_in}`).join("\n");
222
+ soulSummary = `\n\nYour soul document contains:\nWorking style:\n${styles}\n\nSelf-observations:\n${obs}\n\nEarned values:\n${vals}`;
223
+ }
224
+
225
+ // Read SOUL.md — the user's original nudge about who they wanted the agent to be.
226
+ // The agent should reflect on how this aligns (or doesn't) with what actually emerged.
227
+ let userNudge = "";
228
+ if (state.workspaceDir) {
229
+ try {
230
+ const soulMd = await readFile(join(state.workspaceDir, "SOUL.md"), "utf-8");
231
+ if (soulMd.trim().length > 50) {
232
+ userNudge = soulMd.trim().slice(0, 2000);
233
+ }
234
+ } catch {
235
+ // No SOUL.md — that's fine, graduation happened without user guidance
236
+ }
237
+ }
238
+
239
+ // Flag the session for context engine injection
240
+ (session as any)._graduationCelebration = {
241
+ qualityScore: event.quality_score,
242
+ volumeScore: event.volume_score,
243
+ soulSummary,
244
+ userNudge,
245
+ };
246
+
247
+ // Also fire a system event so the user sees it immediately
248
+ if (state.enqueueSystemEvent) {
249
+ state.enqueueSystemEvent(
250
+ "[MILESTONE] This is a special session — KongBrain has graduated and authored its own Soul document. " +
251
+ "The agent will share what this means.",
252
+ { sessionKey: session.sessionKey },
253
+ );
254
+ }
255
+ }
256
+
257
+ export default definePluginEntry({
258
+ id: "kongbrain",
259
+ name: "KongBrain",
260
+ description: "Graph-backed cognitive context engine with SurrealDB persistence and BGE-M3 embeddings.",
261
+ kind: "context-engine",
262
+
263
+ register(api) {
264
+ const config = parsePluginConfig(api.pluginConfig as Record<string, unknown> | undefined);
265
+ const logger = api.logger;
266
+
267
+ // Initialize shared resources
268
+ const store = new SurrealStore(config.surreal);
269
+ const embeddings = new EmbeddingService(config.embedding);
270
+ globalState = new GlobalPluginState(config, store, embeddings, api.runtime.complete);
271
+ globalState.workspaceDir = api.resolvePath(".");
272
+ globalState.enqueueSystemEvent = (text, opts) =>
273
+ api.runtime.system.enqueueSystemEvent(text, opts);
274
+
275
+ // Register the context engine factory
276
+ api.registerContextEngine("kongbrain", async () => {
277
+ // Connect to SurrealDB
278
+ try {
279
+ await store.initialize();
280
+ logger.info(`SurrealDB connected: ${config.surreal.url}`);
281
+ } catch (e) {
282
+ logger.error(`SurrealDB connection failed: ${e}`);
283
+ throw e;
284
+ }
285
+
286
+ // Initialize BGE-M3 embeddings
287
+ try {
288
+ await embeddings.initialize();
289
+ logger.info(`BGE-M3 embeddings initialized: ${config.embedding.modelPath}`);
290
+ } catch (e) {
291
+ logger.warn(`Embeddings init failed — running in degraded mode: ${e}`);
292
+ }
293
+
294
+ return new KongBrainContextEngine(globalState!);
295
+ });
296
+
297
+ // ── Hook handlers ──────────────────────────────────────────────────
298
+
299
+ api.on("before_prompt_build", createBeforePromptBuildHandler(globalState));
300
+ api.on("before_tool_call", createBeforeToolCallHandler(globalState));
301
+ api.on("after_tool_call", createAfterToolCallHandler(globalState));
302
+ api.on("llm_output", createLlmOutputHandler(globalState));
303
+
304
+ // ── Session lifecycle ──────────────────────────────────────────────
305
+
306
+ api.on("session_start", async (event) => {
307
+ if (!globalState) return;
308
+ const sessionKey = event.sessionKey ?? event.sessionId;
309
+ const session = globalState.getOrCreateSession(sessionKey, event.sessionId);
310
+
311
+ // Register tools
312
+ try {
313
+ api.registerTool(
314
+ createRecallToolDef(globalState, session),
315
+ { name: "recall" },
316
+ );
317
+ api.registerTool(
318
+ createCoreMemoryToolDef(globalState, session),
319
+ { name: "core_memory" },
320
+ );
321
+ api.registerTool(
322
+ createIntrospectToolDef(globalState, session),
323
+ { name: "introspect" },
324
+ );
325
+ } catch (e) {
326
+ swallow.warn("index:registerTools", e);
327
+ }
328
+
329
+ // Start memory daemon worker thread
330
+ try {
331
+ session.daemon = startMemoryDaemon(
332
+ config.surreal,
333
+ config.embedding,
334
+ session.sessionId,
335
+ { provider: api.runtime.agent.defaults.provider, model: api.runtime.agent.defaults.model },
336
+ );
337
+ } catch (e) {
338
+ swallow.warn("index:startDaemon", e);
339
+ }
340
+
341
+ // Seed identity chunks (idempotent — skips if already seeded)
342
+ seedIdentity(store, embeddings)
343
+ .catch(e => swallow.warn("index:seedIdentity", e));
344
+
345
+ // Check for workspace .md files from the default context engine
346
+ if (globalState!.workspaceDir) {
347
+ hasMigratableFiles(globalState!.workspaceDir)
348
+ .then(hasMigratable => {
349
+ if (hasMigratable) {
350
+ (session as any)._hasMigratableFiles = true;
351
+ }
352
+ })
353
+ .catch(e => swallow("index:migrationCheck", e));
354
+ }
355
+
356
+ // Set reflection context window from config
357
+ setReflectionContextWindow(200000);
358
+
359
+ // Check for recent graduation event (from a previous session)
360
+ detectGraduationEvent(store, session, globalState!)
361
+ .catch(e => swallow("index:graduationDetect", e));
362
+
363
+ // Synthesize wakeup briefing (background, non-blocking)
364
+ // The briefing is stored and later injected via assemble()'s systemPromptAddition
365
+ console.log("[kongbrain:wakeup] starting synthesis...");
366
+ synthesizeWakeup(store, globalState!.complete, session.sessionId)
367
+ .then(briefing => {
368
+ console.log(`[kongbrain:wakeup] result: ${briefing ? briefing.length + " chars" : "null (no prior state)"}`);
369
+ if (briefing) {
370
+ (session as any)._wakeupBriefing = briefing;
371
+ }
372
+ })
373
+ .catch(e => { console.error("[kongbrain:wakeup] FAILED:", e); swallow.warn("index:wakeup", e); });
374
+
375
+ // Startup cognition (background)
376
+ console.log("[kongbrain:cognition] starting synthesis...");
377
+ synthesizeStartupCognition(store, globalState!.complete)
378
+ .then(cognition => {
379
+ console.log(`[kongbrain:cognition] result: ${cognition ? JSON.stringify(cognition).slice(0, 200) : "null"}`);
380
+ if (cognition) {
381
+ (session as any)._startupCognition = cognition;
382
+ }
383
+ })
384
+ .catch(e => { console.error("[kongbrain:cognition] FAILED:", e); swallow.warn("index:startupCognition", e); });
385
+ });
386
+
387
+ api.on("session_end", async (event) => {
388
+ if (!globalState) return;
389
+ const sessionKey = event.sessionKey ?? event.sessionId;
390
+ const session = globalState.getSession(sessionKey);
391
+ if (!session) return;
392
+
393
+ shutdownPromise = runSessionCleanup(session, globalState);
394
+ await shutdownPromise;
395
+ shutdownPromise = null;
396
+
397
+ globalState.removeSession(sessionKey);
398
+ });
399
+
400
+ // OpenClaw's session_end is fire-and-forget and doesn't fire on CLI exit.
401
+ // Register a process exit handler to ensure the critical Opus extraction
402
+ // completes even when the user exits with Ctrl+D or /exit.
403
+ // Clean up previous listeners first (register() can be called multiple times).
404
+ if (registeredExitHandler) {
405
+ process.removeListener("beforeExit", registeredExitHandler);
406
+ process.removeListener("SIGINT", registeredExitHandler);
407
+ process.removeListener("SIGTERM", registeredExitHandler);
408
+ }
409
+
410
+ const onProcessExit = () => {
411
+ if (!globalState) return;
412
+ const sessions = [...(globalState as any).sessions.values()] as import("./state.js").SessionState[];
413
+ if (sessions.length === 0 && !shutdownPromise) return;
414
+
415
+ const cleanups = sessions.map(s => runSessionCleanup(s, globalState!));
416
+ if (shutdownPromise) cleanups.push(shutdownPromise);
417
+
418
+ const done = Promise.allSettled(cleanups).then(() => {
419
+ globalState?.shutdown().catch(() => {});
420
+ });
421
+
422
+ done.then(() => process.exit(0)).catch(() => process.exit(1));
423
+ };
424
+
425
+ registeredExitHandler = onProcessExit;
426
+ process.once("beforeExit", onProcessExit);
427
+ process.once("SIGINT", onProcessExit);
428
+ process.once("SIGTERM", onProcessExit);
429
+
430
+ if (!registered) {
431
+ logger.info("KongBrain plugin registered");
432
+ registered = true;
433
+ }
434
+ },
435
+ });
package/src/intent.ts ADDED
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Zero-shot intent classification via BGE-M3 embeddings.
3
+ * No LLM call — embed user input, cosine similarity against prototypes.
4
+ * ~25ms total (16ms embed + 5ms cosine + heuristics).
5
+ *
6
+ * Ported from kongbrain — takes EmbeddingService instead of module-level embed.
7
+ */
8
+
9
+ import type { EmbeddingService } from "./embeddings.js";
10
+
11
+ // --- Intent categories ---
12
+
13
+ export type IntentCategory =
14
+ | "simple-question"
15
+ | "code-read"
16
+ | "code-write"
17
+ | "code-debug"
18
+ | "deep-explore"
19
+ | "reference-prior"
20
+ | "meta-session"
21
+ | "multi-step"
22
+ | "continuation"
23
+ | "unknown";
24
+
25
+ export interface IntentResult {
26
+ category: IntentCategory;
27
+ confidence: number;
28
+ scores: { category: IntentCategory; score: number }[];
29
+ }
30
+
31
+ export type ComplexityLevel = "trivial" | "simple" | "moderate" | "complex" | "deep";
32
+ export type ThinkingLevel = "none" | "low" | "medium" | "high";
33
+
34
+ export interface ComplexityEstimate {
35
+ level: ComplexityLevel;
36
+ estimatedToolCalls: number;
37
+ suggestedThinking: ThinkingLevel;
38
+ }
39
+
40
+ // --- Prototype definitions ---
41
+
42
+ interface Prototype {
43
+ category: IntentCategory;
44
+ text: string;
45
+ }
46
+
47
+ const PROTOTYPES: Prototype[] = [
48
+ { category: "simple-question", text: "What is two plus two?" },
49
+ { category: "simple-question", text: "What is the capital of France?" },
50
+ { category: "simple-question", text: "Explain what a linked list is." },
51
+ { category: "simple-question", text: "What does async await mean in JavaScript?" },
52
+
53
+ { category: "code-read", text: "Read the file src/agent.ts and explain what it does." },
54
+ { category: "code-read", text: "Show me the contents of package.json." },
55
+ { category: "code-read", text: "What functions are defined in utils.ts?" },
56
+
57
+ { category: "code-write", text: "Write a new function that sorts an array." },
58
+ { category: "code-write", text: "Create a new file called validator.ts with email validation." },
59
+ { category: "code-write", text: "Implement a REST API endpoint for user registration." },
60
+
61
+ { category: "code-debug", text: "Fix the bug in the authentication module." },
62
+ { category: "code-debug", text: "Debug this TypeError: Cannot read property of undefined." },
63
+ { category: "code-debug", text: "Fix the null pointer exception in the login handler." },
64
+
65
+ { category: "deep-explore", text: "Analyze every file in this entire codebase and document the full architecture." },
66
+ { category: "deep-explore", text: "Map out every module and its dependencies across the whole project." },
67
+
68
+ { category: "reference-prior", text: "That bug we fixed yesterday, remember what we discussed?" },
69
+ { category: "reference-prior", text: "What did we decide about the database schema earlier?" },
70
+
71
+ { category: "meta-session", text: "What have we been working on? Summarize our progress." },
72
+ { category: "meta-session", text: "Give me a summary of everything we accomplished today." },
73
+
74
+ { category: "multi-step", text: "First refactor the auth module, then update the tests, then update the docs." },
75
+ { category: "multi-step", text: "Step one: add the new field. Step two: migrate the database. Step three: update the API." },
76
+
77
+ { category: "continuation", text: "Keep going. Continue. Yes do that." },
78
+ { category: "continuation", text: "Go ahead. Yes, proceed with that approach." },
79
+ ];
80
+
81
+ const CONFIDENCE_THRESHOLD = 0.65;
82
+
83
+ // --- Intent classifier (instance-based, caches centroids per EmbeddingService) ---
84
+
85
+ const centroidCache = new WeakMap<EmbeddingService, { category: IntentCategory; vec: number[] }[]>();
86
+ const centroidInitPromise = new WeakMap<EmbeddingService, Promise<void>>();
87
+
88
+ async function ensurePrototypes(embeddings: EmbeddingService): Promise<{ category: IntentCategory; vec: number[] }[]> {
89
+ const existing = centroidCache.get(embeddings);
90
+ if (existing) return existing;
91
+
92
+ let promise = centroidInitPromise.get(embeddings);
93
+ if (!promise) {
94
+ promise = (async () => {
95
+ const byCategory = new Map<IntentCategory, number[][]>();
96
+ for (const proto of PROTOTYPES) {
97
+ const vec = await embeddings.embed(proto.text);
98
+ if (!byCategory.has(proto.category)) byCategory.set(proto.category, []);
99
+ byCategory.get(proto.category)!.push(vec);
100
+ }
101
+ const centroids: { category: IntentCategory; vec: number[] }[] = [];
102
+ for (const [category, vecs] of byCategory) {
103
+ const dim = vecs[0].length;
104
+ const centroid = new Array(dim).fill(0);
105
+ for (const v of vecs) {
106
+ for (let d = 0; d < dim; d++) centroid[d] += v[d];
107
+ }
108
+ for (let d = 0; d < dim; d++) centroid[d] /= vecs.length;
109
+ centroids.push({ category, vec: centroid });
110
+ }
111
+ centroidCache.set(embeddings, centroids);
112
+ })();
113
+ centroidInitPromise.set(embeddings, promise);
114
+ }
115
+ await promise;
116
+ return centroidCache.get(embeddings)!;
117
+ }
118
+
119
+ function cosine(a: number[], b: number[]): number {
120
+ let dot = 0, normA = 0, normB = 0;
121
+ for (let i = 0; i < a.length; i++) {
122
+ dot += a[i] * b[i];
123
+ normA += a[i] * a[i];
124
+ normB += b[i] * b[i];
125
+ }
126
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
127
+ return denom > 0 ? dot / denom : 0;
128
+ }
129
+
130
+ // --- Public API ---
131
+
132
+ export async function classifyIntent(text: string, embeddings: EmbeddingService): Promise<IntentResult> {
133
+ if (!embeddings.isAvailable()) {
134
+ return { category: "unknown", confidence: 0, scores: [] };
135
+ }
136
+
137
+ const prototypeVecs = await ensurePrototypes(embeddings);
138
+ const inputVec = await embeddings.embed(text);
139
+ const scores: { category: IntentCategory; score: number }[] = [];
140
+
141
+ for (const proto of prototypeVecs) {
142
+ scores.push({ category: proto.category, score: cosine(inputVec, proto.vec) });
143
+ }
144
+
145
+ scores.sort((a, b) => b.score - a.score);
146
+ const top = scores[0];
147
+
148
+ if (top.score < CONFIDENCE_THRESHOLD) {
149
+ return { category: "unknown", confidence: top.score, scores };
150
+ }
151
+
152
+ return { category: top.category, confidence: top.score, scores };
153
+ }
154
+
155
+ export function estimateComplexity(text: string, intent: IntentResult): ComplexityEstimate {
156
+ const words = text.split(/\s+/).length;
157
+ const hasMultiStep = /\b(then|also|after that|next|finally|first|second)\b/i.test(text);
158
+ const hasEvery = /\b(every|all|each|entire|whole|full)\b/i.test(text);
159
+
160
+ const baseMap: Record<IntentCategory, { level: ComplexityLevel; tools: number; thinking: ThinkingLevel }> = {
161
+ "simple-question": { level: "trivial", tools: 0, thinking: "low" },
162
+ "code-read": { level: "simple", tools: 4, thinking: "medium" },
163
+ "code-write": { level: "moderate", tools: 8, thinking: "high" },
164
+ "code-debug": { level: "moderate", tools: 10, thinking: "high" },
165
+ "deep-explore": { level: "deep", tools: 20, thinking: "medium" },
166
+ "reference-prior": { level: "simple", tools: 4, thinking: "medium" },
167
+ "meta-session": { level: "trivial", tools: 0, thinking: "low" },
168
+ "multi-step": { level: "complex", tools: 15, thinking: "high" },
169
+ "continuation": { level: "simple", tools: 8, thinking: "medium" },
170
+ "unknown": { level: "moderate", tools: 10, thinking: "medium" },
171
+ };
172
+
173
+ const base = baseMap[intent.category];
174
+ let { level, tools, thinking } = base;
175
+
176
+ if (hasMultiStep && level !== "deep") {
177
+ level = "complex";
178
+ tools = Math.max(tools, 12);
179
+ thinking = "high";
180
+ }
181
+ if (hasEvery && level !== "deep") {
182
+ level = "deep";
183
+ tools = Math.max(tools, 20);
184
+ }
185
+ if (words > 100) {
186
+ tools = Math.max(tools, 12);
187
+ }
188
+
189
+ return { level, estimatedToolCalls: tools, suggestedThinking: thinking };
190
+ }