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.
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Cognitive Check — Periodic reasoning over retrieved context.
3
+ *
4
+ * Fires every few turns to evaluate what was retrieved, produce behavioral
5
+ * directives for the next turn, and grade retrieval quality with LLM-judged
6
+ * relevance scores that feed back into ACAN training.
7
+ *
8
+ * Ported from kongbrain — per-session state via WeakMap, takes SurrealStore param.
9
+ */
10
+
11
+ import type { CompleteFn, SessionState } from "./state.js";
12
+ import type { SurrealStore } from "./surreal.js";
13
+ import { swallow } from "./errors.js";
14
+
15
+ // --- Types ---
16
+
17
+ export interface CognitiveDirective {
18
+ type: "repeat" | "continuation" | "contradiction" | "noise" | "insight";
19
+ target: string;
20
+ instruction: string;
21
+ priority: "high" | "medium" | "low";
22
+ }
23
+
24
+ export interface RetrievalGrade {
25
+ id: string;
26
+ relevant: boolean;
27
+ reason: string;
28
+ score: number;
29
+ learned: boolean;
30
+ resolved: boolean;
31
+ }
32
+
33
+ export interface UserPreference {
34
+ observation: string;
35
+ confidence: "high" | "medium";
36
+ }
37
+
38
+ export interface CognitiveCheckResult {
39
+ directives: CognitiveDirective[];
40
+ grades: RetrievalGrade[];
41
+ sessionContinuity: "continuation" | "repeat" | "new_topic" | "tangent";
42
+ preferences: UserPreference[];
43
+ }
44
+
45
+ export interface CognitiveCheckInput {
46
+ sessionId: string;
47
+ userQuery: string;
48
+ responseText: string;
49
+ retrievedNodes: { id: string; text: string; score: number; table: string }[];
50
+ recentTurns: { role: string; text: string }[];
51
+ }
52
+
53
+ // --- Per-session state ---
54
+
55
+ interface CognitiveState {
56
+ pendingDirectives: CognitiveDirective[];
57
+ sessionContinuity: string;
58
+ checkInFlight: boolean;
59
+ suppressedNodeIds: Set<string>;
60
+ }
61
+
62
+ const sessionState = new WeakMap<SessionState, CognitiveState>();
63
+
64
+ function getState(session: SessionState): CognitiveState {
65
+ let state = sessionState.get(session);
66
+ if (!state) {
67
+ state = {
68
+ pendingDirectives: [],
69
+ sessionContinuity: "new_topic",
70
+ checkInFlight: false,
71
+ suppressedNodeIds: new Set(),
72
+ };
73
+ sessionState.set(session, state);
74
+ }
75
+ return state;
76
+ }
77
+
78
+ // --- Constants ---
79
+
80
+ const DIRECTIVE_TYPES = new Set(["repeat", "continuation", "contradiction", "noise", "insight"]);
81
+ const PRIORITIES = new Set(["high", "medium", "low"]);
82
+ const CONTINUITY_TYPES = new Set(["continuation", "repeat", "new_topic", "tangent"]);
83
+ const VALID_RECORD_ID = /^[a-z_]+:[a-zA-Z0-9_]+$/;
84
+
85
+ // --- Public API ---
86
+
87
+ /** Returns true on turn 2, then every 3 turns (2, 5, 8, 11...). False if in-flight. */
88
+ export function shouldRunCheck(turnCount: number, session: SessionState): boolean {
89
+ const state = getState(session);
90
+ if (state.checkInFlight) return false;
91
+ if (turnCount < 2) return false;
92
+ return turnCount === 2 || (turnCount - 2) % 3 === 0;
93
+ }
94
+
95
+ export function getPendingDirectives(session: SessionState): CognitiveDirective[] {
96
+ return getState(session).pendingDirectives;
97
+ }
98
+
99
+ export function clearPendingDirectives(session: SessionState): void {
100
+ getState(session).pendingDirectives = [];
101
+ }
102
+
103
+ export function getSessionContinuity(session: SessionState): string {
104
+ return getState(session).sessionContinuity;
105
+ }
106
+
107
+ export function getSuppressedNodeIds(session: SessionState): ReadonlySet<string> {
108
+ return getState(session).suppressedNodeIds;
109
+ }
110
+
111
+ /** Fire-and-forget LLM call. Stores directives, writes grades to DB. */
112
+ export async function runCognitiveCheck(
113
+ params: CognitiveCheckInput,
114
+ session: SessionState,
115
+ store: SurrealStore,
116
+ complete: CompleteFn,
117
+ ): Promise<void> {
118
+ const state = getState(session);
119
+ if (state.checkInFlight) return;
120
+ if (params.retrievedNodes.length === 0) return;
121
+
122
+ state.checkInFlight = true;
123
+ try {
124
+ // Build input sections
125
+ const sections: string[] = [];
126
+ sections.push(`[QUERY] ${params.userQuery.slice(0, 500)}`);
127
+ sections.push(`[RESPONSE] ${params.responseText.slice(0, 500)}`);
128
+
129
+ const nodeLines = params.retrievedNodes
130
+ .slice(0, 20)
131
+ .map(n => `- ${n.id} (score: ${n.score.toFixed(2)}): ${n.text.slice(0, 150)}`);
132
+ sections.push(`[RETRIEVED]\n${nodeLines.join("\n")}`);
133
+
134
+ if (params.recentTurns.length > 0) {
135
+ const trajectory = params.recentTurns
136
+ .slice(-6)
137
+ .map(t => `[${t.role}] ${(t.text ?? "").slice(0, 200)}`)
138
+ .join("\n");
139
+ sections.push(`[TRAJECTORY]\n${trajectory}`);
140
+ }
141
+
142
+ const response = await complete({
143
+ system: `Assess the retrieved context served to an AI assistant. Return JSON:
144
+
145
+ "directives": [{type, target, instruction, priority}] — max 3. Types:
146
+ "repeat": same topic discussed in a prior session — instruct to acknowledge and build on it
147
+ "continuation": user is continuing prior work — instruct to maintain thread
148
+ "contradiction": retrieved info conflicts with current conversation — flag it
149
+ "noise": node is irrelevant despite high similarity score — instruct to ignore
150
+ "insight": useful pattern the model should lean into
151
+ Priority: "high" (must address), "medium" (should note), "low" (nice to know)
152
+
153
+ "grades": [{id, relevant, reason, score, learned, resolved}] — one per retrieved node. Score 0.0-1.0. "learned": true ONLY if the node is a [CORRECTION] memory AND the assistant's response already follows the correction without being prompted. "resolved": true if this memory's topic has been fully addressed/completed in the current conversation. Both default false.
154
+
155
+ "sessionContinuity": "repeat" | "continuation" | "new_topic" | "tangent"
156
+
157
+ "preferences": [{observation, confidence: "high"|"medium"}] — max 2. User communication style, values, or working preferences inferred from the conversation. Only include if clearly observable. Empty [] if nothing notable.
158
+
159
+ Return ONLY valid JSON.`,
160
+ messages: [{
161
+ role: "user",
162
+ content: sections.join("\n\n"),
163
+ }],
164
+ });
165
+
166
+ const responseText = response.text;
167
+
168
+ const result = parseCheckResponse(responseText);
169
+ if (!result) return;
170
+
171
+ // Store directives for next turn's context formatting
172
+ state.pendingDirectives = result.directives;
173
+ state.sessionContinuity = result.sessionContinuity;
174
+
175
+ // Write grades to DB
176
+ if (result.grades.length > 0) {
177
+ await applyRetrievalGrades(result.grades, params.sessionId, store);
178
+ }
179
+
180
+ // Correction importance adjustment based on behavioral compliance
181
+ const correctionGrades = result.grades.filter(g => g.id.startsWith("memory:") && g.relevant);
182
+ for (const g of correctionGrades) {
183
+ if (g.learned) {
184
+ // Agent followed the correction unprompted — decay toward background (floor 3)
185
+ await store.queryExec(
186
+ `UPDATE ${g.id} SET importance = math::max([3, importance - 2])`,
187
+ ).catch(e => swallow.warn("cognitive-check:correctionDecay", e));
188
+ } else {
189
+ // Correction was relevant but agent ignored it — reinforce (cap 9)
190
+ await store.queryExec(
191
+ `UPDATE ${g.id} SET importance = math::min([9, importance + 1])`,
192
+ ).catch(e => swallow.warn("cognitive-check:correctionReinforce", e));
193
+ }
194
+ }
195
+
196
+ // Store high-confidence user preferences as session-pinned core memory
197
+ const highConfPrefs = result.preferences.filter(p => p.confidence === "high");
198
+ for (const pref of highConfPrefs) {
199
+ await store.createCoreMemory(
200
+ `[USER PREFERENCE] ${pref.observation}`,
201
+ "preference", 7, 1, params.sessionId,
202
+ ).catch(e => swallow.warn("cognitive-check:preference", e));
203
+ }
204
+
205
+ // Noise suppression — prevent re-retrieval of irrelevant nodes this session
206
+ for (const g of result.grades) {
207
+ if (!g.relevant && g.score < 0.3) {
208
+ state.suppressedNodeIds.add(g.id);
209
+ }
210
+ }
211
+ for (const d of result.directives) {
212
+ if (d.type === "noise" && VALID_RECORD_ID.test(d.target)) {
213
+ state.suppressedNodeIds.add(d.target);
214
+ }
215
+ }
216
+
217
+ // Mid-session resolution — mark addressed memories immediately
218
+ const resolvedGrades = result.grades.filter(g => g.resolved && g.id.startsWith("memory:"));
219
+ for (const g of resolvedGrades) {
220
+ await store.queryExec(
221
+ `UPDATE ${g.id} SET status = 'resolved', resolved_at = time::now(), resolved_by = $sid`,
222
+ { sid: params.sessionId },
223
+ ).catch(e => swallow.warn("cognitive-check:resolve", e));
224
+ }
225
+ } catch (e) {
226
+ swallow.warn("cognitive-check:run", e);
227
+ } finally {
228
+ state.checkInFlight = false;
229
+ }
230
+ }
231
+
232
+ // --- Response parsing ---
233
+
234
+ export function parseCheckResponse(text: string): CognitiveCheckResult | null {
235
+ // Strip markdown fences if present
236
+ const stripped = text.replace(/```(?:json)?\s*/g, "").replace(/```\s*$/g, "");
237
+ const jsonMatch = stripped.match(/\{[\s\S]*\}/);
238
+ if (!jsonMatch) return null;
239
+
240
+ let raw: any;
241
+ try {
242
+ raw = JSON.parse(jsonMatch[0]);
243
+ } catch {
244
+ try {
245
+ raw = JSON.parse(jsonMatch[0].replace(/,\s*([}\]])/g, "$1"));
246
+ } catch { return null; }
247
+ }
248
+
249
+ // Validate directives
250
+ const directives: CognitiveDirective[] = [];
251
+ if (Array.isArray(raw.directives)) {
252
+ for (const d of raw.directives.slice(0, 3)) {
253
+ if (!d.type || !d.target || !d.instruction) continue;
254
+ if (!DIRECTIVE_TYPES.has(d.type)) continue;
255
+ directives.push({
256
+ type: d.type,
257
+ target: String(d.target).slice(0, 100),
258
+ instruction: String(d.instruction).slice(0, 200),
259
+ priority: PRIORITIES.has(d.priority) ? d.priority : "medium",
260
+ });
261
+ }
262
+ }
263
+
264
+ // Validate grades
265
+ const grades: RetrievalGrade[] = [];
266
+ if (Array.isArray(raw.grades)) {
267
+ for (const g of raw.grades.slice(0, 30)) {
268
+ if (!g.id || typeof g.relevant !== "boolean") continue;
269
+ if (!VALID_RECORD_ID.test(g.id)) continue;
270
+ grades.push({
271
+ id: String(g.id),
272
+ relevant: Boolean(g.relevant),
273
+ reason: String(g.reason ?? "").slice(0, 150),
274
+ score: Math.max(0, Math.min(1, Number(g.score) || 0)),
275
+ learned: g.learned === true,
276
+ resolved: g.resolved === true,
277
+ });
278
+ }
279
+ }
280
+
281
+ // Validate preferences
282
+ const preferences: UserPreference[] = [];
283
+ if (Array.isArray(raw.preferences)) {
284
+ for (const p of raw.preferences.slice(0, 2)) {
285
+ if (!p.observation) continue;
286
+ if (p.confidence !== "high" && p.confidence !== "medium") continue;
287
+ preferences.push({
288
+ observation: String(p.observation).slice(0, 200),
289
+ confidence: p.confidence,
290
+ });
291
+ }
292
+ }
293
+
294
+ const sessionContinuity = CONTINUITY_TYPES.has(raw.sessionContinuity)
295
+ ? raw.sessionContinuity
296
+ : "new_topic";
297
+
298
+ return { directives, grades, sessionContinuity, preferences };
299
+ }
300
+
301
+ // --- Grade application ---
302
+
303
+ async function applyRetrievalGrades(
304
+ grades: RetrievalGrade[],
305
+ sessionId: string,
306
+ store: SurrealStore,
307
+ ): Promise<void> {
308
+ for (const grade of grades) {
309
+ try {
310
+ // Find the most recent retrieval outcome for this memory+session
311
+ const row = await store.queryFirst<{ id: string }>(
312
+ `SELECT id, created_at FROM retrieval_outcome
313
+ WHERE memory_id = $id AND session_id = $sid
314
+ ORDER BY created_at DESC LIMIT 1`,
315
+ { id: grade.id, sid: sessionId },
316
+ );
317
+ if (row?.[0]?.id) {
318
+ await store.queryExec(
319
+ `UPDATE ${row[0].id} SET llm_relevance = $score, llm_relevant = $relevant, llm_reason = $reason`,
320
+ { score: grade.score, relevant: grade.relevant, reason: grade.reason },
321
+ );
322
+ }
323
+ // Feed relevance score into the utility cache — drives WMR provenUtility scoring
324
+ await store.updateUtilityCache(grade.id, grade.score).catch(e =>
325
+ swallow.warn("cognitive-check:utilityCache", e));
326
+ } catch (e) {
327
+ swallow.warn("cognitive-check:applyGrade", e);
328
+ }
329
+ }
330
+ }
package/src/config.ts ADDED
@@ -0,0 +1,64 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+
4
+ export interface SurrealConfig {
5
+ url: string;
6
+ httpUrl: string;
7
+ user: string;
8
+ pass: string;
9
+ ns: string;
10
+ db: string;
11
+ }
12
+
13
+ export interface EmbeddingConfig {
14
+ modelPath: string;
15
+ dimensions: number;
16
+ }
17
+
18
+ export interface KongBrainConfig {
19
+ surreal: SurrealConfig;
20
+ embedding: EmbeddingConfig;
21
+ }
22
+
23
+ /**
24
+ * Parse plugin config from openclaw.plugin.json configSchema values,
25
+ * with env var overrides and sensible defaults.
26
+ */
27
+ export function parsePluginConfig(raw?: Record<string, unknown>): KongBrainConfig {
28
+ const surreal = (raw?.surreal ?? {}) as Record<string, unknown>;
29
+ const embedding = (raw?.embedding ?? {}) as Record<string, unknown>;
30
+
31
+ // Priority: plugin config > env vars > defaults
32
+ const url =
33
+ (typeof surreal.url === "string" ? surreal.url : null) ??
34
+ process.env.SURREAL_URL ??
35
+ "ws://localhost:8042/rpc";
36
+
37
+ return {
38
+ surreal: {
39
+ url,
40
+ get httpUrl() {
41
+ const override = (typeof surreal.httpUrl === "string" ? surreal.httpUrl : null) ??
42
+ process.env.SURREAL_HTTP_URL;
43
+ if (override) return override;
44
+ return this.url
45
+ .replace("ws://", "http://")
46
+ .replace("wss://", "https://")
47
+ .replace("/rpc", "/sql");
48
+ },
49
+ user: (typeof surreal.user === "string" ? surreal.user : null) ?? process.env.SURREAL_USER ?? "root",
50
+ pass: (typeof surreal.pass === "string" ? surreal.pass : null) ?? process.env.SURREAL_PASS ?? "root",
51
+ ns: (typeof surreal.ns === "string" ? surreal.ns : null) ?? process.env.SURREAL_NS ?? "kong",
52
+ db: (typeof surreal.db === "string" ? surreal.db : null) ?? process.env.SURREAL_DB ?? "memory",
53
+ },
54
+ embedding: {
55
+ modelPath:
56
+ process.env.EMBED_MODEL_PATH ??
57
+ (typeof embedding.modelPath === "string"
58
+ ? embedding.modelPath
59
+ : join(homedir(), ".node-llama-cpp", "models", "bge-m3-q4_k_m.gguf")),
60
+ dimensions:
61
+ typeof embedding.dimensions === "number" ? embedding.dimensions : 1024,
62
+ },
63
+ };
64
+ }