kongbrain 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +385 -0
- package/openclaw.plugin.json +66 -0
- package/package.json +65 -0
- package/src/acan.ts +309 -0
- package/src/causal.ts +237 -0
- package/src/cognitive-check.ts +330 -0
- package/src/config.ts +64 -0
- package/src/context-engine.ts +487 -0
- package/src/daemon-manager.ts +148 -0
- package/src/daemon-types.ts +65 -0
- package/src/embeddings.ts +77 -0
- package/src/errors.ts +43 -0
- package/src/graph-context.ts +989 -0
- package/src/hooks/after-tool-call.ts +99 -0
- package/src/hooks/before-prompt-build.ts +44 -0
- package/src/hooks/before-tool-call.ts +86 -0
- package/src/hooks/llm-output.ts +173 -0
- package/src/identity.ts +218 -0
- package/src/index.ts +435 -0
- package/src/intent.ts +190 -0
- package/src/memory-daemon.ts +495 -0
- package/src/orchestrator.ts +348 -0
- package/src/prefetch.ts +200 -0
- package/src/reflection.ts +280 -0
- package/src/retrieval-quality.ts +266 -0
- package/src/schema.surql +387 -0
- package/src/skills.ts +343 -0
- package/src/soul.ts +936 -0
- package/src/state.ts +119 -0
- package/src/surreal.ts +1371 -0
- package/src/tools/core-memory.ts +120 -0
- package/src/tools/introspect.ts +329 -0
- package/src/tools/recall.ts +102 -0
- package/src/wakeup.ts +318 -0
- package/src/workspace-migrate.ts +752 -0
package/src/skills.ts
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Procedural Memory (Skill Library)
|
|
3
|
+
*
|
|
4
|
+
* When the agent successfully completes a multi-step task, extract the procedure
|
|
5
|
+
* as a reusable skill (preconditions, steps, postconditions, outcome).
|
|
6
|
+
* Next time a similar task is requested, inject the proven procedure as context.
|
|
7
|
+
* Skills earn success/failure counts from outcomes — RL-like reinforcement.
|
|
8
|
+
*
|
|
9
|
+
* Ported from kongbrain — takes SurrealStore/EmbeddingService as params.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { CompleteFn } from "./state.js";
|
|
13
|
+
import type { EmbeddingService } from "./embeddings.js";
|
|
14
|
+
import type { SurrealStore } from "./surreal.js";
|
|
15
|
+
import { swallow } from "./errors.js";
|
|
16
|
+
|
|
17
|
+
// --- Types ---
|
|
18
|
+
|
|
19
|
+
export interface SkillStep {
|
|
20
|
+
tool: string;
|
|
21
|
+
description: string;
|
|
22
|
+
argsPattern?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Skill {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
description: string;
|
|
29
|
+
preconditions?: string;
|
|
30
|
+
steps: SkillStep[];
|
|
31
|
+
postconditions?: string;
|
|
32
|
+
successCount: number;
|
|
33
|
+
failureCount: number;
|
|
34
|
+
avgDurationMs: number;
|
|
35
|
+
confidence: number;
|
|
36
|
+
active: boolean;
|
|
37
|
+
score?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ExtractedSkill {
|
|
41
|
+
name: string;
|
|
42
|
+
description: string;
|
|
43
|
+
preconditions: string;
|
|
44
|
+
steps: SkillStep[];
|
|
45
|
+
postconditions: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- Skill Extraction ---
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Run at session end. If the session had 3+ tool calls and final outcomes succeeded,
|
|
52
|
+
* extract the procedure as a reusable skill.
|
|
53
|
+
*/
|
|
54
|
+
export async function extractSkill(
|
|
55
|
+
sessionId: string,
|
|
56
|
+
taskId: string,
|
|
57
|
+
store: SurrealStore,
|
|
58
|
+
embeddings: EmbeddingService,
|
|
59
|
+
complete: CompleteFn,
|
|
60
|
+
): Promise<string | null> {
|
|
61
|
+
if (!store.isAvailable()) return null;
|
|
62
|
+
|
|
63
|
+
// Check if session had enough tool activity
|
|
64
|
+
const metricsRows = await store.queryFirst<{ totalTools: number }>(
|
|
65
|
+
`SELECT math::sum(actual_tool_calls) AS totalTools
|
|
66
|
+
FROM orchestrator_metrics WHERE session_id = $sid GROUP ALL`,
|
|
67
|
+
{ sid: sessionId },
|
|
68
|
+
).catch(() => [] as { totalTools: number }[]);
|
|
69
|
+
const totalTools = Number(metricsRows[0]?.totalTools ?? 0);
|
|
70
|
+
if (totalTools < 3) return null;
|
|
71
|
+
|
|
72
|
+
const turns = await store.getSessionTurns(sessionId, 50);
|
|
73
|
+
if (turns.length < 4) return null;
|
|
74
|
+
|
|
75
|
+
const transcript = turns
|
|
76
|
+
.map((t) => `[${t.role}] ${(t.text ?? "").slice(0, 300)}`)
|
|
77
|
+
.join("\n");
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const response = await complete({
|
|
81
|
+
system: `Return JSON or null. Fields: {name, description, preconditions, steps: [{tool, description}] (max 8), postconditions}. Generic patterns only (no specific paths). null if no clear multi-step workflow.`,
|
|
82
|
+
messages: [{
|
|
83
|
+
role: "user",
|
|
84
|
+
content: `${totalTools} tool calls:\n${transcript.slice(0, 20000)}`,
|
|
85
|
+
}],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const text = response.text;
|
|
89
|
+
|
|
90
|
+
if (text.trim() === "null" || text.trim() === "None") return null;
|
|
91
|
+
|
|
92
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
93
|
+
if (!jsonMatch) return null;
|
|
94
|
+
|
|
95
|
+
const parsed = JSON.parse(jsonMatch[0]) as ExtractedSkill;
|
|
96
|
+
if (!parsed.name || !parsed.description || !Array.isArray(parsed.steps) || parsed.steps.length === 0) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let skillEmb: number[] | null = null;
|
|
101
|
+
if (embeddings.isAvailable()) {
|
|
102
|
+
try { skillEmb = await embeddings.embed(`${parsed.name}: ${parsed.description}`); } catch (e) { swallow("skills:ok", e); }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const record: Record<string, unknown> = {
|
|
106
|
+
name: String(parsed.name).slice(0, 100),
|
|
107
|
+
description: String(parsed.description).slice(0, 200),
|
|
108
|
+
preconditions: parsed.preconditions ? String(parsed.preconditions).slice(0, 200) : undefined,
|
|
109
|
+
steps: parsed.steps.slice(0, 8).map((s) => ({
|
|
110
|
+
tool: String(s.tool ?? "unknown"),
|
|
111
|
+
description: String(s.description ?? "").slice(0, 200),
|
|
112
|
+
})),
|
|
113
|
+
postconditions: parsed.postconditions ? String(parsed.postconditions).slice(0, 200) : undefined,
|
|
114
|
+
confidence: 1.0,
|
|
115
|
+
active: true,
|
|
116
|
+
};
|
|
117
|
+
if (skillEmb?.length) record.embedding = skillEmb;
|
|
118
|
+
|
|
119
|
+
const rows = await store.queryFirst<{ id: string }>(
|
|
120
|
+
`CREATE skill CONTENT $record RETURN id`,
|
|
121
|
+
{ record },
|
|
122
|
+
);
|
|
123
|
+
const skillId = String(rows[0]?.id ?? "");
|
|
124
|
+
|
|
125
|
+
if (skillId && taskId) {
|
|
126
|
+
await store.relate(skillId, "skill_from_task", taskId).catch(e => swallow.warn("skills:relateSkillTask", e));
|
|
127
|
+
}
|
|
128
|
+
if (skillId) await supersedeOldSkills(skillId, skillEmb ?? [], store);
|
|
129
|
+
|
|
130
|
+
return skillId || null;
|
|
131
|
+
} catch (e) {
|
|
132
|
+
swallow.warn("skills:extract", e);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// --- Supersession ---
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* After saving a new skill, fade similar existing skills above similarity threshold.
|
|
141
|
+
*/
|
|
142
|
+
export async function supersedeOldSkills(
|
|
143
|
+
newSkillId: string,
|
|
144
|
+
newEmb: number[],
|
|
145
|
+
store: SurrealStore,
|
|
146
|
+
): Promise<void> {
|
|
147
|
+
if (!newEmb.length || !store.isAvailable()) return;
|
|
148
|
+
try {
|
|
149
|
+
const rows = await store.queryFirst<{ id: string; score: number }>(
|
|
150
|
+
`SELECT id, vector::similarity::cosine(embedding, $vec) AS score
|
|
151
|
+
FROM skill
|
|
152
|
+
WHERE id != $sid
|
|
153
|
+
AND (active = NONE OR active = true)
|
|
154
|
+
AND embedding != NONE AND array::len(embedding) > 0
|
|
155
|
+
ORDER BY score DESC LIMIT 5`,
|
|
156
|
+
{ vec: newEmb, sid: newSkillId },
|
|
157
|
+
);
|
|
158
|
+
for (const row of rows) {
|
|
159
|
+
if ((row.score ?? 0) >= 0.82) {
|
|
160
|
+
await store.queryExec(
|
|
161
|
+
`UPDATE $id SET active = false, superseded_by = $newId`,
|
|
162
|
+
{ id: row.id, newId: newSkillId },
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch (e) { swallow("skills:supersedeOld", e); }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --- Skill Retrieval ---
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Vector search on the skill table. Called from graphTransformContext
|
|
173
|
+
* when the intent is code-write, code-debug, or multi-step.
|
|
174
|
+
*/
|
|
175
|
+
export async function findRelevantSkills(
|
|
176
|
+
queryVec: number[],
|
|
177
|
+
limit = 3,
|
|
178
|
+
store?: SurrealStore,
|
|
179
|
+
): Promise<Skill[]> {
|
|
180
|
+
if (!store?.isAvailable()) return [];
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const rows = await store.queryFirst<any>(
|
|
184
|
+
`SELECT id, name, description, preconditions, steps, postconditions,
|
|
185
|
+
success_count AS successCount, failure_count AS failureCount,
|
|
186
|
+
avg_duration_ms AS avgDurationMs,
|
|
187
|
+
vector::similarity::cosine(embedding, $vec) AS score
|
|
188
|
+
FROM skill
|
|
189
|
+
WHERE embedding != NONE AND array::len(embedding) > 0 AND (active = NONE OR active = true)
|
|
190
|
+
ORDER BY score DESC LIMIT $lim`,
|
|
191
|
+
{ vec: queryVec, lim: limit },
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
return rows
|
|
195
|
+
.filter((r: any) => (r.score ?? 0) > 0.4)
|
|
196
|
+
.map((r: any) => ({
|
|
197
|
+
id: String(r.id),
|
|
198
|
+
name: r.name ?? "",
|
|
199
|
+
description: r.description ?? "",
|
|
200
|
+
preconditions: r.preconditions,
|
|
201
|
+
steps: Array.isArray(r.steps) ? r.steps : [],
|
|
202
|
+
postconditions: r.postconditions,
|
|
203
|
+
successCount: Number(r.successCount ?? 1),
|
|
204
|
+
failureCount: Number(r.failureCount ?? 0),
|
|
205
|
+
avgDurationMs: Number(r.avgDurationMs ?? 0),
|
|
206
|
+
confidence: Number(r.confidence ?? 1.0),
|
|
207
|
+
active: r.active !== false,
|
|
208
|
+
score: r.score,
|
|
209
|
+
}));
|
|
210
|
+
} catch (e) {
|
|
211
|
+
swallow.warn("skills:find", e);
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Format matched skills as a structured context block for the LLM.
|
|
218
|
+
*/
|
|
219
|
+
export function formatSkillContext(skills: Skill[]): string {
|
|
220
|
+
if (skills.length === 0) return "";
|
|
221
|
+
|
|
222
|
+
const lines = skills.map((s) => {
|
|
223
|
+
const total = s.successCount + s.failureCount;
|
|
224
|
+
const rate = total > 0 ? `${s.successCount}/${total} successful` : "new";
|
|
225
|
+
const stepsStr = s.steps
|
|
226
|
+
.map((step, i) => ` ${i + 1}. [${step.tool}] ${step.description}`)
|
|
227
|
+
.join("\n");
|
|
228
|
+
return `### ${s.name} (${rate})\n${s.description}\n${s.preconditions ? `Pre: ${s.preconditions}\n` : ""}Steps:\n${stepsStr}${s.postconditions ? `\nPost: ${s.postconditions}` : ""}`;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return `\n<skill_context>\n[Previously successful procedures — adapt as needed, don't follow blindly]\n${lines.join("\n\n")}\n</skill_context>`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Record skill outcome when a retrieved skill is used in a turn.
|
|
236
|
+
*/
|
|
237
|
+
export async function recordSkillOutcome(
|
|
238
|
+
skillId: string,
|
|
239
|
+
success: boolean,
|
|
240
|
+
durationMs: number,
|
|
241
|
+
store: SurrealStore,
|
|
242
|
+
): Promise<void> {
|
|
243
|
+
if (!store.isAvailable()) return;
|
|
244
|
+
const RECORD_ID_RE = /^[a-zA-Z_][a-zA-Z0-9_]*:[a-zA-Z0-9_]+$/;
|
|
245
|
+
if (!RECORD_ID_RE.test(skillId)) return;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const field = success ? "success_count" : "failure_count";
|
|
249
|
+
await store.queryExec(
|
|
250
|
+
`UPDATE ${skillId} SET
|
|
251
|
+
${field} += 1,
|
|
252
|
+
avg_duration_ms = (avg_duration_ms * (success_count + failure_count - 1) + $dur) / (success_count + failure_count),
|
|
253
|
+
last_used = time::now()`,
|
|
254
|
+
{ dur: durationMs },
|
|
255
|
+
);
|
|
256
|
+
} catch (e) { swallow("skills:non-critical", e); }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// --- Causal Chain -> Skill Graduation ---
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Promote recurring successful causal chains into reusable skills.
|
|
263
|
+
* When 3+ successful chains of the same type exist, synthesize a skill.
|
|
264
|
+
*/
|
|
265
|
+
export async function graduateCausalToSkills(
|
|
266
|
+
store: SurrealStore,
|
|
267
|
+
embeddings: EmbeddingService,
|
|
268
|
+
complete: CompleteFn,
|
|
269
|
+
): Promise<number> {
|
|
270
|
+
if (!store.isAvailable()) return 0;
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const groups = await store.queryFirst<{ chain_type: string; cnt: number; descriptions: string[] }>(
|
|
274
|
+
`SELECT chain_type, count() AS cnt, array::group(description) AS descriptions
|
|
275
|
+
FROM causal_chain
|
|
276
|
+
WHERE success = true AND confidence >= 0.7
|
|
277
|
+
GROUP BY chain_type`,
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
let created = 0;
|
|
281
|
+
|
|
282
|
+
for (const group of groups) {
|
|
283
|
+
if (group.cnt < 3) continue;
|
|
284
|
+
|
|
285
|
+
// Check if a skill already covers this chain type
|
|
286
|
+
const existing = await store.queryFirst<{ id: string }>(
|
|
287
|
+
`SELECT id FROM skill WHERE string::lowercase(name) CONTAINS string::lowercase($ct) LIMIT 1`,
|
|
288
|
+
{ ct: group.chain_type },
|
|
289
|
+
);
|
|
290
|
+
if (existing.length > 0) continue;
|
|
291
|
+
|
|
292
|
+
const resp = await complete({
|
|
293
|
+
system: `Return JSON: {name, description, preconditions, steps: [{tool, description}] (max 6), postconditions}. Synthesize a reusable procedure from these recurring patterns. Generic — no specific file paths or variable names.`,
|
|
294
|
+
messages: [{
|
|
295
|
+
role: "user",
|
|
296
|
+
content: `${group.cnt} successful "${group.chain_type}" patterns:\n${group.descriptions.slice(0, 8).join("\n")}`,
|
|
297
|
+
}],
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const text = resp.text;
|
|
301
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
302
|
+
if (!jsonMatch) continue;
|
|
303
|
+
|
|
304
|
+
let parsed: ExtractedSkill;
|
|
305
|
+
try { parsed = JSON.parse(jsonMatch[0]); } catch { continue; }
|
|
306
|
+
if (!parsed.name || !Array.isArray(parsed.steps) || parsed.steps.length === 0) continue;
|
|
307
|
+
|
|
308
|
+
let skillEmb: number[] | null = null;
|
|
309
|
+
if (embeddings.isAvailable()) {
|
|
310
|
+
try { skillEmb = await embeddings.embed(`${parsed.name}: ${parsed.description}`); } catch (e) { swallow("skills:ok", e); }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const record: Record<string, unknown> = {
|
|
314
|
+
name: String(parsed.name).slice(0, 100),
|
|
315
|
+
description: String(parsed.description).slice(0, 200),
|
|
316
|
+
preconditions: parsed.preconditions ? String(parsed.preconditions).slice(0, 200) : undefined,
|
|
317
|
+
steps: parsed.steps.slice(0, 6).map((s) => ({
|
|
318
|
+
tool: String(s.tool ?? "unknown"),
|
|
319
|
+
description: String(s.description ?? "").slice(0, 200),
|
|
320
|
+
})),
|
|
321
|
+
postconditions: parsed.postconditions ? String(parsed.postconditions).slice(0, 200) : undefined,
|
|
322
|
+
graduated_from: group.chain_type,
|
|
323
|
+
confidence: 1.0,
|
|
324
|
+
active: true,
|
|
325
|
+
};
|
|
326
|
+
if (skillEmb?.length) record.embedding = skillEmb;
|
|
327
|
+
|
|
328
|
+
const rows = await store.queryFirst<{ id: string }>(
|
|
329
|
+
`CREATE skill CONTENT $record RETURN id`,
|
|
330
|
+
{ record },
|
|
331
|
+
);
|
|
332
|
+
if (rows[0]?.id) {
|
|
333
|
+
await supersedeOldSkills(String(rows[0].id), skillEmb ?? [], store);
|
|
334
|
+
created++;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return created;
|
|
339
|
+
} catch (e) {
|
|
340
|
+
swallow.warn("skills:graduateCausal", e);
|
|
341
|
+
return 0;
|
|
342
|
+
}
|
|
343
|
+
}
|