kongbrain 0.4.1 → 0.4.3
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/.github/workflows/ci.yml +45 -0
- package/.github/workflows/pr-check.yml +16 -0
- package/CHANGELOG.md +64 -0
- package/README.github.md +40 -1
- package/SKILL.md +1 -1
- package/TOKEN_FLOW.md +184 -0
- package/package.json +1 -1
- package/src/acan.ts +28 -5
- package/src/causal.ts +18 -25
- package/src/cognitive-bootstrap.ts +6 -6
- package/src/cognitive-check.ts +17 -19
- package/src/config.ts +1 -1
- package/src/context-engine.ts +105 -50
- package/src/daemon-manager.ts +70 -19
- package/src/deferred-cleanup.ts +12 -10
- package/src/embeddings.ts +6 -7
- package/src/errors.ts +5 -3
- package/src/graph-context.ts +281 -178
- package/src/hooks/after-tool-call.ts +2 -1
- package/src/hooks/before-tool-call.ts +15 -11
- package/src/hooks/llm-output.ts +18 -10
- package/src/index.ts +39 -18
- package/src/intent.ts +9 -8
- package/src/log.ts +11 -0
- package/src/memory-daemon.ts +1 -0
- package/src/orchestrator.ts +11 -4
- package/src/prefetch.ts +2 -2
- package/src/reflection.ts +9 -2
- package/src/schema.surql +7 -0
- package/src/skills.ts +32 -10
- package/src/soul.ts +17 -1
- package/src/state.ts +31 -0
- package/src/supersedes.ts +99 -0
- package/src/surreal.ts +174 -110
- package/src/tools/introspect.ts +1 -1
- package/src/wakeup.ts +0 -142
|
@@ -68,7 +68,8 @@ export function createAfterToolCallHandler(state: GlobalPluginState) {
|
|
|
68
68
|
|
|
69
69
|
// Auto-track file artifacts from write/edit tools
|
|
70
70
|
if (!isError) {
|
|
71
|
-
|
|
71
|
+
// Fire-and-forget: artifact tracking is best-effort enrichment, not critical path
|
|
72
|
+
trackArtifact(event.toolName, event.params, session.taskId, session.projectId, state)
|
|
72
73
|
.catch(e => swallow.warn("hook:afterToolCall:artifact", e));
|
|
73
74
|
}
|
|
74
75
|
|
|
@@ -10,7 +10,7 @@ import type { GlobalPluginState } from "../state.js";
|
|
|
10
10
|
import { recordToolCall } from "../orchestrator.js";
|
|
11
11
|
import { cosineSimilarity } from "../graph-context.js";
|
|
12
12
|
|
|
13
|
-
const DEFAULT_TOOL_LIMIT =
|
|
13
|
+
const DEFAULT_TOOL_LIMIT = 9;
|
|
14
14
|
const CLASSIFICATION_LIMITS: Record<string, number> = { LOOKUP: 3, EDIT: 4, REFACTOR: 8 };
|
|
15
15
|
const API_CYCLE_CAP = 16;
|
|
16
16
|
const RECALL_SIMILARITY_THRESHOLD = 0.80;
|
|
@@ -59,7 +59,7 @@ export function createBeforeToolCallHandler(state: GlobalPluginState) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
// Tool limit
|
|
62
|
-
if (session.toolCallCount
|
|
62
|
+
if (session.toolCallCount >= session.toolLimit) {
|
|
63
63
|
return {
|
|
64
64
|
block: true,
|
|
65
65
|
blockReason: `Tool call limit reached (${session.toolLimit}). Stop calling tools. Continue exactly where you left off — deliver your answer from what you've gathered. Do NOT repeat anything you already said. State what's done and what remains.`,
|
|
@@ -100,22 +100,26 @@ export function createBeforeToolCallHandler(state: GlobalPluginState) {
|
|
|
100
100
|
// Planning gate: model must output text before first tool call
|
|
101
101
|
if (textLengthSoFar === 0 && toolIndex === 0) {
|
|
102
102
|
const retrievalNote = session.lastRetrievalSummary
|
|
103
|
-
?
|
|
103
|
+
? ` Context: ${session.lastRetrievalSummary}.`
|
|
104
104
|
: "";
|
|
105
105
|
return {
|
|
106
106
|
block: true,
|
|
107
107
|
blockReason:
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
|
|
111
|
-
retrievalNote + "\n" +
|
|
112
|
-
"3. List each planned call and what SPECIFIC GAP it fills that memory doesn't cover\n" +
|
|
113
|
-
"4. Every step still happens, but COMBINED. Edit + test in one bash call, not two.\n" +
|
|
114
|
-
"If injected context already answers the question, you may need ZERO tool calls.\n" +
|
|
115
|
-
"Speak your plan, then proceed.",
|
|
108
|
+
"Plan before tools. Classify (LOOKUP/EDIT/REFACTOR), state what you know from <graph_context>," +
|
|
109
|
+
" list each call + what gap it fills. Combine steps. 0 calls if context answers it." +
|
|
110
|
+
retrievalNote,
|
|
116
111
|
};
|
|
117
112
|
}
|
|
118
113
|
|
|
114
|
+
// Inline classification: if text was emitted with a classification keyword,
|
|
115
|
+
// parse and apply the tool limit (even on first tool call after text)
|
|
116
|
+
if (toolIndex === 0 && textLengthSoFar > 0 && session.toolLimit === DEFAULT_TOOL_LIMIT) {
|
|
117
|
+
const parsed = parseClassificationFromText(session.lastAssistantText ?? "");
|
|
118
|
+
if (parsed !== null) {
|
|
119
|
+
session.toolLimit = parsed;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
119
123
|
return undefined;
|
|
120
124
|
};
|
|
121
125
|
}
|
package/src/hooks/llm-output.ts
CHANGED
|
@@ -61,16 +61,24 @@ export function createLlmOutputHandler(state: GlobalPluginState) {
|
|
|
61
61
|
? Math.round(deltaTokens * (reportedOutput / reportedTotal))
|
|
62
62
|
: (deltaTokens > 0 ? deltaTokens : Math.ceil(textLen / 4));
|
|
63
63
|
|
|
64
|
-
//
|
|
64
|
+
// Batch session stats writes — accumulate in-memory, flush every 5th response
|
|
65
65
|
if (session.surrealSessionId) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
66
|
+
session._pendingInputTokens = (session._pendingInputTokens ?? 0) + inputTokens;
|
|
67
|
+
session._pendingOutputTokens = (session._pendingOutputTokens ?? 0) + outputTokens;
|
|
68
|
+
session._statsFlushCounter = (session._statsFlushCounter ?? 0) + 1;
|
|
69
|
+
if (session._statsFlushCounter >= 5) {
|
|
70
|
+
try {
|
|
71
|
+
await state.store.updateSessionStats(
|
|
72
|
+
session.surrealSessionId,
|
|
73
|
+
session._pendingInputTokens,
|
|
74
|
+
session._pendingOutputTokens,
|
|
75
|
+
);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
swallow("hook:llmOutput:sessionStats", e);
|
|
78
|
+
}
|
|
79
|
+
session._pendingInputTokens = 0;
|
|
80
|
+
session._pendingOutputTokens = 0;
|
|
81
|
+
session._statsFlushCounter = 0;
|
|
74
82
|
}
|
|
75
83
|
}
|
|
76
84
|
|
|
@@ -95,7 +103,7 @@ export function createLlmOutputHandler(state: GlobalPluginState) {
|
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
// Capture thinking blocks for monologue extraction
|
|
98
|
-
const lastAssistant = event.lastAssistant as
|
|
106
|
+
const lastAssistant = event.lastAssistant as { content?: { type: string; thinking?: string; text?: string }[] } | undefined;
|
|
99
107
|
if (lastAssistant?.content && Array.isArray(lastAssistant.content)) {
|
|
100
108
|
for (const block of lastAssistant.content) {
|
|
101
109
|
if (block.type === "thinking") {
|
package/src/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { hasMigratableFiles, migrateWorkspace } from "./workspace-migrate.js";
|
|
|
33
33
|
import { writeHandoffFileSync } from "./handoff-file.js";
|
|
34
34
|
import { runDeferredCleanup } from "./deferred-cleanup.js";
|
|
35
35
|
import { swallow } from "./errors.js";
|
|
36
|
+
import { log } from "./log.js";
|
|
36
37
|
|
|
37
38
|
// Use process-global symbols so state survives Jiti re-importing the module.
|
|
38
39
|
// Jiti may load this file multiple times (fresh module scope each time),
|
|
@@ -40,17 +41,20 @@ import { swallow } from "./errors.js";
|
|
|
40
41
|
const GLOBAL_KEY = Symbol.for("kongbrain.globalState");
|
|
41
42
|
const REGISTERED_KEY = Symbol.for("kongbrain.registered");
|
|
42
43
|
|
|
44
|
+
// Typed accessor for process-global symbol keys on globalThis
|
|
45
|
+
const _g = globalThis as Record<symbol, unknown>;
|
|
46
|
+
|
|
43
47
|
function getGlobalState(): GlobalPluginState | null {
|
|
44
|
-
return (
|
|
48
|
+
return (_g[GLOBAL_KEY] as GlobalPluginState) ?? null;
|
|
45
49
|
}
|
|
46
50
|
function setGlobalState(state: GlobalPluginState): void {
|
|
47
|
-
|
|
51
|
+
_g[GLOBAL_KEY] = state;
|
|
48
52
|
}
|
|
49
53
|
function isRegistered(): boolean {
|
|
50
|
-
return
|
|
54
|
+
return _g[REGISTERED_KEY] === true;
|
|
51
55
|
}
|
|
52
56
|
function markRegistered(): void {
|
|
53
|
-
|
|
57
|
+
_g[REGISTERED_KEY] = true;
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
let shutdownPromise: Promise<void> | null = null;
|
|
@@ -107,7 +111,7 @@ async function runSessionCleanup(
|
|
|
107
111
|
const turnData = recentTurns.map(t => ({
|
|
108
112
|
role: t.role as "user" | "assistant",
|
|
109
113
|
text: t.text,
|
|
110
|
-
turnId: (t as
|
|
114
|
+
turnId: (t as { id?: string }).id,
|
|
111
115
|
}));
|
|
112
116
|
session.daemon!.sendTurnBatch(turnData, [...session.pendingThinking], []);
|
|
113
117
|
} catch (e) { swallow.warn("cleanup:finalDaemonFlush", e); }
|
|
@@ -278,7 +282,7 @@ async function detectGraduationEvent(
|
|
|
278
282
|
}
|
|
279
283
|
|
|
280
284
|
// Flag the session for context engine injection
|
|
281
|
-
|
|
285
|
+
session._graduationCelebration = {
|
|
282
286
|
qualityScore: event.quality_score,
|
|
283
287
|
volumeScore: event.volume_score,
|
|
284
288
|
soulSummary,
|
|
@@ -333,10 +337,8 @@ export default definePluginEntry({
|
|
|
333
337
|
}
|
|
334
338
|
|
|
335
339
|
const complete: CompleteFn = async (params) => {
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
return apiRef.runtime.complete(params);
|
|
339
|
-
}
|
|
340
|
+
// NOTE: runtime.complete exists in 2026.4.2 but fails for plugin-initiated
|
|
341
|
+
// calls with "Profile anthropic:default timed out" — use pi-ai directly instead.
|
|
340
342
|
if (!piAi) {
|
|
341
343
|
if (!piAiPath) {
|
|
342
344
|
throw new Error("LLM completion not available: @mariozechner/pi-ai not found and runtime.complete missing");
|
|
@@ -345,8 +347,20 @@ export default definePluginEntry({
|
|
|
345
347
|
}
|
|
346
348
|
// Fall back to calling pi-ai directly (runtime.complete not in OpenClaw 2026.3.24)
|
|
347
349
|
const provider = params.provider ?? apiRef.runtime.agent.defaults.provider;
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
+
const rawModel = params.model ?? apiRef.runtime.agent.defaults.model;
|
|
351
|
+
// defaults.model may be an object {primary: '...', fallbacks: []} — unwrap it
|
|
352
|
+
const modelIdRaw = typeof rawModel === 'object' && rawModel !== null
|
|
353
|
+
? (rawModel as any).primary ?? (rawModel as any).id ?? String(rawModel)
|
|
354
|
+
: rawModel;
|
|
355
|
+
// modelId may be "provider/model" format — split if provider not set
|
|
356
|
+
let resolvedProvider = provider;
|
|
357
|
+
let modelId = modelIdRaw;
|
|
358
|
+
if (typeof modelId === 'string' && modelId.includes('/') && !resolvedProvider) {
|
|
359
|
+
const idx = modelId.indexOf('/');
|
|
360
|
+
resolvedProvider = modelId.slice(0, idx);
|
|
361
|
+
modelId = modelId.slice(idx + 1);
|
|
362
|
+
}
|
|
363
|
+
const model = piAi!.getModel(resolvedProvider, modelId);
|
|
350
364
|
if (!model) {
|
|
351
365
|
throw new Error(`Model "${modelId}" not found for provider "${provider}"`);
|
|
352
366
|
}
|
|
@@ -365,14 +379,21 @@ export default definePluginEntry({
|
|
|
365
379
|
);
|
|
366
380
|
const context = { systemPrompt: params.system, messages };
|
|
367
381
|
// Pass apiKey directly in options so the provider can use it
|
|
382
|
+
log.info(`complete(): provider=${resolvedProvider} model=${modelId} msgs=${params.messages.length}`);
|
|
383
|
+
// NOTE: outputFormat (structured output) is intentionally NOT passed to pi-ai.
|
|
384
|
+
// pi-ai's SimpleStreamOptions doesn't support it, and injecting it via onPayload
|
|
385
|
+
// causes the Anthropic API to return empty responses. The daemon's JSON parsing
|
|
386
|
+
// cascade (direct parse → greedy regex → trailing comma fix → field-by-field)
|
|
387
|
+
// handles free-text JSON extraction reliably without structured output.
|
|
368
388
|
const response = await piAi!.completeSimple(model, context, {
|
|
369
389
|
apiKey: auth.apiKey,
|
|
370
390
|
});
|
|
391
|
+
log.info(`complete(): blocks=${response.content?.length} stop=${response.stopReason}`);
|
|
371
392
|
let text = "";
|
|
372
393
|
let thinking: string | undefined;
|
|
373
394
|
for (const block of response.content) {
|
|
374
395
|
if (block.type === "text") text += block.text;
|
|
375
|
-
else if ((block as
|
|
396
|
+
else if ((block as { type: string; thinking?: string }).type === "thinking") thinking = (thinking ?? "") + (block as { type: string; thinking?: string }).thinking;
|
|
376
397
|
}
|
|
377
398
|
return { text, thinking, usage: { input: response.usage.input, output: response.usage.output } };
|
|
378
399
|
};
|
|
@@ -478,7 +499,7 @@ export default definePluginEntry({
|
|
|
478
499
|
hasMigratableFiles(globalState!.workspaceDir)
|
|
479
500
|
.then(hasMigratable => {
|
|
480
501
|
if (hasMigratable) {
|
|
481
|
-
|
|
502
|
+
session._hasMigratableFiles = true;
|
|
482
503
|
}
|
|
483
504
|
})
|
|
484
505
|
.catch(e => swallow.warn("index:migrationCheck", e));
|
|
@@ -492,7 +513,7 @@ export default definePluginEntry({
|
|
|
492
513
|
.catch(e => swallow("index:graduationDetect", e));
|
|
493
514
|
|
|
494
515
|
// Synthesize wakeup briefing — store the promise so assemble() can await it
|
|
495
|
-
|
|
516
|
+
session._wakeupPromise = synthesizeWakeup(
|
|
496
517
|
globalState!.store, globalState!.complete, session.sessionId, globalState!.workspaceDir,
|
|
497
518
|
).catch(e => { swallow.warn("index:wakeup", e); return null; });
|
|
498
519
|
|
|
@@ -541,7 +562,7 @@ export default definePluginEntry({
|
|
|
541
562
|
const syncExitHandler = () => {
|
|
542
563
|
const gs = getGlobalState();
|
|
543
564
|
if (!gs?.workspaceDir) return;
|
|
544
|
-
const sessions =
|
|
565
|
+
const sessions = gs.allSessions();
|
|
545
566
|
for (const session of sessions) {
|
|
546
567
|
if (session.cleanedUp) continue;
|
|
547
568
|
writeHandoffFileSync({
|
|
@@ -559,14 +580,14 @@ export default definePluginEntry({
|
|
|
559
580
|
const asyncExitHandler = () => {
|
|
560
581
|
const gs = getGlobalState();
|
|
561
582
|
if (!gs) return;
|
|
562
|
-
const sessions =
|
|
583
|
+
const sessions = gs.allSessions();
|
|
563
584
|
if (sessions.length === 0 && !shutdownPromise) return;
|
|
564
585
|
|
|
565
586
|
const cleanups = sessions.map(s => runSessionCleanup(s, gs));
|
|
566
587
|
if (shutdownPromise) cleanups.push(shutdownPromise);
|
|
567
588
|
|
|
568
589
|
const done = Promise.allSettled(cleanups).then(() => {
|
|
569
|
-
gs.shutdown().catch(
|
|
590
|
+
gs.shutdown().catch(e => log.error("shutdown error:", e));
|
|
570
591
|
});
|
|
571
592
|
|
|
572
593
|
done.then(() => process.exit(0)).catch(() => process.exit(1));
|
package/src/intent.ts
CHANGED
|
@@ -92,20 +92,21 @@ async function ensurePrototypes(embeddings: EmbeddingService): Promise<{ categor
|
|
|
92
92
|
let promise = centroidInitPromise.get(embeddings);
|
|
93
93
|
if (!promise) {
|
|
94
94
|
promise = (async () => {
|
|
95
|
+
const vecs = await embeddings.embedBatch(PROTOTYPES.map(p => p.text));
|
|
95
96
|
const byCategory = new Map<IntentCategory, number[][]>();
|
|
96
|
-
for (
|
|
97
|
-
const
|
|
98
|
-
if (!byCategory.has(
|
|
99
|
-
byCategory.get(
|
|
97
|
+
for (let i = 0; i < PROTOTYPES.length; i++) {
|
|
98
|
+
const cat = PROTOTYPES[i].category;
|
|
99
|
+
if (!byCategory.has(cat)) byCategory.set(cat, []);
|
|
100
|
+
byCategory.get(cat)!.push(vecs[i]);
|
|
100
101
|
}
|
|
101
102
|
const centroids: { category: IntentCategory; vec: number[] }[] = [];
|
|
102
|
-
for (const [category,
|
|
103
|
-
const dim =
|
|
103
|
+
for (const [category, catVecs] of byCategory) {
|
|
104
|
+
const dim = catVecs[0].length;
|
|
104
105
|
const centroid = new Array(dim).fill(0);
|
|
105
|
-
for (const v of
|
|
106
|
+
for (const v of catVecs) {
|
|
106
107
|
for (let d = 0; d < dim; d++) centroid[d] += v[d];
|
|
107
108
|
}
|
|
108
|
-
for (let d = 0; d < dim; d++) centroid[d] /=
|
|
109
|
+
for (let d = 0; d < dim; d++) centroid[d] /= catVecs.length;
|
|
109
110
|
centroids.push({ category, vec: centroid });
|
|
110
111
|
}
|
|
111
112
|
centroidCache.set(embeddings, centroids);
|
package/src/log.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const LEVELS = { error: 0, warn: 1, info: 2, debug: 3 } as const;
|
|
2
|
+
type Level = keyof typeof LEVELS;
|
|
3
|
+
|
|
4
|
+
const currentLevel: Level = (process.env.KONGBRAIN_LOG_LEVEL as Level) ?? "warn";
|
|
5
|
+
|
|
6
|
+
export const log = {
|
|
7
|
+
error: (...args: unknown[]) => { if (LEVELS[currentLevel] >= LEVELS.error) console.error("[kongbrain]", ...args); },
|
|
8
|
+
warn: (...args: unknown[]) => { if (LEVELS[currentLevel] >= LEVELS.warn) console.warn("[kongbrain]", ...args); },
|
|
9
|
+
info: (...args: unknown[]) => { if (LEVELS[currentLevel] >= LEVELS.info) console.info("[kongbrain]", ...args); },
|
|
10
|
+
debug: (...args: unknown[]) => { if (LEVELS[currentLevel] >= LEVELS.debug) console.debug("[kongbrain]", ...args); },
|
|
11
|
+
};
|
package/src/memory-daemon.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { EmbeddingService } from "./embeddings.js";
|
|
|
14
14
|
import { swallow } from "./errors.js";
|
|
15
15
|
import { assertRecordId } from "./surreal.js";
|
|
16
16
|
import { linkConceptHierarchy, linkToRelevantConcepts } from "./concept-extract.js";
|
|
17
|
+
import { linkSupersedesEdges } from "./supersedes.js";
|
|
17
18
|
|
|
18
19
|
// --- Build the extraction prompt ---
|
|
19
20
|
|
package/src/orchestrator.ts
CHANGED
|
@@ -141,6 +141,8 @@ interface OrchestratorSessionState {
|
|
|
141
141
|
turnIndex: number;
|
|
142
142
|
currentTurnTools: { name: string; args?: string }[];
|
|
143
143
|
steeringCandidates: SteeringCandidate[];
|
|
144
|
+
cachedUtilAvg: number | null;
|
|
145
|
+
utilAvgTurn: number;
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
const sessionOrchState = new WeakMap<SessionState, OrchestratorSessionState>();
|
|
@@ -153,6 +155,8 @@ function getOrchState(session: SessionState): OrchestratorSessionState {
|
|
|
153
155
|
turnIndex: 0,
|
|
154
156
|
currentTurnTools: [],
|
|
155
157
|
steeringCandidates: [],
|
|
158
|
+
cachedUtilAvg: null,
|
|
159
|
+
utilAvgTurn: 0,
|
|
156
160
|
};
|
|
157
161
|
sessionOrchState.set(session, state);
|
|
158
162
|
}
|
|
@@ -252,11 +256,14 @@ export async function preflight(
|
|
|
252
256
|
config.toolLimit = Math.min(complexity.estimatedToolCalls, Math.ceil(config.toolLimit * 1.5), 20);
|
|
253
257
|
}
|
|
254
258
|
|
|
255
|
-
// Adaptive token budget from rolling retrieval quality
|
|
259
|
+
// Adaptive token budget from rolling retrieval quality (cached, refreshed every 10 turns)
|
|
256
260
|
if (!config.skipRetrieval) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
261
|
+
if (orch.cachedUtilAvg === null || orch.turnIndex - orch.utilAvgTurn >= 10) {
|
|
262
|
+
orch.cachedUtilAvg = await getRecentUtilizationAvg(session.sessionId, 10).catch(() => null);
|
|
263
|
+
orch.utilAvgTurn = orch.turnIndex;
|
|
264
|
+
}
|
|
265
|
+
if (orch.cachedUtilAvg !== null) {
|
|
266
|
+
const scale = Math.max(0.5, Math.min(1.3, 0.5 + orch.cachedUtilAvg * 0.8));
|
|
260
267
|
config.tokenBudget = Math.round(config.tokenBudget * scale);
|
|
261
268
|
}
|
|
262
269
|
}
|
package/src/prefetch.ts
CHANGED
|
@@ -115,7 +115,7 @@ export async function prefetchContext(
|
|
|
115
115
|
|
|
116
116
|
evictStale();
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
await Promise.all(queries.map(async (query) => {
|
|
119
119
|
try {
|
|
120
120
|
const queryVec = await embeddings.embed(query);
|
|
121
121
|
|
|
@@ -152,7 +152,7 @@ export async function prefetchContext(
|
|
|
152
152
|
} catch (e) {
|
|
153
153
|
swallow("prefetch:query", e);
|
|
154
154
|
}
|
|
155
|
-
}
|
|
155
|
+
}));
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
// --- Cache Lookup ---
|
package/src/reflection.ts
CHANGED
|
@@ -143,8 +143,15 @@ export async function generateReflection(
|
|
|
143
143
|
): Promise<void> {
|
|
144
144
|
if (!store.isAvailable()) return;
|
|
145
145
|
|
|
146
|
+
// Gate: only reflect if session metrics warrant it
|
|
147
|
+
const metrics = await gatherSessionMetrics(sessionId, store);
|
|
148
|
+
if (metrics) {
|
|
149
|
+
const { reflect } = shouldReflect(metrics);
|
|
150
|
+
if (!reflect) return;
|
|
151
|
+
}
|
|
152
|
+
|
|
146
153
|
// Get session turns directly — no dependency on orchestrator_metrics
|
|
147
|
-
const turns = await store.getSessionTurns(sessionId,
|
|
154
|
+
const turns = await store.getSessionTurns(sessionId, 15).catch(() => []);
|
|
148
155
|
if (turns.length < 3) return; // Too short for meaningful reflection
|
|
149
156
|
|
|
150
157
|
const transcript = turns
|
|
@@ -183,7 +190,7 @@ export async function generateReflection(
|
|
|
183
190
|
{ vec: reflEmb },
|
|
184
191
|
);
|
|
185
192
|
const top = existing[0];
|
|
186
|
-
if (top &&
|
|
193
|
+
if (top && typeof top.score === "number" && top.score > 0.85) {
|
|
187
194
|
const newImportance = Math.min(10, (top.importance ?? 7) + 0.5);
|
|
188
195
|
await store.queryFirst<any>(
|
|
189
196
|
`UPDATE $id SET importance = $imp, updated_at = time::now()`,
|
package/src/schema.surql
CHANGED
|
@@ -42,6 +42,7 @@ DEFINE FIELD IF NOT EXISTS embedding ON artifact TYPE option<array<float>>;
|
|
|
42
42
|
DEFINE FIELD IF NOT EXISTS tags ON artifact TYPE option<array>;
|
|
43
43
|
DEFINE FIELD IF NOT EXISTS created_at ON artifact TYPE datetime DEFAULT time::now();
|
|
44
44
|
DEFINE INDEX IF NOT EXISTS artifact_vec_idx ON artifact FIELDS embedding HNSW DIMENSION 1024 DIST COSINE;
|
|
45
|
+
DEFINE INDEX IF NOT EXISTS artifact_type_idx ON artifact FIELDS type;
|
|
45
46
|
|
|
46
47
|
-- ============================================================
|
|
47
48
|
-- PILLAR 5: Concept (semantic knowledge nodes)
|
|
@@ -78,6 +79,7 @@ DEFINE FIELD IF NOT EXISTS model ON turn TYPE option<string>;
|
|
|
78
79
|
DEFINE FIELD IF NOT EXISTS usage ON turn TYPE option<object>;
|
|
79
80
|
DEFINE INDEX IF NOT EXISTS turn_vec_idx ON turn FIELDS embedding HNSW DIMENSION 1024 DIST COSINE;
|
|
80
81
|
DEFINE INDEX IF NOT EXISTS turn_session_idx ON turn FIELDS session_id;
|
|
82
|
+
DEFINE INDEX IF NOT EXISTS turn_tool_name_idx ON turn FIELDS tool_name;
|
|
81
83
|
|
|
82
84
|
-- Identity chunks (agent persona / identity)
|
|
83
85
|
DEFINE TABLE IF NOT EXISTS identity_chunk SCHEMALESS;
|
|
@@ -115,6 +117,7 @@ DEFINE FIELD IF NOT EXISTS status ON memory TYPE option<string> DEFAULT "active"
|
|
|
115
117
|
DEFINE FIELD IF NOT EXISTS resolved_at ON memory TYPE option<datetime>;
|
|
116
118
|
DEFINE FIELD IF NOT EXISTS resolved_by ON memory TYPE option<string>;
|
|
117
119
|
DEFINE INDEX IF NOT EXISTS memory_vec_idx ON memory FIELDS embedding HNSW DIMENSION 1024 DIST COSINE;
|
|
120
|
+
DEFINE INDEX IF NOT EXISTS memory_category_idx ON memory FIELDS category;
|
|
118
121
|
|
|
119
122
|
-- ============================================================
|
|
120
123
|
-- GRAPH EDGES: Turn-level
|
|
@@ -150,6 +153,9 @@ DEFINE TABLE IF NOT EXISTS supports TYPE RELATION IN memory OUT memory;
|
|
|
150
153
|
DEFINE TABLE IF NOT EXISTS contradicts TYPE RELATION IN memory OUT memory;
|
|
151
154
|
DEFINE TABLE IF NOT EXISTS describes TYPE RELATION IN memory OUT memory;
|
|
152
155
|
|
|
156
|
+
-- Concept evolution
|
|
157
|
+
DEFINE TABLE IF NOT EXISTS supersedes TYPE RELATION IN memory OUT concept;
|
|
158
|
+
|
|
153
159
|
-- Cross-pillar links
|
|
154
160
|
DEFINE TABLE IF NOT EXISTS about_concept TYPE RELATION IN memory OUT concept;
|
|
155
161
|
DEFINE TABLE IF NOT EXISTS artifact_mentions TYPE RELATION IN artifact OUT concept;
|
|
@@ -260,6 +266,7 @@ DEFINE FIELD IF NOT EXISTS avg_duration_ms ON skill TYPE float DEFAULT 0.0;
|
|
|
260
266
|
DEFINE FIELD IF NOT EXISTS last_used ON skill TYPE option<datetime>;
|
|
261
267
|
DEFINE FIELD IF NOT EXISTS created_at ON skill TYPE datetime DEFAULT time::now();
|
|
262
268
|
DEFINE INDEX IF NOT EXISTS skill_vec_idx ON skill FIELDS embedding HNSW DIMENSION 1024 DIST COSINE;
|
|
269
|
+
DEFINE INDEX IF NOT EXISTS skill_active_idx ON skill FIELDS active;
|
|
263
270
|
|
|
264
271
|
DEFINE TABLE IF NOT EXISTS skill_from_task TYPE RELATION IN skill OUT task;
|
|
265
272
|
DEFINE TABLE IF NOT EXISTS skill_uses_concept TYPE RELATION IN skill OUT concept;
|
package/src/skills.ts
CHANGED
|
@@ -16,6 +16,20 @@ import { swallow } from "./errors.js";
|
|
|
16
16
|
import { linkToRelevantConcepts } from "./concept-extract.js";
|
|
17
17
|
import { assertRecordId } from "./surreal.js";
|
|
18
18
|
|
|
19
|
+
// --- Shared schema for structured output ---
|
|
20
|
+
|
|
21
|
+
const skillSchema = {
|
|
22
|
+
type: "object" as const,
|
|
23
|
+
properties: {
|
|
24
|
+
name: { type: "string" },
|
|
25
|
+
description: { type: "string" },
|
|
26
|
+
preconditions: { type: "string" },
|
|
27
|
+
steps: { type: "array", items: { type: "object", properties: { tool: { type: "string" }, description: { type: "string" } } } },
|
|
28
|
+
postconditions: { type: "string" },
|
|
29
|
+
},
|
|
30
|
+
required: ["name", "description", "steps"],
|
|
31
|
+
};
|
|
32
|
+
|
|
19
33
|
// --- Types ---
|
|
20
34
|
|
|
21
35
|
export interface SkillStep {
|
|
@@ -71,21 +85,27 @@ export async function extractSkill(
|
|
|
71
85
|
|
|
72
86
|
try {
|
|
73
87
|
const response = await complete({
|
|
74
|
-
system: `
|
|
88
|
+
system: `Extract a reusable skill procedure. Generic patterns only (no specific paths). Return null if no clear multi-step workflow.`,
|
|
75
89
|
messages: [{
|
|
76
90
|
role: "user",
|
|
77
91
|
content: `${turns.length} turns:\n${transcript.slice(0, 20000)}`,
|
|
78
92
|
}],
|
|
93
|
+
outputFormat: { type: "json_schema", schema: skillSchema },
|
|
79
94
|
});
|
|
80
95
|
|
|
81
96
|
const text = response.text;
|
|
82
97
|
|
|
83
98
|
if (text.trim() === "null" || text.trim() === "None") return null;
|
|
84
99
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
// Try direct JSON.parse first (structured output), fall back to regex extraction
|
|
101
|
+
let parsed: ExtractedSkill;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(text);
|
|
104
|
+
} catch {
|
|
105
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/); // greedy — handles nested objects
|
|
106
|
+
if (!jsonMatch) return null;
|
|
107
|
+
parsed = JSON.parse(jsonMatch[0]) as ExtractedSkill;
|
|
108
|
+
}
|
|
89
109
|
if (!parsed.name || !parsed.description || !Array.isArray(parsed.steps) || parsed.steps.length === 0) {
|
|
90
110
|
return null;
|
|
91
111
|
}
|
|
@@ -290,19 +310,21 @@ export async function graduateCausalToSkills(
|
|
|
290
310
|
if (existing.length > 0) continue;
|
|
291
311
|
|
|
292
312
|
const resp = await complete({
|
|
293
|
-
system: `
|
|
313
|
+
system: `Synthesize a reusable procedure from recurring patterns. Generic — no specific file paths or variable names.`,
|
|
294
314
|
messages: [{
|
|
295
315
|
role: "user",
|
|
296
316
|
content: `${group.cnt} successful "${group.chain_type}" patterns:\n${group.descriptions.slice(0, 8).join("\n")}`,
|
|
297
317
|
}],
|
|
318
|
+
outputFormat: { type: "json_schema", schema: skillSchema },
|
|
298
319
|
});
|
|
299
320
|
|
|
300
321
|
const text = resp.text;
|
|
301
|
-
const jsonMatch = text.match(/\{[\s\S]*?\}/);
|
|
302
|
-
if (!jsonMatch) continue;
|
|
303
|
-
|
|
304
322
|
let parsed: ExtractedSkill;
|
|
305
|
-
try { parsed = JSON.parse(
|
|
323
|
+
try { parsed = JSON.parse(text); } catch {
|
|
324
|
+
const jsonMatch = text.match(/\{[\s\S]*?\}/);
|
|
325
|
+
if (!jsonMatch) continue;
|
|
326
|
+
try { parsed = JSON.parse(jsonMatch[0]); } catch { continue; }
|
|
327
|
+
}
|
|
306
328
|
if (!parsed.name || !Array.isArray(parsed.steps) || parsed.steps.length === 0) continue;
|
|
307
329
|
|
|
308
330
|
let skillEmb: number[] | null = null;
|
package/src/soul.ts
CHANGED
|
@@ -551,16 +551,32 @@ Output ONLY valid JSON:
|
|
|
551
551
|
Be honest, not aspirational. Only claim what the data supports.`;
|
|
552
552
|
|
|
553
553
|
try {
|
|
554
|
+
const soulSchema = {
|
|
555
|
+
type: "object" as const,
|
|
556
|
+
properties: {
|
|
557
|
+
working_style: { type: "array", items: { type: "string" } },
|
|
558
|
+
emotional_dimensions: { type: "array", items: { type: "object" } },
|
|
559
|
+
self_observations: { type: "array", items: { type: "string" } },
|
|
560
|
+
earned_values: { type: "array", items: { type: "object" } },
|
|
561
|
+
},
|
|
562
|
+
required: ["working_style", "emotional_dimensions", "self_observations", "earned_values"],
|
|
563
|
+
};
|
|
564
|
+
|
|
554
565
|
const response = await complete({
|
|
555
566
|
system: "You are introspecting on your own experience to write a self-assessment. Be genuine and grounded.",
|
|
556
567
|
messages: [{
|
|
557
568
|
role: "user",
|
|
558
569
|
content: prompt,
|
|
559
570
|
}],
|
|
571
|
+
outputFormat: { type: "json_schema", schema: soulSchema },
|
|
560
572
|
});
|
|
561
573
|
|
|
562
574
|
const text = response.text.trim();
|
|
563
|
-
|
|
575
|
+
// With structured output, response should be valid JSON directly
|
|
576
|
+
let jsonMatch: RegExpMatchArray | null;
|
|
577
|
+
try { JSON.parse(text); jsonMatch = [text]; } catch {
|
|
578
|
+
jsonMatch = text.match(/\{[\s\S]*?\}/);
|
|
579
|
+
}
|
|
564
580
|
if (!jsonMatch) return null;
|
|
565
581
|
|
|
566
582
|
const parsed = JSON.parse(jsonMatch[0]);
|
package/src/state.ts
CHANGED
|
@@ -4,6 +4,12 @@ import type { EmbeddingService } from "./embeddings.js";
|
|
|
4
4
|
import type { AdaptiveConfig } from "./orchestrator.js";
|
|
5
5
|
import type { MemoryDaemon } from "./daemon-manager.js";
|
|
6
6
|
|
|
7
|
+
/** JSON schema for structured output (forces API to return valid JSON matching schema). */
|
|
8
|
+
export type OutputFormat = {
|
|
9
|
+
type: "json_schema";
|
|
10
|
+
schema: Record<string, unknown>;
|
|
11
|
+
};
|
|
12
|
+
|
|
7
13
|
/** Parameters for an LLM completion call. */
|
|
8
14
|
export type CompleteParams = {
|
|
9
15
|
system?: string;
|
|
@@ -13,6 +19,8 @@ export type CompleteParams = {
|
|
|
13
19
|
temperature?: number;
|
|
14
20
|
maxTokens?: number;
|
|
15
21
|
reasoning?: "none" | "low" | "medium" | "high";
|
|
22
|
+
/** When set, API returns structured JSON matching the schema (no markdown, no preamble). */
|
|
23
|
+
outputFormat?: OutputFormat;
|
|
16
24
|
};
|
|
17
25
|
|
|
18
26
|
/** Result of an LLM completion call. */
|
|
@@ -32,6 +40,7 @@ export type CompleteFn = (params: CompleteParams) => Promise<CompleteResult>;
|
|
|
32
40
|
|
|
33
41
|
const DEFAULT_TOOL_LIMIT = 10;
|
|
34
42
|
|
|
43
|
+
/** Per-session mutable state: turn counters, daemon refs, 5-pillar IDs, and adaptive config. */
|
|
35
44
|
export class SessionState {
|
|
36
45
|
readonly sessionId: string;
|
|
37
46
|
readonly sessionKey: string;
|
|
@@ -41,6 +50,8 @@ export class SessionState {
|
|
|
41
50
|
lastAssistantTurnId = "";
|
|
42
51
|
lastUserText = "";
|
|
43
52
|
lastAssistantText = "";
|
|
53
|
+
/** Embedding of last user message from ingest — reused in buildContextualQueryVec to avoid re-embedding. */
|
|
54
|
+
lastUserEmbedding: number[] | null = null;
|
|
44
55
|
toolCallCount = 0;
|
|
45
56
|
toolLimit = DEFAULT_TOOL_LIMIT;
|
|
46
57
|
turnTextLength = 0;
|
|
@@ -92,6 +103,20 @@ export class SessionState {
|
|
|
92
103
|
taskId = "";
|
|
93
104
|
surrealSessionId = "";
|
|
94
105
|
|
|
106
|
+
// Cross-concern state (set by index.ts hooks, consumed by context-engine.ts assemble)
|
|
107
|
+
/** Structured summary stashed after compaction for next assemble() injection. */
|
|
108
|
+
_compactionSummary?: string;
|
|
109
|
+
/** Promise resolving to wakeup briefing text (synthesized at session start). */
|
|
110
|
+
_wakeupPromise?: Promise<string | null>;
|
|
111
|
+
/** Graduation celebration payload for context injection. */
|
|
112
|
+
_graduationCelebration?: {
|
|
113
|
+
qualityScore: number;
|
|
114
|
+
volumeScore: number;
|
|
115
|
+
soulSummary: string;
|
|
116
|
+
};
|
|
117
|
+
/** Whether workspace has files from the default context engine that can be migrated. */
|
|
118
|
+
_hasMigratableFiles?: boolean;
|
|
119
|
+
|
|
95
120
|
constructor(sessionId: string, sessionKey: string) {
|
|
96
121
|
this.sessionId = sessionId;
|
|
97
122
|
this.sessionKey = sessionKey;
|
|
@@ -118,6 +143,7 @@ export class SessionState {
|
|
|
118
143
|
/** Function to enqueue a system event visible to the user. */
|
|
119
144
|
export type EnqueueSystemEventFn = (text: string, options: { sessionKey: string }) => boolean;
|
|
120
145
|
|
|
146
|
+
/** Singleton shared state: config, SurrealDB store, embedding service, and session map. */
|
|
121
147
|
export class GlobalPluginState {
|
|
122
148
|
readonly config: KongBrainConfig;
|
|
123
149
|
readonly store: SurrealStore;
|
|
@@ -162,6 +188,11 @@ export class GlobalPluginState {
|
|
|
162
188
|
this.sessions.delete(sessionKey);
|
|
163
189
|
}
|
|
164
190
|
|
|
191
|
+
/** Return all active sessions (for exit handlers). */
|
|
192
|
+
allSessions(): SessionState[] {
|
|
193
|
+
return [...this.sessions.values()];
|
|
194
|
+
}
|
|
195
|
+
|
|
165
196
|
/** Shut down all shared resources. */
|
|
166
197
|
async shutdown(): Promise<void> {
|
|
167
198
|
this.sessions.clear();
|