kongbrain 0.4.0 → 0.4.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/.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 +32 -6
- package/src/causal.ts +18 -25
- package/src/cognitive-bootstrap.ts +6 -6
- package/src/cognitive-check.ts +19 -21
- package/src/concept-extract.ts +1 -1
- package/src/config.ts +1 -1
- package/src/context-engine.ts +81 -48
- package/src/daemon-manager.ts +65 -25
- package/src/deferred-cleanup.ts +14 -16
- package/src/embeddings.ts +6 -7
- package/src/errors.ts +5 -3
- package/src/graph-context.ts +269 -173
- package/src/handoff-file.ts +12 -5
- package/src/hooks/after-tool-call.ts +3 -2
- package/src/hooks/before-tool-call.ts +15 -11
- package/src/hooks/llm-output.ts +18 -10
- package/src/index.ts +25 -14
- package/src/intent.ts +9 -8
- package/src/log.ts +11 -0
- package/src/orchestrator.ts +12 -5
- package/src/prefetch.ts +2 -2
- package/src/reflection.ts +10 -2
- package/src/schema.surql +4 -0
- package/src/skills.ts +32 -10
- package/src/soul.ts +18 -2
- package/src/state.ts +31 -0
- package/src/surreal.ts +138 -110
- package/src/tools/introspect.ts +1 -1
- package/src/tools/recall.ts +1 -1
- package/src/wakeup.ts +0 -142
package/src/handoff-file.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* so the next session's wakeup has context even before deferred
|
|
7
7
|
* extraction runs.
|
|
8
8
|
*/
|
|
9
|
-
import { readFileSync, writeFileSync, unlinkSync, existsSync,
|
|
9
|
+
import { readFileSync, writeFileSync, unlinkSync, existsSync, renameSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
|
|
12
12
|
const HANDOFF_FILENAME = ".kongbrain-handoff.json";
|
|
@@ -42,14 +42,21 @@ export function readAndDeleteHandoffFile(
|
|
|
42
42
|
workspaceDir: string,
|
|
43
43
|
): HandoffFileData | null {
|
|
44
44
|
const path = join(workspaceDir, HANDOFF_FILENAME);
|
|
45
|
+
const processingPath = path + ".processing";
|
|
46
|
+
// Also clean up stale .processing files from prior crashes
|
|
47
|
+
if (existsSync(processingPath) && !existsSync(path)) {
|
|
48
|
+
try { unlinkSync(processingPath); } catch { /* ignore */ }
|
|
49
|
+
}
|
|
45
50
|
if (!existsSync(path)) return null;
|
|
46
51
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
// Atomic rename first so a crash between read and delete can't re-process
|
|
53
|
+
renameSync(path, processingPath);
|
|
54
|
+
const raw = readFileSync(processingPath, "utf-8");
|
|
55
|
+
unlinkSync(processingPath);
|
|
49
56
|
const parsed = JSON.parse(raw);
|
|
50
57
|
// Runtime validation — reject prototype pollution and malformed data
|
|
51
58
|
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
52
|
-
if ("__proto__"
|
|
59
|
+
if (Object.hasOwn(parsed, "__proto__") || Object.hasOwn(parsed, "constructor")) return null;
|
|
53
60
|
const data: HandoffFileData = {
|
|
54
61
|
sessionId: typeof parsed.sessionId === "string" ? parsed.sessionId.slice(0, 200) : "",
|
|
55
62
|
timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp.slice(0, 50) : "",
|
|
@@ -61,7 +68,7 @@ export function readAndDeleteHandoffFile(
|
|
|
61
68
|
return data;
|
|
62
69
|
} catch {
|
|
63
70
|
// Corrupted or deleted between check and read
|
|
64
|
-
try { unlinkSync(
|
|
71
|
+
try { unlinkSync(processingPath); } catch { /* ignore */ }
|
|
65
72
|
return null;
|
|
66
73
|
}
|
|
67
74
|
}
|
|
@@ -54,7 +54,7 @@ export function createAfterToolCallHandler(state: GlobalPluginState) {
|
|
|
54
54
|
});
|
|
55
55
|
if (assistantTurnId) session.lastAssistantTurnId = assistantTurnId;
|
|
56
56
|
} catch (e) {
|
|
57
|
-
swallow("hook:afterToolCall:eagerAssistantTurn", e);
|
|
57
|
+
swallow.warn("hook:afterToolCall:eagerAssistantTurn", e);
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
if (session.lastAssistantTurnId) {
|
|
@@ -63,11 +63,12 @@ export function createAfterToolCallHandler(state: GlobalPluginState) {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
} catch (e) {
|
|
66
|
-
swallow("hook:afterToolCall:store", e);
|
|
66
|
+
swallow.warn("hook:afterToolCall:store", e);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// Auto-track file artifacts from write/edit tools
|
|
70
70
|
if (!isError) {
|
|
71
|
+
// Fire-and-forget: artifact tracking is best-effort enrichment, not critical path
|
|
71
72
|
trackArtifact(event.toolName, event.params, session.taskId, session.projectId, state)
|
|
72
73
|
.catch(e => swallow.warn("hook:afterToolCall:artifact", e));
|
|
73
74
|
}
|
|
@@ -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); }
|
|
@@ -147,11 +151,18 @@ async function runSessionCleanup(
|
|
|
147
151
|
new Promise(resolve => setTimeout(resolve, 150_000)),
|
|
148
152
|
]);
|
|
149
153
|
|
|
154
|
+
// Await the graduation promise once and reuse the result below
|
|
155
|
+
let gradResult: Awaited<typeof graduationPromise> = null;
|
|
156
|
+
try {
|
|
157
|
+
gradResult = await graduationPromise;
|
|
158
|
+
} catch (e) {
|
|
159
|
+
swallow.warn("cleanup:graduationAwait", e);
|
|
160
|
+
}
|
|
161
|
+
|
|
150
162
|
// If soul graduation just happened, persist a graduation event so the next
|
|
151
163
|
// session can celebrate with the user. We also fire a system event for
|
|
152
164
|
// immediate visibility if the session is still active.
|
|
153
165
|
try {
|
|
154
|
-
const gradResult = await graduationPromise;
|
|
155
166
|
if (gradResult?.graduated && gradResult.soul) {
|
|
156
167
|
// Check if this is a NEW graduation (not a pre-existing soul)
|
|
157
168
|
const isNewGraduation = gradResult.report.stage === "ready";
|
|
@@ -189,7 +200,6 @@ async function runSessionCleanup(
|
|
|
189
200
|
// Soul evolution — if soul already exists, check if it should be revised
|
|
190
201
|
// based on new experience (runs every 10 sessions after last revision)
|
|
191
202
|
try {
|
|
192
|
-
const gradResult = await graduationPromise;
|
|
193
203
|
if (gradResult?.graduated && gradResult.report.stage !== "ready") {
|
|
194
204
|
// Pre-existing soul — check for evolution
|
|
195
205
|
await evolveSoul(s, complete);
|
|
@@ -272,7 +282,7 @@ async function detectGraduationEvent(
|
|
|
272
282
|
}
|
|
273
283
|
|
|
274
284
|
// Flag the session for context engine injection
|
|
275
|
-
|
|
285
|
+
session._graduationCelebration = {
|
|
276
286
|
qualityScore: event.quality_score,
|
|
277
287
|
volumeScore: event.volume_score,
|
|
278
288
|
soulSummary,
|
|
@@ -361,12 +371,13 @@ export default definePluginEntry({
|
|
|
361
371
|
// Pass apiKey directly in options so the provider can use it
|
|
362
372
|
const response = await piAi!.completeSimple(model, context, {
|
|
363
373
|
apiKey: auth.apiKey,
|
|
374
|
+
...(params.outputFormat && { outputFormat: params.outputFormat }),
|
|
364
375
|
});
|
|
365
376
|
let text = "";
|
|
366
377
|
let thinking: string | undefined;
|
|
367
378
|
for (const block of response.content) {
|
|
368
379
|
if (block.type === "text") text += block.text;
|
|
369
|
-
else if ((block as
|
|
380
|
+
else if ((block as { type: string; thinking?: string }).type === "thinking") thinking = (thinking ?? "") + (block as { type: string; thinking?: string }).thinking;
|
|
370
381
|
}
|
|
371
382
|
return { text, thinking, usage: { input: response.usage.input, output: response.usage.output } };
|
|
372
383
|
};
|
|
@@ -472,7 +483,7 @@ export default definePluginEntry({
|
|
|
472
483
|
hasMigratableFiles(globalState!.workspaceDir)
|
|
473
484
|
.then(hasMigratable => {
|
|
474
485
|
if (hasMigratable) {
|
|
475
|
-
|
|
486
|
+
session._hasMigratableFiles = true;
|
|
476
487
|
}
|
|
477
488
|
})
|
|
478
489
|
.catch(e => swallow.warn("index:migrationCheck", e));
|
|
@@ -486,7 +497,7 @@ export default definePluginEntry({
|
|
|
486
497
|
.catch(e => swallow("index:graduationDetect", e));
|
|
487
498
|
|
|
488
499
|
// Synthesize wakeup briefing — store the promise so assemble() can await it
|
|
489
|
-
|
|
500
|
+
session._wakeupPromise = synthesizeWakeup(
|
|
490
501
|
globalState!.store, globalState!.complete, session.sessionId, globalState!.workspaceDir,
|
|
491
502
|
).catch(e => { swallow.warn("index:wakeup", e); return null; });
|
|
492
503
|
|
|
@@ -535,7 +546,7 @@ export default definePluginEntry({
|
|
|
535
546
|
const syncExitHandler = () => {
|
|
536
547
|
const gs = getGlobalState();
|
|
537
548
|
if (!gs?.workspaceDir) return;
|
|
538
|
-
const sessions =
|
|
549
|
+
const sessions = gs.allSessions();
|
|
539
550
|
for (const session of sessions) {
|
|
540
551
|
if (session.cleanedUp) continue;
|
|
541
552
|
writeHandoffFileSync({
|
|
@@ -553,14 +564,14 @@ export default definePluginEntry({
|
|
|
553
564
|
const asyncExitHandler = () => {
|
|
554
565
|
const gs = getGlobalState();
|
|
555
566
|
if (!gs) return;
|
|
556
|
-
const sessions =
|
|
567
|
+
const sessions = gs.allSessions();
|
|
557
568
|
if (sessions.length === 0 && !shutdownPromise) return;
|
|
558
569
|
|
|
559
570
|
const cleanups = sessions.map(s => runSessionCleanup(s, gs));
|
|
560
571
|
if (shutdownPromise) cleanups.push(shutdownPromise);
|
|
561
572
|
|
|
562
573
|
const done = Promise.allSettled(cleanups).then(() => {
|
|
563
|
-
gs.shutdown().catch(
|
|
574
|
+
gs.shutdown().catch(e => log.error("shutdown error:", e));
|
|
564
575
|
});
|
|
565
576
|
|
|
566
577
|
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/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
|
}
|
|
@@ -192,7 +196,7 @@ export async function preflight(
|
|
|
192
196
|
|
|
193
197
|
// Non-first-turn short inputs → continuation
|
|
194
198
|
if (orch.turnIndex > 1 && input.length < 20 && !input.includes("?")) {
|
|
195
|
-
const inheritedLimit = Math.
|
|
199
|
+
const inheritedLimit = Math.min(orch.lastConfig.toolLimit, 25);
|
|
196
200
|
const config: AdaptiveConfig = {
|
|
197
201
|
...orch.lastConfig, toolLimit: inheritedLimit, skipRetrieval: true,
|
|
198
202
|
vectorSearchLimits: { turn: 0, identity: 0, concept: 0, memory: 0, artifact: 0 },
|
|
@@ -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()`,
|
|
@@ -207,6 +214,7 @@ export async function generateReflection(
|
|
|
207
214
|
{ record },
|
|
208
215
|
);
|
|
209
216
|
const reflectionId = String(rows[0]?.id ?? "");
|
|
217
|
+
store.clearReflectionCache();
|
|
210
218
|
|
|
211
219
|
if (reflectionId && surrealSessionId) {
|
|
212
220
|
await store.relate(reflectionId, "reflects_on", surrealSessionId).catch(e => swallow.warn("reflection:relate", e));
|
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
|
|
@@ -260,6 +263,7 @@ DEFINE FIELD IF NOT EXISTS avg_duration_ms ON skill TYPE float DEFAULT 0.0;
|
|
|
260
263
|
DEFINE FIELD IF NOT EXISTS last_used ON skill TYPE option<datetime>;
|
|
261
264
|
DEFINE FIELD IF NOT EXISTS created_at ON skill TYPE datetime DEFAULT time::now();
|
|
262
265
|
DEFINE INDEX IF NOT EXISTS skill_vec_idx ON skill FIELDS embedding HNSW DIMENSION 1024 DIST COSINE;
|
|
266
|
+
DEFINE INDEX IF NOT EXISTS skill_active_idx ON skill FIELDS active;
|
|
263
267
|
|
|
264
268
|
DEFINE TABLE IF NOT EXISTS skill_from_task TYPE RELATION IN skill OUT task;
|
|
265
269
|
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
|
@@ -191,7 +191,7 @@ export async function getQualitySignals(store: SurrealStore): Promise<QualitySig
|
|
|
191
191
|
const totalSuccess = Number(skillRow?.totalSuccess ?? 0);
|
|
192
192
|
const totalFailure = Number(skillRow?.totalFailure ?? 0);
|
|
193
193
|
const skillTotal = totalSuccess + totalFailure;
|
|
194
|
-
const skillSuccessRate = skillTotal > 0 ? totalSuccess / skillTotal : 0;
|
|
194
|
+
const skillSuccessRate = skillTotal > 0 && Number.isFinite(skillTotal) ? totalSuccess / skillTotal : 0;
|
|
195
195
|
|
|
196
196
|
const critCount = Number(critRow?.count ?? 0);
|
|
197
197
|
const reflCount = Number(totalRow?.count ?? 0);
|
|
@@ -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]);
|