kongbrain 0.3.11 → 0.3.14
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/README.github.md +7 -7
- package/README.md +7 -7
- package/README.npm.md +7 -7
- package/SKILL.md +7 -8
- package/openclaw.plugin.json +4 -0
- package/package.json +1 -1
- package/scripts/backfill-embeddings.ts +74 -0
- package/src/concept-extract.ts +42 -0
- package/src/context-engine.ts +3 -21
- package/src/daemon-manager.ts +1 -1
- package/src/daemon-types.ts +1 -0
- package/src/hooks/after-tool-call.ts +3 -3
- package/src/memory-daemon.ts +92 -54
- package/src/schema.surql +2 -0
- package/src/skills.ts +3 -3
- package/src/surreal.ts +13 -3
package/README.github.md
CHANGED
|
@@ -60,15 +60,13 @@ npm install -g openclaw
|
|
|
60
60
|
|
|
61
61
|
Install SurrealDB via your platform's package manager (see [surrealdb.com/install](https://surrealdb.com/docs/surrealdb/installation)):
|
|
62
62
|
|
|
63
|
+
macOS:
|
|
63
64
|
```bash
|
|
64
|
-
# macOS
|
|
65
65
|
brew install surrealdb/tap/surreal
|
|
66
|
-
|
|
67
|
-
# Linux (Debian/Ubuntu)
|
|
68
|
-
curl -sSf https://install.surrealdb.com | sh
|
|
69
|
-
export PATH="$HOME/.surrealdb:$PATH"
|
|
70
66
|
```
|
|
71
67
|
|
|
68
|
+
Linux — see `https://surrealdb.com/docs/surrealdb/installation` for your distro.
|
|
69
|
+
|
|
72
70
|
Then start it locally, **change the credentials before use**:
|
|
73
71
|
|
|
74
72
|
```bash
|
|
@@ -88,11 +86,13 @@ docker run -d --name surrealdb -p 127.0.0.1:8042:8000 \
|
|
|
88
86
|
|
|
89
87
|
### 3. Install KongBrain
|
|
90
88
|
|
|
89
|
+
From ClawHub (recommended):
|
|
91
90
|
```bash
|
|
92
|
-
# From ClawHub (recommended)
|
|
93
91
|
openclaw plugins install clawhub:kongbrain
|
|
92
|
+
```
|
|
94
93
|
|
|
95
|
-
|
|
94
|
+
From npm:
|
|
95
|
+
```bash
|
|
96
96
|
openclaw plugins install kongbrain
|
|
97
97
|
```
|
|
98
98
|
|
package/README.md
CHANGED
|
@@ -50,15 +50,13 @@ npm install -g openclaw
|
|
|
50
50
|
|
|
51
51
|
Install SurrealDB via your platform's package manager (see [surrealdb.com/install](https://surrealdb.com/docs/surrealdb/installation)):
|
|
52
52
|
|
|
53
|
+
macOS:
|
|
53
54
|
```bash
|
|
54
|
-
# macOS
|
|
55
55
|
brew install surrealdb/tap/surreal
|
|
56
|
-
|
|
57
|
-
# Linux (Debian/Ubuntu)
|
|
58
|
-
curl -sSf https://install.surrealdb.com | sh
|
|
59
|
-
export PATH="$HOME/.surrealdb:$PATH"
|
|
60
56
|
```
|
|
61
57
|
|
|
58
|
+
Linux — see `https://surrealdb.com/docs/surrealdb/installation` for your distro.
|
|
59
|
+
|
|
62
60
|
Then start it locally — **change the credentials before use**:
|
|
63
61
|
|
|
64
62
|
```bash
|
|
@@ -78,11 +76,13 @@ docker run -d --name surrealdb -p 127.0.0.1:8042:8000 \
|
|
|
78
76
|
|
|
79
77
|
### 3. Install KongBrain
|
|
80
78
|
|
|
79
|
+
From ClawHub (recommended):
|
|
81
80
|
```bash
|
|
82
|
-
# From ClawHub (recommended)
|
|
83
81
|
openclaw plugins install clawhub:kongbrain
|
|
82
|
+
```
|
|
84
83
|
|
|
85
|
-
|
|
84
|
+
From npm:
|
|
85
|
+
```bash
|
|
86
86
|
openclaw plugins install kongbrain
|
|
87
87
|
```
|
|
88
88
|
|
package/README.npm.md
CHANGED
|
@@ -50,15 +50,13 @@ npm install -g openclaw
|
|
|
50
50
|
|
|
51
51
|
Install SurrealDB via your platform's package manager (see [surrealdb.com/install](https://surrealdb.com/docs/surrealdb/installation)):
|
|
52
52
|
|
|
53
|
+
macOS:
|
|
53
54
|
```bash
|
|
54
|
-
# macOS
|
|
55
55
|
brew install surrealdb/tap/surreal
|
|
56
|
-
|
|
57
|
-
# Linux (Debian/Ubuntu)
|
|
58
|
-
curl -sSf https://install.surrealdb.com | sh
|
|
59
|
-
export PATH="$HOME/.surrealdb:$PATH"
|
|
60
56
|
```
|
|
61
57
|
|
|
58
|
+
Linux — see `https://surrealdb.com/docs/surrealdb/installation` for your distro.
|
|
59
|
+
|
|
62
60
|
Then start it locally — **change the credentials before use**:
|
|
63
61
|
|
|
64
62
|
```bash
|
|
@@ -78,11 +76,13 @@ docker run -d --name surrealdb -p 127.0.0.1:8042:8000 \
|
|
|
78
76
|
|
|
79
77
|
### 3. Install KongBrain
|
|
80
78
|
|
|
79
|
+
From ClawHub (recommended):
|
|
81
80
|
```bash
|
|
82
|
-
# From ClawHub (recommended)
|
|
83
81
|
openclaw plugins install clawhub:kongbrain
|
|
82
|
+
```
|
|
84
83
|
|
|
85
|
-
|
|
84
|
+
From npm:
|
|
85
|
+
```bash
|
|
86
86
|
openclaw plugins install kongbrain
|
|
87
87
|
```
|
|
88
88
|
|
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: kongbrain
|
|
3
3
|
description: Graph-backed persistent memory engine for OpenClaw. Replaces the default context window with SurrealDB + vector embeddings that learn across sessions.
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.14
|
|
5
5
|
homepage: https://github.com/42U/kongbrain
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|
|
@@ -48,23 +48,22 @@ KongBrain gives your OpenClaw agent persistent, structured memory:
|
|
|
48
48
|
|
|
49
49
|
See the official install guide: https://surrealdb.com/docs/surrealdb/installation
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
macOS:
|
|
53
52
|
```bash
|
|
54
|
-
# macOS
|
|
55
53
|
brew install surrealdb/tap/surreal
|
|
54
|
+
```
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
curl -sSf https://install.surrealdb.com | sh
|
|
56
|
+
Linux — see `https://surrealdb.com/docs/surrealdb/installation` for your distro.
|
|
59
57
|
|
|
60
|
-
|
|
58
|
+
Docker:
|
|
59
|
+
```bash
|
|
61
60
|
docker pull surrealdb/surrealdb:latest
|
|
62
61
|
```
|
|
63
62
|
|
|
64
63
|
### Start SurrealDB
|
|
65
64
|
|
|
65
|
+
Local only (recommended) — use strong credentials in production:
|
|
66
66
|
```bash
|
|
67
|
-
# Local only (recommended) - use strong credentials in production
|
|
68
67
|
surreal start --user youruser --pass yourpass --bind 127.0.0.1:8000 surrealkv:~/.kongbrain/surreal.db
|
|
69
68
|
```
|
|
70
69
|
|
package/openclaw.plugin.json
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
"name": "KongBrain",
|
|
4
4
|
"description": "Graph-backed cognitive context engine with SurrealDB + BGE-M3",
|
|
5
5
|
"kind": "context-engine",
|
|
6
|
+
"requires": {
|
|
7
|
+
"bins": ["surreal"],
|
|
8
|
+
"env": ["SURREAL_URL", "SURREAL_USER", "SURREAL_PASS", "SURREAL_NS", "SURREAL_DB"]
|
|
9
|
+
},
|
|
6
10
|
"uiHints": {
|
|
7
11
|
"surreal.url": {
|
|
8
12
|
"label": "SurrealDB URL",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kongbrain",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.14",
|
|
4
4
|
"description": "Graph-backed persistent memory engine for OpenClaw. Replaces the default context window with SurrealDB + vector embeddings that learn across sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* One-shot backfill: embed all concepts that have `content` but no embedding vector.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* cd /home/zero/voidorigin/kongbrain
|
|
7
|
+
* npx tsx scripts/backfill-embeddings.ts
|
|
8
|
+
*
|
|
9
|
+
* Env vars (all have defaults matching the plugin config):
|
|
10
|
+
* SURREAL_URL (default: ws://localhost:8042/rpc)
|
|
11
|
+
* SURREAL_USER (default: root)
|
|
12
|
+
* SURREAL_PASS (default: root)
|
|
13
|
+
* SURREAL_NS (default: kong)
|
|
14
|
+
* SURREAL_DB (default: memory)
|
|
15
|
+
* EMBED_MODEL_PATH (default: ~/.node-llama-cpp/models/bge-m3-q4_k_m.gguf)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { parsePluginConfig } from "../src/config.js";
|
|
19
|
+
import { SurrealStore } from "../src/surreal.js";
|
|
20
|
+
import { EmbeddingService } from "../src/embeddings.js";
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
const config = parsePluginConfig();
|
|
24
|
+
const store = new SurrealStore(config.surreal);
|
|
25
|
+
const embeddings = new EmbeddingService(config.embedding);
|
|
26
|
+
|
|
27
|
+
console.log("[backfill] Connecting to SurrealDB...");
|
|
28
|
+
await store.initialize();
|
|
29
|
+
|
|
30
|
+
console.log("[backfill] Loading embedding model...");
|
|
31
|
+
await embeddings.initialize();
|
|
32
|
+
|
|
33
|
+
// Find concepts with content but no embedding
|
|
34
|
+
const bare = await store.queryFirst<{ id: string; content: string }>(
|
|
35
|
+
`SELECT id, content FROM concept
|
|
36
|
+
WHERE content IS NOT NONE AND content != ''
|
|
37
|
+
AND (embedding IS NONE OR array::len(embedding) = 0)`,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
console.log(`[backfill] Found ${bare.length} concepts needing embeddings.`);
|
|
41
|
+
if (bare.length === 0) {
|
|
42
|
+
console.log("[backfill] Nothing to do.");
|
|
43
|
+
await embeddings.dispose();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let ok = 0;
|
|
48
|
+
let fail = 0;
|
|
49
|
+
|
|
50
|
+
for (const concept of bare) {
|
|
51
|
+
const id = String(concept.id);
|
|
52
|
+
try {
|
|
53
|
+
const vec = await embeddings.embed(concept.content);
|
|
54
|
+
await store.queryExec(
|
|
55
|
+
`UPDATE ${id} SET embedding = $emb`,
|
|
56
|
+
{ emb: vec },
|
|
57
|
+
);
|
|
58
|
+
ok++;
|
|
59
|
+
if (ok % 10 === 0) console.log(`[backfill] ${ok}/${bare.length} done...`);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
fail++;
|
|
62
|
+
console.error(`[backfill] Failed ${id}: ${e}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(`[backfill] Complete. Embedded: ${ok}, Failed: ${fail}`);
|
|
67
|
+
await embeddings.dispose();
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
main().catch((e) => {
|
|
72
|
+
console.error("[backfill] Fatal:", e);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
});
|
package/src/concept-extract.ts
CHANGED
|
@@ -79,6 +79,48 @@ export async function upsertAndLinkConcepts(
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Embedding-based concept linking — replaces batch-local linkToConcepts.
|
|
84
|
+
*
|
|
85
|
+
* Given a source node (memory, artifact, turn, skill) and its text content,
|
|
86
|
+
* embeds the text and finds the top-N most similar concepts in the graph,
|
|
87
|
+
* then creates edges from source → concept via the specified relation.
|
|
88
|
+
*
|
|
89
|
+
* This ensures linking works even when relevant concepts were created in
|
|
90
|
+
* prior batches or sessions — no batch-timing dependency.
|
|
91
|
+
*/
|
|
92
|
+
export async function linkToRelevantConcepts(
|
|
93
|
+
sourceId: string,
|
|
94
|
+
edgeName: string,
|
|
95
|
+
text: string,
|
|
96
|
+
store: SurrealStore,
|
|
97
|
+
embeddings: EmbeddingService,
|
|
98
|
+
logTag: string,
|
|
99
|
+
limit = 5,
|
|
100
|
+
threshold = 0.65,
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
if (!embeddings.isAvailable() || !text) return;
|
|
103
|
+
try {
|
|
104
|
+
const vec = await embeddings.embed(text);
|
|
105
|
+
if (!vec?.length) return;
|
|
106
|
+
const matches = await store.queryFirst<{ id: string; score: number }>(
|
|
107
|
+
`SELECT id, vector::similarity::cosine(embedding, $vec) AS score
|
|
108
|
+
FROM concept
|
|
109
|
+
WHERE embedding != NONE AND array::len(embedding) > 0
|
|
110
|
+
ORDER BY score DESC
|
|
111
|
+
LIMIT $lim`,
|
|
112
|
+
{ vec, lim: limit },
|
|
113
|
+
);
|
|
114
|
+
for (const m of matches) {
|
|
115
|
+
if (m.score < threshold) break;
|
|
116
|
+
await store.relate(sourceId, edgeName, String(m.id))
|
|
117
|
+
.catch(e => swallow(`${logTag}:relate`, e));
|
|
118
|
+
}
|
|
119
|
+
} catch (e) {
|
|
120
|
+
swallow(`${logTag}:embed`, e);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
82
124
|
/**
|
|
83
125
|
* Link a newly-upserted concept to existing concepts via narrower/broader
|
|
84
126
|
* edges when one concept's name is a substring of the other (indicating a
|
package/src/context-engine.ts
CHANGED
|
@@ -49,7 +49,6 @@ import { extractSkill } from "./skills.js";
|
|
|
49
49
|
import { generateReflection } from "./reflection.js";
|
|
50
50
|
import { graduateCausalToSkills } from "./skills.js";
|
|
51
51
|
import { swallow } from "./errors.js";
|
|
52
|
-
import { upsertAndLinkConcepts } from "./concept-extract.js";
|
|
53
52
|
|
|
54
53
|
export class KongBrainContextEngine implements ContextEngine {
|
|
55
54
|
readonly info: ContextEngineInfo = {
|
|
@@ -265,11 +264,7 @@ export class KongBrainContextEngine implements ContextEngine {
|
|
|
265
264
|
.catch(e => swallow.warn("ingest:responds_to", e));
|
|
266
265
|
}
|
|
267
266
|
|
|
268
|
-
//
|
|
269
|
-
if (worthEmbedding) {
|
|
270
|
-
extractAndLinkConcepts(turnId, text, this.state, session)
|
|
271
|
-
.catch(e => swallow.warn("ingest:concepts", e));
|
|
272
|
-
}
|
|
267
|
+
// Concept extraction (mentions edges) handled by daemon via LLM
|
|
273
268
|
}
|
|
274
269
|
|
|
275
270
|
if (role === "user") {
|
|
@@ -400,7 +395,7 @@ export class KongBrainContextEngine implements ContextEngine {
|
|
|
400
395
|
const turnData = recentTurns.map(t => ({
|
|
401
396
|
role: t.role as "user" | "assistant",
|
|
402
397
|
text: t.text,
|
|
403
|
-
turnId: (t as any).id,
|
|
398
|
+
turnId: String((t as any).id ?? ""),
|
|
404
399
|
}));
|
|
405
400
|
|
|
406
401
|
// Gather retrieved memory IDs for dedup
|
|
@@ -441,7 +436,7 @@ export class KongBrainContextEngine implements ContextEngine {
|
|
|
441
436
|
const turnData = recentTurns.map(t => ({
|
|
442
437
|
role: t.role as "user" | "assistant",
|
|
443
438
|
text: t.text,
|
|
444
|
-
turnId: (t as any).id,
|
|
439
|
+
turnId: String((t as any).id ?? ""),
|
|
445
440
|
}));
|
|
446
441
|
session.daemon!.sendTurnBatch(turnData, [...session.pendingThinking], []);
|
|
447
442
|
})
|
|
@@ -537,16 +532,3 @@ function hasSemantic(text: string): boolean {
|
|
|
537
532
|
}
|
|
538
533
|
|
|
539
534
|
// --- Concept extraction (delegates to shared helper) ---
|
|
540
|
-
|
|
541
|
-
async function extractAndLinkConcepts(
|
|
542
|
-
turnId: string,
|
|
543
|
-
text: string,
|
|
544
|
-
state: GlobalPluginState,
|
|
545
|
-
session?: SessionState,
|
|
546
|
-
): Promise<void> {
|
|
547
|
-
await upsertAndLinkConcepts(
|
|
548
|
-
turnId, "mentions", text,
|
|
549
|
-
state.store, state.embeddings, "concepts",
|
|
550
|
-
session ? { taskId: session.taskId, projectId: session.projectId } : undefined,
|
|
551
|
-
);
|
|
552
|
-
}
|
package/src/daemon-manager.ts
CHANGED
|
@@ -121,7 +121,7 @@ export function startMemoryDaemon(
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
const counts = await writeExtractionResults(result, sessionId, store, embeddings, priorState, taskId, projectId);
|
|
124
|
+
const counts = await writeExtractionResults(result, sessionId, store, embeddings, priorState, taskId, projectId, turns);
|
|
125
125
|
extractedTurnCount = turns.length;
|
|
126
126
|
}
|
|
127
127
|
|
package/src/daemon-types.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import type { GlobalPluginState } from "../state.js";
|
|
6
6
|
import { recordToolOutcome } from "../retrieval-quality.js";
|
|
7
7
|
import { swallow } from "../errors.js";
|
|
8
|
-
import {
|
|
8
|
+
import { linkToRelevantConcepts } from "../concept-extract.js";
|
|
9
9
|
|
|
10
10
|
export function createAfterToolCallHandler(state: GlobalPluginState) {
|
|
11
11
|
return async (
|
|
@@ -128,8 +128,8 @@ async function trackArtifact(
|
|
|
128
128
|
await state.store.relate(artifactId, "used_in", projectId)
|
|
129
129
|
.catch(e => swallow.warn("artifact:used_in", e));
|
|
130
130
|
}
|
|
131
|
-
// Link artifact to concepts it mentions
|
|
132
|
-
await
|
|
131
|
+
// Link artifact to concepts it mentions (embedding-based similarity)
|
|
132
|
+
await linkToRelevantConcepts(
|
|
133
133
|
artifactId, "artifact_mentions", description,
|
|
134
134
|
state.store, state.embeddings, "artifact:concepts",
|
|
135
135
|
);
|
package/src/memory-daemon.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { SurrealStore } from "./surreal.js";
|
|
|
13
13
|
import type { EmbeddingService } from "./embeddings.js";
|
|
14
14
|
import { swallow } from "./errors.js";
|
|
15
15
|
import { assertRecordId } from "./surreal.js";
|
|
16
|
-
import { upsertAndLinkConcepts, linkConceptHierarchy } from "./concept-extract.js";
|
|
16
|
+
import { upsertAndLinkConcepts, linkConceptHierarchy, linkToRelevantConcepts } from "./concept-extract.js";
|
|
17
17
|
|
|
18
18
|
// --- Build the extraction prompt ---
|
|
19
19
|
|
|
@@ -127,12 +127,66 @@ export async function writeExtractionResults(
|
|
|
127
127
|
priorState: PriorExtractions,
|
|
128
128
|
taskId?: string,
|
|
129
129
|
projectId?: string,
|
|
130
|
+
turns?: TurnData[],
|
|
130
131
|
): Promise<ExtractionCounts> {
|
|
131
132
|
const counts: ExtractionCounts = {
|
|
132
133
|
causal: 0, monologue: 0, resolved: 0, concept: 0,
|
|
133
134
|
correction: 0, preference: 0, artifact: 0, decision: 0, skill: 0,
|
|
134
135
|
};
|
|
135
136
|
|
|
137
|
+
// ── Phase 1: Upsert concepts first (LLM-extracted) so we have IDs ────
|
|
138
|
+
// These IDs are used to create mentions/about_concept/artifact_mentions
|
|
139
|
+
// edges in Phase 2, replacing the old regex-based extraction.
|
|
140
|
+
|
|
141
|
+
const extractedConceptIds: string[] = [];
|
|
142
|
+
|
|
143
|
+
if (Array.isArray(result.concepts) && result.concepts.length > 0) {
|
|
144
|
+
for (const c of result.concepts.slice(0, 11)) {
|
|
145
|
+
if (!c.name || !c.content) continue;
|
|
146
|
+
if (priorState.conceptNames.includes(c.name)) continue;
|
|
147
|
+
counts.concept++;
|
|
148
|
+
priorState.conceptNames.push(c.name);
|
|
149
|
+
try {
|
|
150
|
+
let emb: number[] | null = null;
|
|
151
|
+
if (embeddings.isAvailable()) {
|
|
152
|
+
try { emb = await embeddings.embed(c.content); } catch (e) { swallow("daemon:embedConcept", e); }
|
|
153
|
+
}
|
|
154
|
+
const conceptId = await store.upsertConcept(c.content, emb, `daemon:${sessionId}`);
|
|
155
|
+
if (conceptId) {
|
|
156
|
+
extractedConceptIds.push(conceptId);
|
|
157
|
+
await linkConceptHierarchy(conceptId, c.name, store, embeddings, "daemon:concept");
|
|
158
|
+
if (taskId) {
|
|
159
|
+
await store.relate(conceptId, "derived_from", taskId)
|
|
160
|
+
.catch(e => swallow("daemon:concept:derived_from", e));
|
|
161
|
+
}
|
|
162
|
+
if (projectId) {
|
|
163
|
+
await store.relate(conceptId, "relevant_to", projectId)
|
|
164
|
+
.catch(e => swallow("daemon:concept:relevant_to", e));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch (e) {
|
|
168
|
+
swallow.warn("daemon:upsertConcept", e);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Phase 2: Create mentions edges (turn → concept) via embedding similarity ─
|
|
174
|
+
// Each turn's text is embedded and matched against existing concepts in the
|
|
175
|
+
// graph. This replaces the old batch-local linking that only worked when
|
|
176
|
+
// concepts and turns were extracted in the same batch.
|
|
177
|
+
|
|
178
|
+
if (turns && turns.length > 0) {
|
|
179
|
+
const turnIds = turns.filter(t => t.turnId && t.text).slice(0, 15);
|
|
180
|
+
for (const t of turnIds) {
|
|
181
|
+
await linkToRelevantConcepts(
|
|
182
|
+
t.turnId!, "mentions", t.text!,
|
|
183
|
+
store, embeddings, "daemon:mentions", 5, 0.65,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Phase 3: All other extractions in parallel ───────────────────────
|
|
189
|
+
|
|
136
190
|
const writeOps: Promise<void>[] = [];
|
|
137
191
|
|
|
138
192
|
// 1. Causal chains
|
|
@@ -187,37 +241,7 @@ export async function writeExtractionResults(
|
|
|
187
241
|
})());
|
|
188
242
|
}
|
|
189
243
|
|
|
190
|
-
// 4.
|
|
191
|
-
if (Array.isArray(result.concepts) && result.concepts.length > 0) {
|
|
192
|
-
for (const c of result.concepts.slice(0, 11)) {
|
|
193
|
-
if (!c.name || !c.content) continue;
|
|
194
|
-
if (priorState.conceptNames.includes(c.name)) continue;
|
|
195
|
-
counts.concept++;
|
|
196
|
-
priorState.conceptNames.push(c.name);
|
|
197
|
-
writeOps.push((async () => {
|
|
198
|
-
let emb: number[] | null = null;
|
|
199
|
-
if (embeddings.isAvailable()) {
|
|
200
|
-
try { emb = await embeddings.embed(c.content); } catch (e) { swallow("daemon:embedConcept", e); }
|
|
201
|
-
}
|
|
202
|
-
const conceptId = await store.upsertConcept(c.content, emb, `daemon:${sessionId}`);
|
|
203
|
-
if (conceptId) {
|
|
204
|
-
await linkConceptHierarchy(conceptId, c.name, store, embeddings, "daemon:concept");
|
|
205
|
-
// derived_from: concept → task
|
|
206
|
-
if (taskId) {
|
|
207
|
-
await store.relate(conceptId, "derived_from", taskId)
|
|
208
|
-
.catch(e => swallow("daemon:concept:derived_from", e));
|
|
209
|
-
}
|
|
210
|
-
// relevant_to: concept → project
|
|
211
|
-
if (projectId) {
|
|
212
|
-
await store.relate(conceptId, "relevant_to", projectId)
|
|
213
|
-
.catch(e => swallow("daemon:concept:relevant_to", e));
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
})());
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// 5. Corrections — high-importance memories
|
|
244
|
+
// 4. Corrections — high-importance memories, linked to LLM-extracted concepts
|
|
221
245
|
if (Array.isArray(result.corrections) && result.corrections.length > 0) {
|
|
222
246
|
for (const c of result.corrections.slice(0, 5)) {
|
|
223
247
|
if (!c.original || !c.correction) continue;
|
|
@@ -230,13 +254,13 @@ export async function writeExtractionResults(
|
|
|
230
254
|
}
|
|
231
255
|
const memId = await store.createMemory(text, emb, 9, "correction", sessionId);
|
|
232
256
|
if (memId) {
|
|
233
|
-
await
|
|
257
|
+
await linkToRelevantConcepts(memId, "about_concept", text, store, embeddings, "daemon:correction:about_concept");
|
|
234
258
|
}
|
|
235
259
|
})());
|
|
236
260
|
}
|
|
237
261
|
}
|
|
238
262
|
|
|
239
|
-
//
|
|
263
|
+
// 5. User preferences
|
|
240
264
|
if (Array.isArray(result.preferences) && result.preferences.length > 0) {
|
|
241
265
|
for (const p of result.preferences.slice(0, 5)) {
|
|
242
266
|
if (!p.preference) continue;
|
|
@@ -249,13 +273,13 @@ export async function writeExtractionResults(
|
|
|
249
273
|
}
|
|
250
274
|
const memId = await store.createMemory(text, emb, 7, "preference", sessionId);
|
|
251
275
|
if (memId) {
|
|
252
|
-
await
|
|
276
|
+
await linkToRelevantConcepts(memId, "about_concept", text, store, embeddings, "daemon:preference:about_concept");
|
|
253
277
|
}
|
|
254
278
|
})());
|
|
255
279
|
}
|
|
256
280
|
}
|
|
257
281
|
|
|
258
|
-
//
|
|
282
|
+
// 6. Artifacts
|
|
259
283
|
if (Array.isArray(result.artifacts) && result.artifacts.length > 0) {
|
|
260
284
|
for (const a of result.artifacts.slice(0, 10)) {
|
|
261
285
|
if (!a.path) continue;
|
|
@@ -270,7 +294,7 @@ export async function writeExtractionResults(
|
|
|
270
294
|
}
|
|
271
295
|
const artId = await store.createArtifact(a.path, a.action ?? "modified", desc, emb);
|
|
272
296
|
if (artId) {
|
|
273
|
-
await
|
|
297
|
+
await linkToRelevantConcepts(artId, "artifact_mentions", `${a.path} ${desc}`, store, embeddings, "daemon:artifact:artifact_mentions");
|
|
274
298
|
// used_in: artifact → project
|
|
275
299
|
if (projectId) {
|
|
276
300
|
await store.relate(artId, "used_in", projectId)
|
|
@@ -281,7 +305,7 @@ export async function writeExtractionResults(
|
|
|
281
305
|
}
|
|
282
306
|
}
|
|
283
307
|
|
|
284
|
-
//
|
|
308
|
+
// 7. Decisions
|
|
285
309
|
if (Array.isArray(result.decisions) && result.decisions.length > 0) {
|
|
286
310
|
for (const d of result.decisions.slice(0, 6)) {
|
|
287
311
|
if (!d.decision) continue;
|
|
@@ -294,13 +318,13 @@ export async function writeExtractionResults(
|
|
|
294
318
|
}
|
|
295
319
|
const memId = await store.createMemory(text, emb, 7, "decision", sessionId);
|
|
296
320
|
if (memId) {
|
|
297
|
-
await
|
|
321
|
+
await linkToRelevantConcepts(memId, "about_concept", text, store, embeddings, "daemon:decision:about_concept");
|
|
298
322
|
}
|
|
299
323
|
})());
|
|
300
324
|
}
|
|
301
325
|
}
|
|
302
326
|
|
|
303
|
-
//
|
|
327
|
+
// 8. Skills — get ID back to create skill_from_task + skill_uses_concept edges
|
|
304
328
|
if (Array.isArray(result.skills) && result.skills.length > 0) {
|
|
305
329
|
for (const s of result.skills.slice(0, 3)) {
|
|
306
330
|
if (!s.name || !Array.isArray(s.steps) || s.steps.length === 0) continue;
|
|
@@ -313,21 +337,35 @@ export async function writeExtractionResults(
|
|
|
313
337
|
if (embeddings.isAvailable()) {
|
|
314
338
|
try { emb = await embeddings.embed(content); } catch (e) { swallow("daemon:embedSkill", e); }
|
|
315
339
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
340
|
+
try {
|
|
341
|
+
const rows = await store.queryFirst<{ id: string }>(
|
|
342
|
+
`CREATE skill CONTENT $record RETURN id`,
|
|
343
|
+
{
|
|
344
|
+
record: {
|
|
345
|
+
name: String(s.name).slice(0, 100),
|
|
346
|
+
description: content,
|
|
347
|
+
content,
|
|
348
|
+
steps: s.steps.map((st: string) => String(st).slice(0, 200)),
|
|
349
|
+
trigger_context: String(s.trigger_context ?? "").slice(0, 200),
|
|
350
|
+
tags: ["auto-extracted"],
|
|
351
|
+
session_id: sessionId,
|
|
352
|
+
...(emb ? { embedding: emb } : {}),
|
|
353
|
+
},
|
|
328
354
|
},
|
|
329
|
-
|
|
330
|
-
|
|
355
|
+
);
|
|
356
|
+
const skillId = rows[0]?.id ? String(rows[0].id) : null;
|
|
357
|
+
if (skillId) {
|
|
358
|
+
// skill_from_task: skill → task
|
|
359
|
+
if (taskId) {
|
|
360
|
+
await store.relate(skillId, "skill_from_task", taskId)
|
|
361
|
+
.catch(e => swallow.warn("daemon:skill:skill_from_task", e));
|
|
362
|
+
}
|
|
363
|
+
// skill_uses_concept: skill → concept
|
|
364
|
+
await upsertAndLinkConcepts(skillId, "skill_uses_concept", content, store, embeddings, "daemon:skill:concepts");
|
|
365
|
+
}
|
|
366
|
+
} catch (e) {
|
|
367
|
+
swallow.warn("daemon:createSkill", e);
|
|
368
|
+
}
|
|
331
369
|
})());
|
|
332
370
|
}
|
|
333
371
|
}
|
package/src/schema.surql
CHANGED
|
@@ -47,6 +47,8 @@ DEFINE INDEX IF NOT EXISTS artifact_vec_idx ON artifact FIELDS embedding HNSW DI
|
|
|
47
47
|
-- PILLAR 5: Concept (semantic knowledge nodes)
|
|
48
48
|
-- ============================================================
|
|
49
49
|
DEFINE TABLE IF NOT EXISTS concept SCHEMALESS;
|
|
50
|
+
-- Recovery: restore content from name if the rename migration ran before revert
|
|
51
|
+
UPDATE concept SET content = name WHERE content = NONE AND name != NONE;
|
|
50
52
|
DEFINE FIELD IF NOT EXISTS content ON concept TYPE string;
|
|
51
53
|
DEFINE FIELD IF NOT EXISTS embedding ON concept TYPE option<array<float>>;
|
|
52
54
|
DEFINE FIELD IF NOT EXISTS stability ON concept TYPE float DEFAULT 1.0;
|
package/src/skills.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { CompleteFn } from "./state.js";
|
|
|
13
13
|
import type { EmbeddingService } from "./embeddings.js";
|
|
14
14
|
import type { SurrealStore } from "./surreal.js";
|
|
15
15
|
import { swallow } from "./errors.js";
|
|
16
|
-
import {
|
|
16
|
+
import { linkToRelevantConcepts } from "./concept-extract.js";
|
|
17
17
|
import { assertRecordId } from "./surreal.js";
|
|
18
18
|
|
|
19
19
|
// --- Types ---
|
|
@@ -122,7 +122,7 @@ export async function extractSkill(
|
|
|
122
122
|
await supersedeOldSkills(skillId, skillEmb ?? [], store);
|
|
123
123
|
// skill_uses_concept: skill → concept
|
|
124
124
|
const skillDesc = `${parsed.name} ${parsed.description ?? ""} ${(parsed.preconditions ?? "")}`;
|
|
125
|
-
await
|
|
125
|
+
await linkToRelevantConcepts(skillId, "skill_uses_concept", skillDesc, store, embeddings, "skills:concepts");
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
return skillId || null;
|
|
@@ -334,7 +334,7 @@ export async function graduateCausalToSkills(
|
|
|
334
334
|
await supersedeOldSkills(gradSkillId, skillEmb ?? [], store);
|
|
335
335
|
// skill_uses_concept: skill → concept
|
|
336
336
|
const skillDesc = `${parsed.name} ${parsed.description ?? ""}`;
|
|
337
|
-
await
|
|
337
|
+
await linkToRelevantConcepts(gradSkillId, "skill_uses_concept", skillDesc, store, embeddings, "skills:graduate:concepts");
|
|
338
338
|
created++;
|
|
339
339
|
}
|
|
340
340
|
}
|
package/src/surreal.ts
CHANGED
|
@@ -684,15 +684,25 @@ export class SurrealStore {
|
|
|
684
684
|
embedding: number[] | null,
|
|
685
685
|
source?: string,
|
|
686
686
|
): Promise<string> {
|
|
687
|
+
if (!content?.trim()) return "";
|
|
688
|
+
content = content.trim();
|
|
687
689
|
const rows = await this.queryFirst<{ id: string }>(
|
|
688
690
|
`SELECT id FROM concept WHERE string::lowercase(content) = string::lowercase($content) LIMIT 1`,
|
|
689
691
|
{ content },
|
|
690
692
|
);
|
|
691
693
|
if (rows.length > 0) {
|
|
692
694
|
const id = String(rows[0].id);
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
695
|
+
// Backfill embedding if the existing concept is missing one
|
|
696
|
+
if (embedding?.length) {
|
|
697
|
+
await this.queryExec(
|
|
698
|
+
`UPDATE ${id} SET access_count += 1, last_accessed = time::now(), embedding = IF embedding IS NONE OR array::len(embedding) = 0 THEN $emb ELSE embedding END`,
|
|
699
|
+
{ emb: embedding },
|
|
700
|
+
);
|
|
701
|
+
} else {
|
|
702
|
+
await this.queryExec(
|
|
703
|
+
`UPDATE ${id} SET access_count += 1, last_accessed = time::now()`,
|
|
704
|
+
);
|
|
705
|
+
}
|
|
696
706
|
return id;
|
|
697
707
|
}
|
|
698
708
|
const emb = embedding?.length ? embedding : undefined;
|