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
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* after_tool_call hook — artifact tracking + tool outcome recording.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { GlobalPluginState } from "../state.js";
|
|
6
|
+
import { recordToolOutcome } from "../retrieval-quality.js";
|
|
7
|
+
import { swallow } from "../errors.js";
|
|
8
|
+
|
|
9
|
+
export function createAfterToolCallHandler(state: GlobalPluginState) {
|
|
10
|
+
return async (
|
|
11
|
+
event: {
|
|
12
|
+
toolName: string;
|
|
13
|
+
params: Record<string, unknown>;
|
|
14
|
+
toolCallId?: string;
|
|
15
|
+
result?: unknown;
|
|
16
|
+
error?: string;
|
|
17
|
+
durationMs?: number;
|
|
18
|
+
},
|
|
19
|
+
ctx: { sessionKey?: string; sessionId?: string },
|
|
20
|
+
) => {
|
|
21
|
+
const sessionKey = ctx.sessionKey ?? ctx.sessionId ?? "default";
|
|
22
|
+
const session = state.getSession(sessionKey);
|
|
23
|
+
if (!session) return;
|
|
24
|
+
|
|
25
|
+
const isError = !!event.error;
|
|
26
|
+
recordToolOutcome(!isError);
|
|
27
|
+
|
|
28
|
+
// Store tool result snippet
|
|
29
|
+
const resultText = typeof event.result === "string"
|
|
30
|
+
? event.result.slice(0, 500)
|
|
31
|
+
: JSON.stringify(event.result ?? "").slice(0, 500);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await state.store.upsertTurn({
|
|
35
|
+
session_id: session.sessionId,
|
|
36
|
+
role: "tool",
|
|
37
|
+
text: `[${event.toolName}] ${resultText}`,
|
|
38
|
+
embedding: null,
|
|
39
|
+
});
|
|
40
|
+
} catch (e) {
|
|
41
|
+
swallow("hook:afterToolCall:store", e);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Auto-track file artifacts from write/edit tools
|
|
45
|
+
if (!isError) {
|
|
46
|
+
trackArtifact(event.toolName, event.params, session.taskId, state)
|
|
47
|
+
.catch(e => swallow.warn("hook:afterToolCall:artifact", e));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Clean up pending args
|
|
51
|
+
if (event.toolCallId) {
|
|
52
|
+
session.pendingToolArgs.delete(event.toolCallId);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function trackArtifact(
|
|
58
|
+
toolName: string,
|
|
59
|
+
args: Record<string, unknown>,
|
|
60
|
+
taskId: string,
|
|
61
|
+
state: GlobalPluginState,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
const ARTIFACT_TOOLS: Record<string, string> = {
|
|
64
|
+
write: "created", edit: "edited", bash: "shell",
|
|
65
|
+
};
|
|
66
|
+
const action = ARTIFACT_TOOLS[toolName];
|
|
67
|
+
if (!action) return;
|
|
68
|
+
|
|
69
|
+
let description: string | null = null;
|
|
70
|
+
|
|
71
|
+
if (toolName === "write" && args.path) {
|
|
72
|
+
description = `File created: ${args.path}`;
|
|
73
|
+
} else if (toolName === "edit" && args.path) {
|
|
74
|
+
description = `File edited: ${args.path}`;
|
|
75
|
+
} else if (toolName === "bash" && typeof args.command === "string") {
|
|
76
|
+
const cmd = args.command;
|
|
77
|
+
if (/\b(cp|mv|touch|mkdir|npm init|git init|tsc)\b/.test(cmd)) {
|
|
78
|
+
description = `Shell: ${cmd.slice(0, 200)}`;
|
|
79
|
+
} else {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!description) return;
|
|
85
|
+
|
|
86
|
+
let emb: number[] | null = null;
|
|
87
|
+
if (state.embeddings.isAvailable()) {
|
|
88
|
+
try { emb = await state.embeddings.embed(description); } catch { /* ok */ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const ext = (args.path as string)?.split(".").pop() ?? "unknown";
|
|
92
|
+
const artifactId = await state.store.createArtifact(
|
|
93
|
+
(args.path as string) ?? "shell", ext, description, emb,
|
|
94
|
+
);
|
|
95
|
+
if (artifactId && taskId) {
|
|
96
|
+
await state.store.relate(taskId, "produced", artifactId)
|
|
97
|
+
.catch(e => swallow.warn("artifact:relate", e));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* before_prompt_build hook — orchestrator preflight.
|
|
3
|
+
* Classifies intent, adapts retrieval config, sets thinking level.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { GlobalPluginState } from "../state.js";
|
|
7
|
+
import { preflight } from "../orchestrator.js";
|
|
8
|
+
import { swallow } from "../errors.js";
|
|
9
|
+
|
|
10
|
+
export function createBeforePromptBuildHandler(state: GlobalPluginState) {
|
|
11
|
+
return async (
|
|
12
|
+
event: { prompt: string; messages: unknown[] },
|
|
13
|
+
ctx: { sessionKey?: string; sessionId?: string },
|
|
14
|
+
) => {
|
|
15
|
+
const sessionKey = ctx.sessionKey ?? ctx.sessionId ?? "default";
|
|
16
|
+
const session = state.getSession(sessionKey);
|
|
17
|
+
if (!session) return;
|
|
18
|
+
|
|
19
|
+
// Reset per-turn counters
|
|
20
|
+
session.resetTurn();
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const result = await preflight(
|
|
24
|
+
event.prompt,
|
|
25
|
+
session,
|
|
26
|
+
state.embeddings,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Store config on session for graph-context to read
|
|
30
|
+
session.currentConfig = result.config;
|
|
31
|
+
|
|
32
|
+
// Return system prompt addition with thinking level override
|
|
33
|
+
return {
|
|
34
|
+
prependSystemContext: result.config.skipRetrieval
|
|
35
|
+
? undefined
|
|
36
|
+
: `[Intent: ${result.intent.category} (${(result.intent.confidence * 100).toFixed(0)}%) | Tool budget: ${result.config.toolLimit} | Retrieval: ${result.config.tokenBudget} tokens]`,
|
|
37
|
+
thinkingLevel: result.config.thinkingLevel,
|
|
38
|
+
};
|
|
39
|
+
} catch (e) {
|
|
40
|
+
swallow.warn("hook:beforePromptBuild", e);
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* before_tool_call hook — planning gate + tool limit enforcement.
|
|
3
|
+
*
|
|
4
|
+
* - Planning gate: model must output text before its first tool call
|
|
5
|
+
* - Tool limit: blocks when budget exceeded
|
|
6
|
+
* - Soft interrupt: blocks when user pressed Ctrl+C
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { GlobalPluginState } from "../state.js";
|
|
10
|
+
import { recordToolCall } from "../orchestrator.js";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_TOOL_LIMIT = 10;
|
|
13
|
+
const CLASSIFICATION_LIMITS: Record<string, number> = { LOOKUP: 3, EDIT: 4, REFACTOR: 8 };
|
|
14
|
+
|
|
15
|
+
export function createBeforeToolCallHandler(state: GlobalPluginState) {
|
|
16
|
+
return async (
|
|
17
|
+
event: {
|
|
18
|
+
toolName: string;
|
|
19
|
+
params: Record<string, unknown>;
|
|
20
|
+
runId?: string;
|
|
21
|
+
toolCallId?: string;
|
|
22
|
+
assistantTextLengthSoFar?: number;
|
|
23
|
+
toolCallIndexInTurn?: number;
|
|
24
|
+
},
|
|
25
|
+
ctx: { sessionKey?: string; sessionId?: string },
|
|
26
|
+
) => {
|
|
27
|
+
const sessionKey = ctx.sessionKey ?? ctx.sessionId ?? "default";
|
|
28
|
+
const session = state.getSession(sessionKey);
|
|
29
|
+
if (!session) return;
|
|
30
|
+
|
|
31
|
+
session.toolCallCount++;
|
|
32
|
+
session.toolCallsSinceLastText++;
|
|
33
|
+
|
|
34
|
+
// Record for steering analysis
|
|
35
|
+
recordToolCall(session, event.toolName);
|
|
36
|
+
|
|
37
|
+
// Use native fields when available, fall back to plugin-tracked state
|
|
38
|
+
const textLengthSoFar = event.assistantTextLengthSoFar ?? session.turnTextLength;
|
|
39
|
+
const toolIndex = event.toolCallIndexInTurn ?? (session.toolCallCount - 1);
|
|
40
|
+
|
|
41
|
+
// Soft interrupt
|
|
42
|
+
if (session.softInterrupted) {
|
|
43
|
+
return {
|
|
44
|
+
block: true,
|
|
45
|
+
blockReason: "The user pressed Ctrl+C to interrupt you. Stop all tool calls immediately. Summarize what you've found so far, respond to the user with your current progress, and ask how to proceed.",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Tool limit
|
|
50
|
+
if (session.toolCallCount > session.toolLimit) {
|
|
51
|
+
return {
|
|
52
|
+
block: true,
|
|
53
|
+
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.`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Planning gate: model must output text before first tool call
|
|
58
|
+
if (textLengthSoFar === 0 && toolIndex === 0) {
|
|
59
|
+
return {
|
|
60
|
+
block: true,
|
|
61
|
+
blockReason:
|
|
62
|
+
"PLANNING GATE — You must announce your plan before making tool calls.\n" +
|
|
63
|
+
"1. Classify: LOOKUP (3 calls max), EDIT (4 max), REFACTOR (8 max)\n" +
|
|
64
|
+
"2. STATE WHAT YOU ALREADY KNOW from injected memory/context — if you have prior knowledge about these files, say so\n" +
|
|
65
|
+
"3. List each planned call and what SPECIFIC GAP it fills that memory doesn't cover\n" +
|
|
66
|
+
"4. Every step still happens, but COMBINED. Edit + test in one bash call, not two.\n" +
|
|
67
|
+
"If injected context already answers the question, you may need ZERO tool calls.\n" +
|
|
68
|
+
"Speak your plan, then proceed.",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return undefined;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Parse LOOKUP/EDIT/REFACTOR classification from planning gate response.
|
|
78
|
+
* Called from llm_output to dynamically adjust tool limit.
|
|
79
|
+
*/
|
|
80
|
+
export function parseClassificationFromText(text: string): number | null {
|
|
81
|
+
const match = text.match(/\b(LOOKUP|EDIT|REFACTOR)\b/);
|
|
82
|
+
if (match && CLASSIFICATION_LIMITS[match[1]]) {
|
|
83
|
+
return CLASSIFICATION_LIMITS[match[1]];
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* llm_output hook — token tracking, text length accumulation,
|
|
3
|
+
* dynamic budget parsing, and cognitive check triggering.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { GlobalPluginState } from "../state.js";
|
|
7
|
+
import { parseClassificationFromText } from "./before-tool-call.js";
|
|
8
|
+
import { swallow } from "../errors.js";
|
|
9
|
+
|
|
10
|
+
export function createLlmOutputHandler(state: GlobalPluginState) {
|
|
11
|
+
return async (
|
|
12
|
+
event: {
|
|
13
|
+
runId: string;
|
|
14
|
+
sessionId: string;
|
|
15
|
+
provider: string;
|
|
16
|
+
model: string;
|
|
17
|
+
assistantTexts: string[];
|
|
18
|
+
lastAssistant?: unknown;
|
|
19
|
+
usage?: {
|
|
20
|
+
input?: number;
|
|
21
|
+
output?: number;
|
|
22
|
+
cacheRead?: number;
|
|
23
|
+
cacheWrite?: number;
|
|
24
|
+
total?: number;
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
ctx: { sessionKey?: string; sessionId?: string },
|
|
28
|
+
) => {
|
|
29
|
+
const sessionKey = ctx.sessionKey ?? ctx.sessionId ?? "default";
|
|
30
|
+
const session = state.getSession(sessionKey);
|
|
31
|
+
if (!session) return;
|
|
32
|
+
|
|
33
|
+
// Track token usage
|
|
34
|
+
if (event.usage) {
|
|
35
|
+
const inputTokens = event.usage.input ?? 0;
|
|
36
|
+
const outputTokens = event.usage.output ?? 0;
|
|
37
|
+
|
|
38
|
+
// Update session stats in SurrealDB
|
|
39
|
+
try {
|
|
40
|
+
await state.store.updateSessionStats(
|
|
41
|
+
session.sessionId,
|
|
42
|
+
inputTokens,
|
|
43
|
+
outputTokens,
|
|
44
|
+
);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
swallow("hook:llmOutput:sessionStats", e);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Accumulate for daemon batching
|
|
50
|
+
session.newContentTokens += inputTokens + outputTokens;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Track accumulated text output for planning gate
|
|
54
|
+
const textLen = event.assistantTexts.reduce((s, t) => s + t.length, 0);
|
|
55
|
+
session.turnTextLength += textLen;
|
|
56
|
+
|
|
57
|
+
if (textLen > 50) {
|
|
58
|
+
session.toolCallsSinceLastText = 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Dynamic budget: parse LOOKUP/EDIT/REFACTOR from first assistant text
|
|
62
|
+
if (session.toolCallCount <= 1 && event.assistantTexts.length > 0) {
|
|
63
|
+
const fullText = event.assistantTexts.join("");
|
|
64
|
+
const classLimit = parseClassificationFromText(fullText);
|
|
65
|
+
if (classLimit !== null) {
|
|
66
|
+
session.toolLimit = classLimit;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Capture thinking blocks for monologue extraction
|
|
71
|
+
const lastAssistant = event.lastAssistant as any;
|
|
72
|
+
if (lastAssistant?.content && Array.isArray(lastAssistant.content)) {
|
|
73
|
+
for (const block of lastAssistant.content) {
|
|
74
|
+
if (block.type === "thinking") {
|
|
75
|
+
const thinking = block.thinking ?? block.text ?? "";
|
|
76
|
+
if (thinking.length > 50) {
|
|
77
|
+
session.pendingThinking.push(thinking);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Store assistant turn with embedding
|
|
84
|
+
if (event.assistantTexts.length > 0) {
|
|
85
|
+
const text = event.assistantTexts.join("\n");
|
|
86
|
+
if (text.length > 0) {
|
|
87
|
+
try {
|
|
88
|
+
const embedLimit = Math.round(8192 * 3.4 * 0.8);
|
|
89
|
+
let embedding: number[] | null = null;
|
|
90
|
+
if (hasSemantic(text) && state.embeddings.isAvailable()) {
|
|
91
|
+
try {
|
|
92
|
+
embedding = await state.embeddings.embed(text.slice(0, embedLimit));
|
|
93
|
+
} catch (e) { swallow("hook:llmOutput:embed", e); }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const turnId = await state.store.upsertTurn({
|
|
97
|
+
session_id: session.sessionId,
|
|
98
|
+
role: "assistant",
|
|
99
|
+
text,
|
|
100
|
+
embedding,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (turnId) {
|
|
104
|
+
await state.store.relate(turnId, "part_of", session.sessionId)
|
|
105
|
+
.catch(e => swallow("hook:llmOutput:relate", e));
|
|
106
|
+
|
|
107
|
+
// Extract and link concepts
|
|
108
|
+
if (hasSemantic(text)) {
|
|
109
|
+
extractAndLinkConcepts(turnId, text, state)
|
|
110
|
+
.catch(e => swallow.warn("hook:llmOutput:concepts", e));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
session.lastAssistantText = text;
|
|
115
|
+
} catch (e) {
|
|
116
|
+
swallow.warn("hook:llmOutput:storeTurn", e);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function hasSemantic(text: string): boolean {
|
|
124
|
+
if (text.length < 15) return false;
|
|
125
|
+
if (/^(ok|yes|no|sure|thanks|done|got it|hmm|hm|yep|nope|cool|nice|great)\s*[.!?]?\s*$/i.test(text)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return text.split(/\s+/).filter(w => w.length > 2).length >= 3;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// --- Concept extraction ---
|
|
132
|
+
|
|
133
|
+
const CONCEPT_RE = /\b(?:(?:use|using|implement|create|add|configure|setup|install|import)\s+)([A-Z][a-zA-Z0-9_-]+(?:\s+[A-Z][a-zA-Z0-9_-]+)?)/g;
|
|
134
|
+
const TECH_TERMS = /\b(api|database|schema|migration|endpoint|middleware|component|service|module|handler|controller|model|interface|type|class|function|method|hook|plugin|extension|config|cache|queue|worker|daemon)\b/gi;
|
|
135
|
+
|
|
136
|
+
async function extractAndLinkConcepts(
|
|
137
|
+
turnId: string,
|
|
138
|
+
text: string,
|
|
139
|
+
state: GlobalPluginState,
|
|
140
|
+
): Promise<void> {
|
|
141
|
+
const concepts = new Set<string>();
|
|
142
|
+
|
|
143
|
+
// Named concepts (PascalCase after action verbs)
|
|
144
|
+
let match: RegExpExecArray | null;
|
|
145
|
+
const re1 = new RegExp(CONCEPT_RE.source, CONCEPT_RE.flags);
|
|
146
|
+
while ((match = re1.exec(text)) !== null) {
|
|
147
|
+
concepts.add(match[1].trim());
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Technical terms
|
|
151
|
+
const re2 = new RegExp(TECH_TERMS.source, TECH_TERMS.flags);
|
|
152
|
+
while ((match = re2.exec(text)) !== null) {
|
|
153
|
+
concepts.add(match[1].toLowerCase());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (concepts.size === 0) return;
|
|
157
|
+
|
|
158
|
+
for (const conceptText of [...concepts].slice(0, 10)) {
|
|
159
|
+
try {
|
|
160
|
+
let embedding: number[] | null = null;
|
|
161
|
+
if (state.embeddings.isAvailable()) {
|
|
162
|
+
try { embedding = await state.embeddings.embed(conceptText); } catch { /* ok */ }
|
|
163
|
+
}
|
|
164
|
+
const conceptId = await state.store.upsertConcept(conceptText, embedding);
|
|
165
|
+
if (conceptId) {
|
|
166
|
+
await state.store.relate(turnId, "mentions", conceptId)
|
|
167
|
+
.catch(e => swallow("concepts:relate", e));
|
|
168
|
+
}
|
|
169
|
+
} catch (e) {
|
|
170
|
+
swallow("concepts:upsert", e);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
package/src/identity.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity ingestion — seeds KongBrain's self-knowledge into the graph.
|
|
3
|
+
*
|
|
4
|
+
* Two layers:
|
|
5
|
+
* 1. Core identity (hardcoded) — factual capabilities, architecture, tools.
|
|
6
|
+
* Prevents the LLM from falling back to generic "I'm a stateless AI".
|
|
7
|
+
* 2. User identity (WAKEUP.md) — personality, tone, role defined by the user
|
|
8
|
+
* on first run. Read, processed, saved to graph, then deleted.
|
|
9
|
+
*
|
|
10
|
+
* Ported from kongbrain — takes SurrealStore/EmbeddingService as params.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync, unlinkSync, existsSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import type { SurrealStore } from "./surreal.js";
|
|
15
|
+
import type { EmbeddingService } from "./embeddings.js";
|
|
16
|
+
import { swallow } from "./errors.js";
|
|
17
|
+
|
|
18
|
+
const IDENTITY_SOURCE = "core_identity";
|
|
19
|
+
|
|
20
|
+
const IDENTITY_CHUNKS: { text: string; importance: number }[] = [
|
|
21
|
+
{
|
|
22
|
+
text: "KongBrain has persistent memory across sessions. It stores every conversation turn, concept, decision, and file artifact in a SurrealDB graph database. Context from previous sessions is automatically retrieved and injected via vector search and graph traversal. It is NOT a stateless chatbot — it remembers.",
|
|
23
|
+
importance: 0.9,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
text: "KongBrain's capabilities: read/write/edit files, run shell commands, search code with grep/find, search its own persistent memory graph with the recall tool, and spawn autonomous subagents. It has coding tools plus memory recall plus subagent spawning.",
|
|
27
|
+
importance: 0.8,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
text: "KongBrain learns from experience. It extracts causal chains (cause->effect patterns) from debug sessions, learns reusable skills (step-by-step procedures) from successful multi-step tasks, and generates metacognitive reflections when sessions have problems. These are stored in the graph and retrieved for future similar situations.",
|
|
31
|
+
importance: 0.85,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
text: "KongBrain has a skill library — procedural memory extracted from successful multi-step sessions. Each skill has preconditions, steps, postconditions, and success/failure tracking. Skills are retrieved via vector similarity when similar tasks come up. The agent gets better at recurring task categories over time.",
|
|
35
|
+
importance: 0.8,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
text: "KongBrain uses intent classification to adapt its behavior per turn. It classifies user input into categories (code-write, code-debug, code-read, multi-step, deep-explore, etc.) and adjusts thinking depth, tool limits, and retrieval strategy accordingly. This happens before the LLM sees the prompt.",
|
|
39
|
+
importance: 0.7,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
text: "KongBrain's memory graph includes: turns (conversation history), concepts (extracted knowledge), memories (compacted summaries), artifacts (files worked on), skills (learned procedures), reflections (metacognitive lessons), and causal chains (cause->effect patterns). All are embedded and retrievable via vector similarity.",
|
|
43
|
+
importance: 0.8,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
text: "When KongBrain doesn't know whether it has a capability or piece of knowledge, it should use the recall tool to search its own memory graph rather than guessing. The graph contains the ground truth about what it knows and what it has done in past sessions.",
|
|
47
|
+
importance: 0.9,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
text: "Tool efficiency: Plan before acting — state goal and call budget (LOOKUP=1, EDIT=2, REFACTOR=6). Maximize each call by combining operations into single bash invocations. Check injected context before calling any tool. If you already have the answer, don't make a call.",
|
|
51
|
+
importance: 1.0,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
text: "KongBrain's cognitive architecture follows the IKONG pillars: Intelligence (intent classification and adaptive orchestration), Knowledge (memory graph, concepts, skills, reflections, identity), Operation (tool execution, skill procedures, causal chain tracking), Network (graph traversal, cross-pillar edges, neighbor expansion), Graph (SurrealDB persistence, vector search, BGE-M3 embeddings). These five systems work together every turn.",
|
|
55
|
+
importance: 0.85,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
text: "KongBrain's structural graph has 5 entity pillars: Agent (you — kongbrain), Project (the workspace you're in), Task (each session's work), Artifact (files you've created or modified), and Concept (knowledge nodes extracted from conversations). These are linked by edges: owns, performed, task_part_of, produced, derived_from, relevant_to. Graph traversal follows these edges to find related context.",
|
|
59
|
+
importance: 0.85,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
text: "KongBrain has a Soul — an emergent identity document written by the agent itself after graduation. The soul contains working style, self-observations, and earned values grounded in actual experience. Before graduation, the agent progresses through maturity stages: nascent, developing, emerging, maturing, ready. Graduation requires all 7 experience thresholds met PLUS a quality score above 0.6. The soul evolves over time as new experience accumulates.",
|
|
63
|
+
importance: 0.9,
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
export async function seedIdentity(
|
|
68
|
+
store: SurrealStore,
|
|
69
|
+
embeddings: EmbeddingService,
|
|
70
|
+
): Promise<number> {
|
|
71
|
+
if (!store.isAvailable() || !embeddings.isAvailable()) return 0;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const rows = await store.queryFirst<{ count: number }>(
|
|
75
|
+
`SELECT count() AS count FROM identity_chunk WHERE source = $source GROUP ALL`,
|
|
76
|
+
{ source: IDENTITY_SOURCE },
|
|
77
|
+
);
|
|
78
|
+
const count = rows[0]?.count ?? 0;
|
|
79
|
+
if (count >= IDENTITY_CHUNKS.length) return 0;
|
|
80
|
+
|
|
81
|
+
if (count > 0) {
|
|
82
|
+
await store.queryExec(
|
|
83
|
+
`DELETE identity_chunk WHERE source = $source`,
|
|
84
|
+
{ source: IDENTITY_SOURCE },
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {
|
|
88
|
+
swallow.warn("identity:check", e);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let seeded = 0;
|
|
93
|
+
for (let i = 0; i < IDENTITY_CHUNKS.length; i++) {
|
|
94
|
+
const chunk = IDENTITY_CHUNKS[i];
|
|
95
|
+
try {
|
|
96
|
+
const vec = await embeddings.embed(chunk.text);
|
|
97
|
+
await store.queryExec(
|
|
98
|
+
`CREATE identity_chunk CONTENT $data`,
|
|
99
|
+
{
|
|
100
|
+
data: {
|
|
101
|
+
agent_id: "kongbrain",
|
|
102
|
+
source: IDENTITY_SOURCE,
|
|
103
|
+
chunk_index: i,
|
|
104
|
+
text: chunk.text,
|
|
105
|
+
embedding: vec,
|
|
106
|
+
importance: chunk.importance,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
seeded++;
|
|
111
|
+
} catch (e) {
|
|
112
|
+
swallow("identity:seedChunk", e);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return seeded;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── WAKEUP.md — User-defined identity on first run ──
|
|
120
|
+
|
|
121
|
+
const USER_IDENTITY_SOURCE = "user_identity";
|
|
122
|
+
|
|
123
|
+
export async function hasUserIdentity(store: SurrealStore): Promise<boolean> {
|
|
124
|
+
if (!store.isAvailable()) return true;
|
|
125
|
+
try {
|
|
126
|
+
const rows = await store.queryFirst<{ count: number }>(
|
|
127
|
+
`SELECT count() AS count FROM identity_chunk WHERE source = $source GROUP ALL`,
|
|
128
|
+
{ source: USER_IDENTITY_SOURCE },
|
|
129
|
+
);
|
|
130
|
+
return (rows[0]?.count ?? 0) > 0;
|
|
131
|
+
} catch (e) {
|
|
132
|
+
swallow("identity:hasUserIdentity", e);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function findWakeupFile(cwd: string): string | null {
|
|
138
|
+
const path = join(cwd, "WAKEUP.md");
|
|
139
|
+
return existsSync(path) ? path : null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function readWakeupFile(path: string): string {
|
|
143
|
+
return readFileSync(path, "utf-8").trim();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function deleteWakeupFile(path: string): void {
|
|
147
|
+
try {
|
|
148
|
+
unlinkSync(path);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
swallow.warn("identity:deleteWakeupFile", e);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function saveUserIdentity(
|
|
155
|
+
chunks: string[],
|
|
156
|
+
store: SurrealStore,
|
|
157
|
+
embeddings: EmbeddingService,
|
|
158
|
+
): Promise<number> {
|
|
159
|
+
if (!store.isAvailable() || !embeddings.isAvailable()) return 0;
|
|
160
|
+
if (chunks.length === 0) return 0;
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
await store.queryExec(
|
|
164
|
+
`DELETE identity_chunk WHERE source = $source`,
|
|
165
|
+
{ source: USER_IDENTITY_SOURCE },
|
|
166
|
+
);
|
|
167
|
+
} catch (e) {
|
|
168
|
+
swallow.warn("identity:clearUserIdentity", e);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let saved = 0;
|
|
172
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
173
|
+
const text = chunks[i].trim();
|
|
174
|
+
if (!text) continue;
|
|
175
|
+
try {
|
|
176
|
+
const vec = await embeddings.embed(text);
|
|
177
|
+
await store.queryExec(
|
|
178
|
+
`CREATE identity_chunk CONTENT $data`,
|
|
179
|
+
{
|
|
180
|
+
data: {
|
|
181
|
+
agent_id: "kongbrain",
|
|
182
|
+
source: USER_IDENTITY_SOURCE,
|
|
183
|
+
chunk_index: i,
|
|
184
|
+
text,
|
|
185
|
+
embedding: vec,
|
|
186
|
+
importance: 0.95,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
saved++;
|
|
191
|
+
} catch (e) {
|
|
192
|
+
swallow.warn("identity:saveChunk", e);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return saved;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function buildWakeupPrompt(wakeupContent: string): { systemAddition: string; firstMessage: string } {
|
|
200
|
+
const systemAddition = `
|
|
201
|
+
FIRST RUN — IDENTITY ESTABLISHMENT
|
|
202
|
+
This is your first interaction with this user. A WAKEUP.md file has been provided that defines who you should be — your personality, tone, role, and behavioral guidelines. You must:
|
|
203
|
+
1. Read and internalize the identity described in WAKEUP.md
|
|
204
|
+
2. Introduce yourself according to that identity
|
|
205
|
+
3. Confirm with the user that the identity feels right
|
|
206
|
+
4. The system will save your identity to persistent memory automatically
|
|
207
|
+
|
|
208
|
+
Do NOT fall back to generic AI assistant behavior. You are whoever WAKEUP.md says you are.`;
|
|
209
|
+
|
|
210
|
+
const firstMessage = `[WAKEUP.md — Identity Configuration]
|
|
211
|
+
|
|
212
|
+
${wakeupContent}
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
Process the above identity configuration. Introduce yourself as described, and confirm with me that the personality and tone feel right. If anything needs adjusting, I'll tell you.`;
|
|
216
|
+
|
|
217
|
+
return { systemAddition, firstMessage };
|
|
218
|
+
}
|