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/LICENSE +21 -0
- package/README.md +385 -0
- package/openclaw.plugin.json +66 -0
- package/package.json +65 -0
- package/src/acan.ts +309 -0
- package/src/causal.ts +237 -0
- package/src/cognitive-check.ts +330 -0
- package/src/config.ts +64 -0
- package/src/context-engine.ts +487 -0
- package/src/daemon-manager.ts +148 -0
- package/src/daemon-types.ts +65 -0
- package/src/embeddings.ts +77 -0
- package/src/errors.ts +43 -0
- package/src/graph-context.ts +989 -0
- package/src/hooks/after-tool-call.ts +99 -0
- package/src/hooks/before-prompt-build.ts +44 -0
- package/src/hooks/before-tool-call.ts +86 -0
- package/src/hooks/llm-output.ts +173 -0
- package/src/identity.ts +218 -0
- package/src/index.ts +435 -0
- package/src/intent.ts +190 -0
- package/src/memory-daemon.ts +495 -0
- package/src/orchestrator.ts +348 -0
- package/src/prefetch.ts +200 -0
- package/src/reflection.ts +280 -0
- package/src/retrieval-quality.ts +266 -0
- package/src/schema.surql +387 -0
- package/src/skills.ts +343 -0
- package/src/soul.ts +936 -0
- package/src/state.ts +119 -0
- package/src/surreal.ts +1371 -0
- package/src/tools/core-memory.ts +120 -0
- package/src/tools/introspect.ts +329 -0
- package/src/tools/recall.ts +102 -0
- package/src/wakeup.ts +318 -0
- package/src/workspace-migrate.ts +752 -0
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
|
+
}
|