@framers/agentos 0.1.101 → 0.1.103

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.
Files changed (184) hide show
  1. package/README.md +16 -0
  2. package/dist/api/agency.js +1 -1
  3. package/dist/api/agency.js.map +1 -1
  4. package/dist/api/strategies/graph.d.ts.map +1 -1
  5. package/dist/api/strategies/graph.js +1 -0
  6. package/dist/api/strategies/graph.js.map +1 -1
  7. package/dist/api/strategies/sequential.d.ts.map +1 -1
  8. package/dist/api/strategies/sequential.js +1 -0
  9. package/dist/api/strategies/sequential.js.map +1 -1
  10. package/dist/memory/config.d.ts +39 -0
  11. package/dist/memory/config.d.ts.map +1 -1
  12. package/dist/memory/config.js.map +1 -1
  13. package/dist/memory/consolidation/ConsolidationLoop.d.ts +177 -0
  14. package/dist/memory/consolidation/ConsolidationLoop.d.ts.map +1 -0
  15. package/dist/memory/consolidation/ConsolidationLoop.js +517 -0
  16. package/dist/memory/consolidation/ConsolidationLoop.js.map +1 -0
  17. package/dist/memory/consolidation/ConsolidationPipeline.d.ts.map +1 -1
  18. package/dist/memory/consolidation/ConsolidationPipeline.js +7 -0
  19. package/dist/memory/consolidation/ConsolidationPipeline.js.map +1 -1
  20. package/dist/memory/consolidation/index.d.ts +8 -0
  21. package/dist/memory/consolidation/index.d.ts.map +1 -0
  22. package/dist/memory/consolidation/index.js +7 -0
  23. package/dist/memory/consolidation/index.js.map +1 -0
  24. package/dist/memory/decay/DecayModel.d.ts +33 -0
  25. package/dist/memory/decay/DecayModel.d.ts.map +1 -1
  26. package/dist/memory/decay/DecayModel.js +31 -0
  27. package/dist/memory/decay/DecayModel.js.map +1 -1
  28. package/dist/memory/facade/Memory.d.ts +228 -0
  29. package/dist/memory/facade/Memory.d.ts.map +1 -0
  30. package/dist/memory/facade/Memory.js +823 -0
  31. package/dist/memory/facade/Memory.js.map +1 -0
  32. package/dist/memory/facade/index.d.ts +13 -0
  33. package/dist/memory/facade/index.d.ts.map +1 -0
  34. package/dist/memory/facade/index.js +11 -0
  35. package/dist/memory/facade/index.js.map +1 -0
  36. package/dist/memory/facade/types.d.ts +606 -0
  37. package/dist/memory/facade/types.d.ts.map +1 -0
  38. package/dist/memory/facade/types.js +11 -0
  39. package/dist/memory/facade/types.js.map +1 -0
  40. package/dist/memory/feedback/RetrievalFeedbackSignal.d.ts +132 -0
  41. package/dist/memory/feedback/RetrievalFeedbackSignal.d.ts.map +1 -0
  42. package/dist/memory/feedback/RetrievalFeedbackSignal.js +178 -0
  43. package/dist/memory/feedback/RetrievalFeedbackSignal.js.map +1 -0
  44. package/dist/memory/feedback/index.d.ts +13 -0
  45. package/dist/memory/feedback/index.d.ts.map +1 -0
  46. package/dist/memory/feedback/index.js +12 -0
  47. package/dist/memory/feedback/index.js.map +1 -0
  48. package/dist/memory/index.d.ts +22 -0
  49. package/dist/memory/index.d.ts.map +1 -1
  50. package/dist/memory/index.js +24 -0
  51. package/dist/memory/index.js.map +1 -1
  52. package/dist/memory/ingestion/ChunkingEngine.d.ts +143 -0
  53. package/dist/memory/ingestion/ChunkingEngine.d.ts.map +1 -0
  54. package/dist/memory/ingestion/ChunkingEngine.js +508 -0
  55. package/dist/memory/ingestion/ChunkingEngine.js.map +1 -0
  56. package/dist/memory/ingestion/DoclingLoader.d.ts +44 -0
  57. package/dist/memory/ingestion/DoclingLoader.d.ts.map +1 -0
  58. package/dist/memory/ingestion/DoclingLoader.js +228 -0
  59. package/dist/memory/ingestion/DoclingLoader.js.map +1 -0
  60. package/dist/memory/ingestion/DocxLoader.d.ts +37 -0
  61. package/dist/memory/ingestion/DocxLoader.d.ts.map +1 -0
  62. package/dist/memory/ingestion/DocxLoader.js +111 -0
  63. package/dist/memory/ingestion/DocxLoader.js.map +1 -0
  64. package/dist/memory/ingestion/FolderScanner.d.ts +116 -0
  65. package/dist/memory/ingestion/FolderScanner.d.ts.map +1 -0
  66. package/dist/memory/ingestion/FolderScanner.js +127 -0
  67. package/dist/memory/ingestion/FolderScanner.js.map +1 -0
  68. package/dist/memory/ingestion/HtmlLoader.d.ts +49 -0
  69. package/dist/memory/ingestion/HtmlLoader.d.ts.map +1 -0
  70. package/dist/memory/ingestion/HtmlLoader.js +202 -0
  71. package/dist/memory/ingestion/HtmlLoader.js.map +1 -0
  72. package/dist/memory/ingestion/IDocumentLoader.d.ts +63 -0
  73. package/dist/memory/ingestion/IDocumentLoader.d.ts.map +1 -0
  74. package/dist/memory/ingestion/IDocumentLoader.js +11 -0
  75. package/dist/memory/ingestion/IDocumentLoader.js.map +1 -0
  76. package/dist/memory/ingestion/LoaderRegistry.d.ts +140 -0
  77. package/dist/memory/ingestion/LoaderRegistry.d.ts.map +1 -0
  78. package/dist/memory/ingestion/LoaderRegistry.js +229 -0
  79. package/dist/memory/ingestion/LoaderRegistry.js.map +1 -0
  80. package/dist/memory/ingestion/MarkdownLoader.d.ts +50 -0
  81. package/dist/memory/ingestion/MarkdownLoader.d.ts.map +1 -0
  82. package/dist/memory/ingestion/MarkdownLoader.js +169 -0
  83. package/dist/memory/ingestion/MarkdownLoader.js.map +1 -0
  84. package/dist/memory/ingestion/MultimodalAggregator.d.ts +88 -0
  85. package/dist/memory/ingestion/MultimodalAggregator.d.ts.map +1 -0
  86. package/dist/memory/ingestion/MultimodalAggregator.js +96 -0
  87. package/dist/memory/ingestion/MultimodalAggregator.js.map +1 -0
  88. package/dist/memory/ingestion/OcrPdfLoader.d.ts +41 -0
  89. package/dist/memory/ingestion/OcrPdfLoader.d.ts.map +1 -0
  90. package/dist/memory/ingestion/OcrPdfLoader.js +149 -0
  91. package/dist/memory/ingestion/OcrPdfLoader.js.map +1 -0
  92. package/dist/memory/ingestion/PdfLoader.d.ts +78 -0
  93. package/dist/memory/ingestion/PdfLoader.d.ts.map +1 -0
  94. package/dist/memory/ingestion/PdfLoader.js +179 -0
  95. package/dist/memory/ingestion/PdfLoader.js.map +1 -0
  96. package/dist/memory/ingestion/TextLoader.d.ts +66 -0
  97. package/dist/memory/ingestion/TextLoader.d.ts.map +1 -0
  98. package/dist/memory/ingestion/TextLoader.js +207 -0
  99. package/dist/memory/ingestion/TextLoader.js.map +1 -0
  100. package/dist/memory/ingestion/UrlLoader.d.ts +95 -0
  101. package/dist/memory/ingestion/UrlLoader.d.ts.map +1 -0
  102. package/dist/memory/ingestion/UrlLoader.js +174 -0
  103. package/dist/memory/ingestion/UrlLoader.js.map +1 -0
  104. package/dist/memory/io/ChatGptImporter.d.ts +85 -0
  105. package/dist/memory/io/ChatGptImporter.d.ts.map +1 -0
  106. package/dist/memory/io/ChatGptImporter.js +231 -0
  107. package/dist/memory/io/ChatGptImporter.js.map +1 -0
  108. package/dist/memory/io/JsonExporter.d.ts +67 -0
  109. package/dist/memory/io/JsonExporter.d.ts.map +1 -0
  110. package/dist/memory/io/JsonExporter.js +132 -0
  111. package/dist/memory/io/JsonExporter.js.map +1 -0
  112. package/dist/memory/io/JsonImporter.d.ts +84 -0
  113. package/dist/memory/io/JsonImporter.d.ts.map +1 -0
  114. package/dist/memory/io/JsonImporter.js +234 -0
  115. package/dist/memory/io/JsonImporter.js.map +1 -0
  116. package/dist/memory/io/MarkdownExporter.d.ts +95 -0
  117. package/dist/memory/io/MarkdownExporter.d.ts.map +1 -0
  118. package/dist/memory/io/MarkdownExporter.js +130 -0
  119. package/dist/memory/io/MarkdownExporter.js.map +1 -0
  120. package/dist/memory/io/MarkdownImporter.d.ts +84 -0
  121. package/dist/memory/io/MarkdownImporter.d.ts.map +1 -0
  122. package/dist/memory/io/MarkdownImporter.js +166 -0
  123. package/dist/memory/io/MarkdownImporter.js.map +1 -0
  124. package/dist/memory/io/ObsidianExporter.d.ts +80 -0
  125. package/dist/memory/io/ObsidianExporter.d.ts.map +1 -0
  126. package/dist/memory/io/ObsidianExporter.js +127 -0
  127. package/dist/memory/io/ObsidianExporter.js.map +1 -0
  128. package/dist/memory/io/ObsidianImporter.d.ts +93 -0
  129. package/dist/memory/io/ObsidianImporter.d.ts.map +1 -0
  130. package/dist/memory/io/ObsidianImporter.js +221 -0
  131. package/dist/memory/io/ObsidianImporter.js.map +1 -0
  132. package/dist/memory/io/SqliteExporter.d.ts +47 -0
  133. package/dist/memory/io/SqliteExporter.d.ts.map +1 -0
  134. package/dist/memory/io/SqliteExporter.js +56 -0
  135. package/dist/memory/io/SqliteExporter.js.map +1 -0
  136. package/dist/memory/io/SqliteImporter.d.ts +82 -0
  137. package/dist/memory/io/SqliteImporter.d.ts.map +1 -0
  138. package/dist/memory/io/SqliteImporter.js +232 -0
  139. package/dist/memory/io/SqliteImporter.js.map +1 -0
  140. package/dist/memory/io/index.d.ts +31 -0
  141. package/dist/memory/io/index.d.ts.map +1 -0
  142. package/dist/memory/io/index.js +31 -0
  143. package/dist/memory/io/index.js.map +1 -0
  144. package/dist/memory/store/SqliteBrain.d.ts +125 -0
  145. package/dist/memory/store/SqliteBrain.d.ts.map +1 -0
  146. package/dist/memory/store/SqliteBrain.js +407 -0
  147. package/dist/memory/store/SqliteBrain.js.map +1 -0
  148. package/dist/memory/store/SqliteKnowledgeGraph.d.ts +259 -0
  149. package/dist/memory/store/SqliteKnowledgeGraph.d.ts.map +1 -0
  150. package/dist/memory/store/SqliteKnowledgeGraph.js +1062 -0
  151. package/dist/memory/store/SqliteKnowledgeGraph.js.map +1 -0
  152. package/dist/memory/store/SqliteMemoryGraph.d.ts +251 -0
  153. package/dist/memory/store/SqliteMemoryGraph.d.ts.map +1 -0
  154. package/dist/memory/store/SqliteMemoryGraph.js +637 -0
  155. package/dist/memory/store/SqliteMemoryGraph.js.map +1 -0
  156. package/dist/memory/tools/MemoryAddTool.d.ts +98 -0
  157. package/dist/memory/tools/MemoryAddTool.d.ts.map +1 -0
  158. package/dist/memory/tools/MemoryAddTool.js +131 -0
  159. package/dist/memory/tools/MemoryAddTool.js.map +1 -0
  160. package/dist/memory/tools/MemoryDeleteTool.d.ts +83 -0
  161. package/dist/memory/tools/MemoryDeleteTool.d.ts.map +1 -0
  162. package/dist/memory/tools/MemoryDeleteTool.js +96 -0
  163. package/dist/memory/tools/MemoryDeleteTool.js.map +1 -0
  164. package/dist/memory/tools/MemoryMergeTool.d.ts +95 -0
  165. package/dist/memory/tools/MemoryMergeTool.d.ts.map +1 -0
  166. package/dist/memory/tools/MemoryMergeTool.js +164 -0
  167. package/dist/memory/tools/MemoryMergeTool.js.map +1 -0
  168. package/dist/memory/tools/MemoryReflectTool.d.ts +86 -0
  169. package/dist/memory/tools/MemoryReflectTool.d.ts.map +1 -0
  170. package/dist/memory/tools/MemoryReflectTool.js +102 -0
  171. package/dist/memory/tools/MemoryReflectTool.js.map +1 -0
  172. package/dist/memory/tools/MemorySearchTool.d.ts +117 -0
  173. package/dist/memory/tools/MemorySearchTool.d.ts.map +1 -0
  174. package/dist/memory/tools/MemorySearchTool.js +162 -0
  175. package/dist/memory/tools/MemorySearchTool.js.map +1 -0
  176. package/dist/memory/tools/MemoryUpdateTool.d.ts +92 -0
  177. package/dist/memory/tools/MemoryUpdateTool.d.ts.map +1 -0
  178. package/dist/memory/tools/MemoryUpdateTool.js +125 -0
  179. package/dist/memory/tools/MemoryUpdateTool.js.map +1 -0
  180. package/dist/memory/tools/index.d.ts +32 -0
  181. package/dist/memory/tools/index.d.ts.map +1 -0
  182. package/dist/memory/tools/index.js +26 -0
  183. package/dist/memory/tools/index.js.map +1 -0
  184. package/package.json +6 -1
@@ -0,0 +1,1062 @@
1
+ /**
2
+ * @fileoverview SQLite-backed implementation of IKnowledgeGraph.
3
+ *
4
+ * Persists knowledge entities (nodes), relations (edges), and episodic memories
5
+ * to the `knowledge_nodes` and `knowledge_edges` tables managed by SqliteBrain.
6
+ *
7
+ * Episodic memories are stored as knowledge_nodes with `type = 'memory'` and
8
+ * their memory-specific fields packed into the `properties` JSON column.
9
+ *
10
+ * Graph traversal (BFS/DFS, shortest path, neighbourhood) is implemented via
11
+ * SQLite recursive Common Table Expressions (CTEs).
12
+ *
13
+ * Semantic search loads embeddings from BLOB columns and computes cosine
14
+ * similarity in-process — no external vector DB needed for the SQLite path.
15
+ *
16
+ * @module memory/store/SqliteKnowledgeGraph
17
+ */
18
+ import crypto from 'node:crypto';
19
+ // ---------------------------------------------------------------------------
20
+ // Embedding helpers
21
+ // ---------------------------------------------------------------------------
22
+ /**
23
+ * Serialize a number[] embedding into a Buffer (Float32 little-endian)
24
+ * for storage in an SQLite BLOB column.
25
+ */
26
+ function embeddingToBlob(vec) {
27
+ const buf = Buffer.alloc(vec.length * 4);
28
+ for (let i = 0; i < vec.length; i++) {
29
+ buf.writeFloatLE(vec[i], i * 4);
30
+ }
31
+ return buf;
32
+ }
33
+ /**
34
+ * Deserialize a BLOB (Float32 little-endian) back into a number[] embedding.
35
+ */
36
+ function blobToEmbedding(blob) {
37
+ const count = blob.length / 4;
38
+ const vec = new Array(count);
39
+ for (let i = 0; i < count; i++) {
40
+ vec[i] = blob.readFloatLE(i * 4);
41
+ }
42
+ return vec;
43
+ }
44
+ /**
45
+ * Compute cosine similarity between two equal-length vectors.
46
+ * Returns 0 if either vector has zero magnitude.
47
+ */
48
+ function cosineSimilarity(a, b) {
49
+ let dot = 0;
50
+ let magA = 0;
51
+ let magB = 0;
52
+ for (let i = 0; i < a.length; i++) {
53
+ dot += a[i] * b[i];
54
+ magA += a[i] * a[i];
55
+ magB += b[i] * b[i];
56
+ }
57
+ const denom = Math.sqrt(magA) * Math.sqrt(magB);
58
+ return denom === 0 ? 0 : dot / denom;
59
+ }
60
+ // ---------------------------------------------------------------------------
61
+ // SqliteKnowledgeGraph
62
+ // ---------------------------------------------------------------------------
63
+ /**
64
+ * Persistent knowledge graph backed by SQLite via SqliteBrain.
65
+ *
66
+ * Implements the full `IKnowledgeGraph` interface using the `knowledge_nodes`
67
+ * and `knowledge_edges` tables. Extended entity/relation fields that don't
68
+ * have dedicated columns are serialized into the JSON `properties` / `metadata`
69
+ * columns.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * const brain = new SqliteBrain('/tmp/agent-brain.sqlite');
74
+ * const graph = new SqliteKnowledgeGraph(brain);
75
+ * await graph.initialize();
76
+ *
77
+ * const entity = await graph.upsertEntity({
78
+ * type: 'person',
79
+ * label: 'Alice',
80
+ * properties: { role: 'engineer' },
81
+ * confidence: 0.95,
82
+ * source: { type: 'user_input', timestamp: new Date().toISOString() },
83
+ * });
84
+ * ```
85
+ */
86
+ export class SqliteKnowledgeGraph {
87
+ /**
88
+ * @param brain - A SqliteBrain instance whose `db` handle is used for all queries.
89
+ */
90
+ constructor(brain) {
91
+ this.brain = brain;
92
+ }
93
+ // =========================================================================
94
+ // Initialization
95
+ // =========================================================================
96
+ /**
97
+ * Initialize the knowledge graph.
98
+ *
99
+ * The schema is already created by SqliteBrain's constructor, so this is
100
+ * effectively a no-op. Provided to satisfy the IKnowledgeGraph contract.
101
+ */
102
+ async initialize() {
103
+ // Schema already exists via SqliteBrain._initSchema().
104
+ // Nothing additional to do.
105
+ }
106
+ // =========================================================================
107
+ // Entity Operations
108
+ // =========================================================================
109
+ /**
110
+ * Insert or update a knowledge entity.
111
+ *
112
+ * If `entity.id` is provided and exists, the row is updated (INSERT OR REPLACE).
113
+ * If omitted, a new UUID is generated.
114
+ *
115
+ * Extended fields (ownerId, tags, metadata, updatedAt) are packed into the
116
+ * `properties` JSON column as underscore-prefixed keys to avoid collisions
117
+ * with user-supplied properties.
118
+ */
119
+ async upsertEntity(entity) {
120
+ const now = new Date().toISOString();
121
+ const id = entity.id ?? crypto.randomUUID();
122
+ // Check if the entity already exists to preserve createdAt.
123
+ const existing = this.brain.db
124
+ .prepare('SELECT * FROM knowledge_nodes WHERE id = ?')
125
+ .get(id);
126
+ const createdAt = existing
127
+ ? new Date(existing.created_at).toISOString()
128
+ : now;
129
+ // Pack extended fields into the properties JSON envelope.
130
+ const extended = {
131
+ _props: entity.properties,
132
+ _ownerId: entity.ownerId,
133
+ _tags: entity.tags,
134
+ _metadata: entity.metadata,
135
+ _updatedAt: now,
136
+ };
137
+ const embeddingBlob = entity.embedding
138
+ ? embeddingToBlob(entity.embedding)
139
+ : null;
140
+ this.brain.db
141
+ .prepare(`INSERT OR REPLACE INTO knowledge_nodes
142
+ (id, type, label, properties, embedding, confidence, source, created_at)
143
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
144
+ .run(id, entity.type, entity.label, JSON.stringify(extended), embeddingBlob, entity.confidence, JSON.stringify(entity.source), new Date(createdAt).getTime());
145
+ return this._rowToEntity({
146
+ id,
147
+ type: entity.type,
148
+ label: entity.label,
149
+ properties: JSON.stringify(extended),
150
+ embedding: embeddingBlob,
151
+ confidence: entity.confidence,
152
+ source: JSON.stringify(entity.source),
153
+ created_at: new Date(createdAt).getTime(),
154
+ });
155
+ }
156
+ /**
157
+ * Retrieve a single entity by its ID.
158
+ * Returns `undefined` if the entity does not exist.
159
+ */
160
+ async getEntity(id) {
161
+ const row = this.brain.db
162
+ .prepare('SELECT * FROM knowledge_nodes WHERE id = ?')
163
+ .get(id);
164
+ if (!row)
165
+ return undefined;
166
+ return this._rowToEntity(row);
167
+ }
168
+ /**
169
+ * Query entities with optional filters.
170
+ *
171
+ * Supports filtering by entity type, tags, owner, minimum confidence,
172
+ * full-text search, pagination (limit/offset), and time ranges.
173
+ */
174
+ async queryEntities(options) {
175
+ const clauses = [];
176
+ const params = [];
177
+ // Filter: entity types (only non-memory types for queryEntities).
178
+ if (options?.entityTypes && options.entityTypes.length > 0) {
179
+ const placeholders = options.entityTypes.map(() => '?').join(', ');
180
+ clauses.push(`type IN (${placeholders})`);
181
+ params.push(...options.entityTypes);
182
+ }
183
+ // Filter: minimum confidence.
184
+ if (options?.minConfidence !== undefined) {
185
+ clauses.push('confidence >= ?');
186
+ params.push(options.minConfidence);
187
+ }
188
+ // Filter: time range.
189
+ if (options?.timeRange?.from) {
190
+ clauses.push('created_at >= ?');
191
+ params.push(new Date(options.timeRange.from).getTime());
192
+ }
193
+ if (options?.timeRange?.to) {
194
+ clauses.push('created_at <= ?');
195
+ params.push(new Date(options.timeRange.to).getTime());
196
+ }
197
+ const whereClause = clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '';
198
+ const limit = options?.limit ?? 100;
199
+ const offset = options?.offset ?? 0;
200
+ const sql = `SELECT * FROM knowledge_nodes ${whereClause} ORDER BY created_at DESC LIMIT ? OFFSET ?`;
201
+ params.push(limit, offset);
202
+ const rows = this.brain.db.prepare(sql).all(...params);
203
+ let entities = rows.map((r) => this._rowToEntity(r));
204
+ // Post-filter by ownerId (stored inside properties JSON).
205
+ if (options?.ownerId) {
206
+ entities = entities.filter((e) => e.ownerId === options.ownerId);
207
+ }
208
+ // Post-filter by tags (stored inside properties JSON).
209
+ if (options?.tags && options.tags.length > 0) {
210
+ entities = entities.filter((e) => options.tags.some((tag) => e.tags?.includes(tag)));
211
+ }
212
+ // Post-filter by textSearch (label + properties text match, case-insensitive).
213
+ if (options?.textSearch) {
214
+ const search = options.textSearch.toLowerCase();
215
+ entities = entities.filter((e) => e.label.toLowerCase().includes(search) ||
216
+ JSON.stringify(e.properties).toLowerCase().includes(search));
217
+ }
218
+ return entities;
219
+ }
220
+ /**
221
+ * Delete an entity and all its associated relations (incoming and outgoing).
222
+ * Returns `true` if the entity existed and was deleted.
223
+ */
224
+ async deleteEntity(id) {
225
+ const deleteOp = this.brain.db.transaction(() => {
226
+ // Delete all edges touching this entity.
227
+ this.brain.db
228
+ .prepare('DELETE FROM knowledge_edges WHERE source_id = ? OR target_id = ?')
229
+ .run(id, id);
230
+ // Delete the node itself.
231
+ const result = this.brain.db
232
+ .prepare('DELETE FROM knowledge_nodes WHERE id = ?')
233
+ .run(id);
234
+ return result.changes > 0;
235
+ });
236
+ return deleteOp();
237
+ }
238
+ // =========================================================================
239
+ // Relation Operations
240
+ // =========================================================================
241
+ /**
242
+ * Insert or update a knowledge relation (edge).
243
+ *
244
+ * Extended edge fields (label, properties, confidence, source, validFrom,
245
+ * validTo) are packed into the `metadata` JSON column.
246
+ */
247
+ async upsertRelation(relation) {
248
+ const now = new Date().toISOString();
249
+ const id = relation.id ?? crypto.randomUUID();
250
+ // Preserve createdAt if edge already exists.
251
+ const existing = this.brain.db
252
+ .prepare('SELECT * FROM knowledge_edges WHERE id = ?')
253
+ .get(id);
254
+ const createdAt = existing
255
+ ? new Date(existing.created_at).toISOString()
256
+ : now;
257
+ // Pack extended edge fields into the metadata JSON column.
258
+ const extended = {
259
+ _label: relation.label,
260
+ _properties: relation.properties,
261
+ _confidence: relation.confidence,
262
+ _source: relation.source,
263
+ _validFrom: relation.validFrom,
264
+ _validTo: relation.validTo,
265
+ };
266
+ this.brain.db
267
+ .prepare(`INSERT OR REPLACE INTO knowledge_edges
268
+ (id, source_id, target_id, type, weight, bidirectional, metadata, created_at)
269
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
270
+ .run(id, relation.sourceId, relation.targetId, relation.type, relation.weight, relation.bidirectional ? 1 : 0, JSON.stringify(extended), new Date(createdAt).getTime());
271
+ return this._rowToRelation({
272
+ id,
273
+ source_id: relation.sourceId,
274
+ target_id: relation.targetId,
275
+ type: relation.type,
276
+ weight: relation.weight,
277
+ bidirectional: relation.bidirectional ? 1 : 0,
278
+ metadata: JSON.stringify(extended),
279
+ created_at: new Date(createdAt).getTime(),
280
+ });
281
+ }
282
+ /**
283
+ * Get all relations for a given entity.
284
+ *
285
+ * @param entityId - The entity whose relations to retrieve.
286
+ * @param options - Optional filters: direction ('outgoing'|'incoming'|'both'), types.
287
+ */
288
+ async getRelations(entityId, options) {
289
+ const direction = options?.direction ?? 'both';
290
+ const clauses = [];
291
+ const params = [];
292
+ if (direction === 'outgoing') {
293
+ clauses.push('(source_id = ?)');
294
+ params.push(entityId);
295
+ }
296
+ else if (direction === 'incoming') {
297
+ clauses.push('(target_id = ?)');
298
+ params.push(entityId);
299
+ }
300
+ else {
301
+ clauses.push('(source_id = ? OR target_id = ?)');
302
+ params.push(entityId, entityId);
303
+ }
304
+ if (options?.types && options.types.length > 0) {
305
+ const placeholders = options.types.map(() => '?').join(', ');
306
+ clauses.push(`type IN (${placeholders})`);
307
+ params.push(...options.types);
308
+ }
309
+ const sql = `SELECT * FROM knowledge_edges WHERE ${clauses.join(' AND ')}`;
310
+ const rows = this.brain.db.prepare(sql).all(...params);
311
+ return rows.map((r) => this._rowToRelation(r));
312
+ }
313
+ /**
314
+ * Delete a single relation by its ID.
315
+ * Returns `true` if the relation existed and was deleted.
316
+ */
317
+ async deleteRelation(id) {
318
+ const result = this.brain.db
319
+ .prepare('DELETE FROM knowledge_edges WHERE id = ?')
320
+ .run(id);
321
+ return result.changes > 0;
322
+ }
323
+ // =========================================================================
324
+ // Episodic Memory Operations
325
+ // =========================================================================
326
+ /**
327
+ * Record an episodic memory.
328
+ *
329
+ * Memories are stored as knowledge_nodes with `type = 'memory'`. The
330
+ * memory-specific fields are packed into the `properties` JSON column.
331
+ */
332
+ async recordMemory(memory) {
333
+ const now = new Date().toISOString();
334
+ const id = crypto.randomUUID();
335
+ const memFields = {
336
+ _props: {},
337
+ _updatedAt: now,
338
+ _memoryType: memory.type,
339
+ _summary: memory.summary,
340
+ _description: memory.description,
341
+ _participants: memory.participants,
342
+ _valence: memory.valence,
343
+ _importance: memory.importance,
344
+ _entityIds: memory.entityIds,
345
+ _occurredAt: memory.occurredAt,
346
+ _durationMs: memory.durationMs,
347
+ _outcome: memory.outcome,
348
+ _insights: memory.insights,
349
+ _context: memory.context,
350
+ _accessCount: 0,
351
+ _lastAccessedAt: now,
352
+ };
353
+ const embeddingBlob = memory.embedding
354
+ ? embeddingToBlob(memory.embedding)
355
+ : null;
356
+ const source = {
357
+ type: 'conversation',
358
+ timestamp: now,
359
+ };
360
+ this.brain.db
361
+ .prepare(`INSERT INTO knowledge_nodes
362
+ (id, type, label, properties, embedding, confidence, source, created_at)
363
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
364
+ .run(id, 'memory', memory.summary.slice(0, 200), JSON.stringify(memFields), embeddingBlob, memory.importance, JSON.stringify(source), new Date(now).getTime());
365
+ return {
366
+ id,
367
+ type: memory.type,
368
+ summary: memory.summary,
369
+ description: memory.description,
370
+ participants: memory.participants,
371
+ valence: memory.valence,
372
+ importance: memory.importance,
373
+ entityIds: memory.entityIds,
374
+ embedding: memory.embedding,
375
+ occurredAt: memory.occurredAt,
376
+ durationMs: memory.durationMs,
377
+ outcome: memory.outcome,
378
+ insights: memory.insights,
379
+ context: memory.context,
380
+ createdAt: now,
381
+ accessCount: 0,
382
+ lastAccessedAt: now,
383
+ };
384
+ }
385
+ /**
386
+ * Get an episodic memory by ID.
387
+ *
388
+ * Looks up the knowledge_node with the given ID and `type = 'memory'`,
389
+ * then unpacks the memory-specific fields from the `properties` JSON.
390
+ */
391
+ async getMemory(id) {
392
+ const row = this.brain.db
393
+ .prepare(`SELECT * FROM knowledge_nodes WHERE id = ? AND type = 'memory'`)
394
+ .get(id);
395
+ if (!row)
396
+ return undefined;
397
+ return this._rowToMemory(row);
398
+ }
399
+ /**
400
+ * Query episodic memories with optional filters.
401
+ *
402
+ * Supports filtering by memory sub-type, participants, minimum importance,
403
+ * time range, and result limit.
404
+ */
405
+ async queryMemories(options) {
406
+ const clauses = ["type = 'memory'"];
407
+ const params = [];
408
+ // Filter by minimum importance (stored as confidence).
409
+ if (options?.minImportance !== undefined) {
410
+ clauses.push('confidence >= ?');
411
+ params.push(options.minImportance);
412
+ }
413
+ // Filter by time range on created_at.
414
+ if (options?.timeRange?.from) {
415
+ clauses.push('created_at >= ?');
416
+ params.push(new Date(options.timeRange.from).getTime());
417
+ }
418
+ if (options?.timeRange?.to) {
419
+ clauses.push('created_at <= ?');
420
+ params.push(new Date(options.timeRange.to).getTime());
421
+ }
422
+ const limit = options?.limit ?? 100;
423
+ const sql = `SELECT * FROM knowledge_nodes WHERE ${clauses.join(' AND ')} ORDER BY created_at DESC LIMIT ?`;
424
+ params.push(limit);
425
+ const rows = this.brain.db.prepare(sql).all(...params);
426
+ let memories = rows.map((r) => this._rowToMemory(r));
427
+ // Post-filter by memory sub-type (stored in properties JSON).
428
+ if (options?.types && options.types.length > 0) {
429
+ const typeSet = new Set(options.types);
430
+ memories = memories.filter((m) => typeSet.has(m.type));
431
+ }
432
+ // Post-filter by participants (stored in properties JSON).
433
+ if (options?.participants && options.participants.length > 0) {
434
+ memories = memories.filter((m) => options.participants.some((p) => m.participants.includes(p)));
435
+ }
436
+ return memories;
437
+ }
438
+ /**
439
+ * Recall relevant memories via keyword search against summaries.
440
+ *
441
+ * Performs a case-insensitive substring match on the label (which contains
442
+ * the summary text). Increments accessCount and updates lastAccessedAt for
443
+ * each returned memory (Hebbian reinforcement).
444
+ *
445
+ * Full semantic (embedding-based) recall requires the Memory facade.
446
+ */
447
+ async recallMemories(query, topK) {
448
+ const limit = topK ?? 10;
449
+ const searchTerm = `%${query}%`;
450
+ const rows = this.brain.db
451
+ .prepare(`SELECT * FROM knowledge_nodes
452
+ WHERE type = 'memory' AND (label LIKE ? OR properties LIKE ?)
453
+ ORDER BY created_at DESC
454
+ LIMIT ?`)
455
+ .all(searchTerm, searchTerm, limit);
456
+ const memories = rows.map((r) => this._rowToMemory(r));
457
+ // Update access count and last accessed for each recalled memory.
458
+ const now = new Date().toISOString();
459
+ for (const mem of memories) {
460
+ const row = this.brain.db
461
+ .prepare('SELECT * FROM knowledge_nodes WHERE id = ?')
462
+ .get(mem.id);
463
+ if (row) {
464
+ const fields = JSON.parse(row.properties);
465
+ fields._accessCount = (fields._accessCount ?? 0) + 1;
466
+ fields._lastAccessedAt = now;
467
+ this.brain.db
468
+ .prepare('UPDATE knowledge_nodes SET properties = ? WHERE id = ?')
469
+ .run(JSON.stringify(fields), mem.id);
470
+ mem.accessCount = fields._accessCount;
471
+ mem.lastAccessedAt = now;
472
+ }
473
+ }
474
+ return memories;
475
+ }
476
+ // =========================================================================
477
+ // Graph Traversal
478
+ // =========================================================================
479
+ /**
480
+ * Traverse the graph from a starting entity using BFS.
481
+ *
482
+ * Uses a recursive CTE (Common Table Expression) to walk the graph up to
483
+ * `maxDepth` hops from the start node. Results are grouped by depth level.
484
+ *
485
+ * @param startEntityId - ID of the entity to start traversal from.
486
+ * @param options - Optional: maxDepth, relationTypes, direction, minWeight, maxNodes.
487
+ */
488
+ async traverse(startEntityId, options) {
489
+ const root = await this.getEntity(startEntityId);
490
+ if (!root) {
491
+ throw new Error(`Entity not found: ${startEntityId}`);
492
+ }
493
+ const maxDepth = options?.maxDepth ?? 3;
494
+ const direction = options?.direction ?? 'both';
495
+ const minWeight = options?.minWeight ?? 0;
496
+ const maxNodes = options?.maxNodes ?? 1000;
497
+ // Build the edge direction clause for the recursive step.
498
+ let edgeJoin;
499
+ if (direction === 'outgoing') {
500
+ edgeJoin = 'knowledge_edges e ON e.source_id = t.entity_id';
501
+ }
502
+ else if (direction === 'incoming') {
503
+ edgeJoin = 'knowledge_edges e ON e.target_id = t.entity_id';
504
+ }
505
+ else {
506
+ edgeJoin = 'knowledge_edges e ON (e.source_id = t.entity_id OR e.target_id = t.entity_id)';
507
+ }
508
+ // Build optional relation type filter.
509
+ let typeFilter = '';
510
+ const typeParams = [];
511
+ if (options?.relationTypes && options.relationTypes.length > 0) {
512
+ const placeholders = options.relationTypes.map(() => '?').join(', ');
513
+ typeFilter = `AND e.type IN (${placeholders})`;
514
+ typeParams.push(...options.relationTypes);
515
+ }
516
+ // Next entity ID expression depends on direction.
517
+ let nextEntityExpr;
518
+ if (direction === 'outgoing') {
519
+ nextEntityExpr = 'e.target_id';
520
+ }
521
+ else if (direction === 'incoming') {
522
+ nextEntityExpr = 'e.source_id';
523
+ }
524
+ else {
525
+ nextEntityExpr = "CASE WHEN e.source_id = t.entity_id THEN e.target_id ELSE e.source_id END";
526
+ }
527
+ const sql = `
528
+ WITH RECURSIVE traverse AS (
529
+ SELECT
530
+ ? AS entity_id,
531
+ 0 AS depth,
532
+ CAST(? AS TEXT) AS edge_id
533
+ UNION ALL
534
+ SELECT
535
+ ${nextEntityExpr},
536
+ t.depth + 1,
537
+ e.id
538
+ FROM traverse t
539
+ JOIN ${edgeJoin}
540
+ WHERE t.depth < ?
541
+ AND e.weight >= ?
542
+ ${typeFilter}
543
+ AND ${nextEntityExpr} != t.entity_id
544
+ )
545
+ SELECT DISTINCT entity_id, depth, edge_id
546
+ FROM traverse
547
+ LIMIT ?
548
+ `;
549
+ const params = [
550
+ startEntityId,
551
+ '', // placeholder edge_id for root
552
+ maxDepth,
553
+ minWeight,
554
+ ...typeParams,
555
+ maxNodes,
556
+ ];
557
+ const traversalRows = this.brain.db.prepare(sql).all(...params);
558
+ // Group by depth. Collect unique entity IDs per level.
559
+ const levelMap = new Map();
560
+ const visitedEntities = new Set();
561
+ for (const row of traversalRows) {
562
+ if (visitedEntities.has(row.entity_id))
563
+ continue;
564
+ visitedEntities.add(row.entity_id);
565
+ if (!levelMap.has(row.depth)) {
566
+ levelMap.set(row.depth, { entityIds: new Set(), edgeIds: new Set() });
567
+ }
568
+ const level = levelMap.get(row.depth);
569
+ level.entityIds.add(row.entity_id);
570
+ if (row.edge_id) {
571
+ level.edgeIds.add(row.edge_id);
572
+ }
573
+ }
574
+ // Resolve entities and relations for each level.
575
+ const levels = [];
576
+ let totalEntities = 0;
577
+ let totalRelations = 0;
578
+ for (const [depth, data] of [...levelMap.entries()].sort((a, b) => a[0] - b[0])) {
579
+ const entities = [];
580
+ for (const eid of data.entityIds) {
581
+ const entity = await this.getEntity(eid);
582
+ if (entity)
583
+ entities.push(entity);
584
+ }
585
+ const relations = [];
586
+ for (const rid of data.edgeIds) {
587
+ const row = this.brain.db
588
+ .prepare('SELECT * FROM knowledge_edges WHERE id = ?')
589
+ .get(rid);
590
+ if (row)
591
+ relations.push(this._rowToRelation(row));
592
+ }
593
+ levels.push({ depth, entities, relations });
594
+ totalEntities += entities.length;
595
+ totalRelations += relations.length;
596
+ }
597
+ return {
598
+ root,
599
+ levels,
600
+ totalEntities,
601
+ totalRelations,
602
+ };
603
+ }
604
+ /**
605
+ * Find the shortest path between two entities using a bidirectional BFS
606
+ * implemented via a recursive CTE.
607
+ *
608
+ * Returns an ordered array of `{ entity, relation? }` steps from source to
609
+ * target, or `null` if no path exists within `maxDepth` hops.
610
+ */
611
+ async findPath(sourceId, targetId, maxDepth) {
612
+ const depth = maxDepth ?? 10;
613
+ // Use a recursive CTE that tracks the path as a comma-separated list of
614
+ // "entityId:edgeId" pairs. Stops when targetId is reached.
615
+ const sql = `
616
+ WITH RECURSIVE path_search AS (
617
+ SELECT
618
+ ? AS current_id,
619
+ CAST(? AS TEXT) AS path_entities,
620
+ CAST('' AS TEXT) AS path_edges,
621
+ 0 AS depth
622
+ UNION ALL
623
+ SELECT
624
+ CASE
625
+ WHEN e.source_id = p.current_id THEN e.target_id
626
+ ELSE e.source_id
627
+ END,
628
+ p.path_entities || ',' ||
629
+ CASE WHEN e.source_id = p.current_id THEN e.target_id ELSE e.source_id END,
630
+ CASE WHEN p.path_edges = '' THEN e.id ELSE p.path_edges || ',' || e.id END,
631
+ p.depth + 1
632
+ FROM path_search p
633
+ JOIN knowledge_edges e ON (e.source_id = p.current_id OR e.target_id = p.current_id)
634
+ WHERE p.depth < ?
635
+ AND p.path_entities NOT LIKE '%' ||
636
+ CASE WHEN e.source_id = p.current_id THEN e.target_id ELSE e.source_id END || '%'
637
+ )
638
+ SELECT path_entities, path_edges, depth
639
+ FROM path_search
640
+ WHERE current_id = ?
641
+ ORDER BY depth ASC
642
+ LIMIT 1
643
+ `;
644
+ const row = this.brain.db.prepare(sql).get(sourceId, sourceId, depth, targetId);
645
+ if (!row)
646
+ return null;
647
+ const entityIds = row.path_entities.split(',').filter(Boolean);
648
+ const edgeIds = row.path_edges.split(',').filter(Boolean);
649
+ const result = [];
650
+ for (let i = 0; i < entityIds.length; i++) {
651
+ const entity = await this.getEntity(entityIds[i]);
652
+ if (!entity)
653
+ return null;
654
+ let relation;
655
+ if (i > 0 && edgeIds[i - 1]) {
656
+ const edgeRow = this.brain.db
657
+ .prepare('SELECT * FROM knowledge_edges WHERE id = ?')
658
+ .get(edgeIds[i - 1]);
659
+ if (edgeRow)
660
+ relation = this._rowToRelation(edgeRow);
661
+ }
662
+ result.push({ entity, relation });
663
+ }
664
+ return result;
665
+ }
666
+ /**
667
+ * Get the neighbourhood of an entity — all entities and relations within
668
+ * `depth` hops.
669
+ *
670
+ * @param entityId - Centre entity.
671
+ * @param depth - Maximum number of hops (default 1).
672
+ */
673
+ async getNeighborhood(entityId, depth) {
674
+ const maxDepth = depth ?? 1;
675
+ const sql = `
676
+ WITH RECURSIVE neighborhood AS (
677
+ SELECT ? AS entity_id, 0 AS depth
678
+ UNION ALL
679
+ SELECT
680
+ CASE
681
+ WHEN e.source_id = n.entity_id THEN e.target_id
682
+ ELSE e.source_id
683
+ END,
684
+ n.depth + 1
685
+ FROM neighborhood n
686
+ JOIN knowledge_edges e ON (e.source_id = n.entity_id OR e.target_id = n.entity_id)
687
+ WHERE n.depth < ?
688
+ )
689
+ SELECT DISTINCT entity_id FROM neighborhood
690
+ `;
691
+ const rows = this.brain.db.prepare(sql).all(entityId, maxDepth);
692
+ const entityIds = new Set(rows.map((r) => r.entity_id));
693
+ const entities = [];
694
+ for (const eid of entityIds) {
695
+ const entity = await this.getEntity(eid);
696
+ if (entity)
697
+ entities.push(entity);
698
+ }
699
+ // Collect all edges where both endpoints are in the neighbourhood.
700
+ const allEdges = this.brain.db
701
+ .prepare('SELECT * FROM knowledge_edges')
702
+ .all();
703
+ const relations = allEdges
704
+ .filter((e) => entityIds.has(e.source_id) && entityIds.has(e.target_id))
705
+ .map((r) => this._rowToRelation(r));
706
+ return { entities, relations };
707
+ }
708
+ // =========================================================================
709
+ // Semantic Search
710
+ // =========================================================================
711
+ /**
712
+ * Semantic search across entities and/or memories.
713
+ *
714
+ * Loads all embeddings from `knowledge_nodes`, computes cosine similarity
715
+ * against the query embedding (if present in `options`), and returns the
716
+ * top-K results above the minimum similarity threshold.
717
+ *
718
+ * NOTE: This implementation requires the caller to provide a query embedding
719
+ * via a pre-processing step. If no nodes have embeddings, an empty array is
720
+ * returned. For full text-to-embedding semantic search, use the Memory facade.
721
+ */
722
+ async semanticSearch(options) {
723
+ const scope = options.scope ?? 'all';
724
+ const topK = options.topK ?? 10;
725
+ const minSimilarity = options.minSimilarity ?? 0;
726
+ // Build WHERE clause based on scope.
727
+ const clauses = ['embedding IS NOT NULL'];
728
+ if (scope === 'entities') {
729
+ clauses.push("type != 'memory'");
730
+ }
731
+ else if (scope === 'memories') {
732
+ clauses.push("type = 'memory'");
733
+ }
734
+ if (options.entityTypes && options.entityTypes.length > 0) {
735
+ const placeholders = options.entityTypes.map(() => '?').join(', ');
736
+ clauses.push(`type IN (${placeholders})`);
737
+ }
738
+ const sql = `SELECT * FROM knowledge_nodes WHERE ${clauses.join(' AND ')}`;
739
+ const params = [];
740
+ if (options.entityTypes && options.entityTypes.length > 0) {
741
+ params.push(...options.entityTypes);
742
+ }
743
+ const rows = this.brain.db.prepare(sql).all(...params);
744
+ // We need a query embedding. For keyword-based fallback, we match by text.
745
+ // Here we do keyword search since we don't have an embedding model.
746
+ const queryLower = options.query.toLowerCase();
747
+ const results = [];
748
+ for (const row of rows) {
749
+ // If the row has an embedding, we'd need a query embedding for cosine sim.
750
+ // As a fallback, do keyword matching and assign a pseudo-similarity.
751
+ const label = row.label.toLowerCase();
752
+ const propsStr = row.properties.toLowerCase();
753
+ let similarity = 0;
754
+ if (label.includes(queryLower)) {
755
+ similarity = 0.9; // Strong match on label.
756
+ }
757
+ else if (propsStr.includes(queryLower)) {
758
+ similarity = 0.7; // Weaker match in properties.
759
+ }
760
+ if (similarity < minSimilarity)
761
+ continue;
762
+ const isMemory = row.type === 'memory';
763
+ // Post-filter by ownerId if specified.
764
+ if (options.ownerId) {
765
+ const entity = this._rowToEntity(row);
766
+ if (entity.ownerId !== options.ownerId)
767
+ continue;
768
+ }
769
+ if (isMemory) {
770
+ results.push({
771
+ item: this._rowToMemory(row),
772
+ type: 'memory',
773
+ similarity,
774
+ });
775
+ }
776
+ else {
777
+ results.push({
778
+ item: this._rowToEntity(row),
779
+ type: 'entity',
780
+ similarity,
781
+ });
782
+ }
783
+ }
784
+ // Also search nodes WITHOUT embeddings by keyword when no embedding vectors
785
+ // are available (pure keyword fallback).
786
+ if (results.length === 0) {
787
+ const allNodesSql = `SELECT * FROM knowledge_nodes WHERE ${scope === 'entities' ? "type != 'memory'" :
788
+ scope === 'memories' ? "type = 'memory'" : '1=1'}`;
789
+ const allRows = this.brain.db.prepare(allNodesSql).all();
790
+ for (const row of allRows) {
791
+ const label = row.label.toLowerCase();
792
+ const propsStr = row.properties.toLowerCase();
793
+ let similarity = 0;
794
+ if (label.includes(queryLower)) {
795
+ similarity = 0.9;
796
+ }
797
+ else if (propsStr.includes(queryLower)) {
798
+ similarity = 0.7;
799
+ }
800
+ if (similarity < minSimilarity)
801
+ continue;
802
+ if (options.ownerId) {
803
+ const entity = this._rowToEntity(row);
804
+ if (entity.ownerId !== options.ownerId)
805
+ continue;
806
+ }
807
+ const isMemory = row.type === 'memory';
808
+ results.push({
809
+ item: isMemory ? this._rowToMemory(row) : this._rowToEntity(row),
810
+ type: isMemory ? 'memory' : 'entity',
811
+ similarity,
812
+ });
813
+ }
814
+ }
815
+ // Sort by similarity descending, take top-K.
816
+ results.sort((a, b) => b.similarity - a.similarity);
817
+ return results.slice(0, topK);
818
+ }
819
+ // =========================================================================
820
+ // Knowledge Extraction
821
+ // =========================================================================
822
+ /**
823
+ * Extract entities and relations from text.
824
+ *
825
+ * This operation requires an LLM and is not supported at the store level.
826
+ * Use the Memory facade for LLM-powered extraction.
827
+ *
828
+ * @throws {Error} Always — extraction requires an LLM.
829
+ */
830
+ async extractFromText(_text, _options) {
831
+ throw new Error('extractFromText requires an LLM — use the Memory facade for LLM-powered extraction');
832
+ }
833
+ // =========================================================================
834
+ // Maintenance
835
+ // =========================================================================
836
+ /**
837
+ * Merge multiple entities into one primary entity.
838
+ *
839
+ * All relations (edges) pointing to or from the non-primary entities are
840
+ * re-linked to the primary entity. The non-primary entities are then deleted.
841
+ *
842
+ * @param entityIds - All entity IDs involved in the merge.
843
+ * @param primaryId - The ID that survives the merge.
844
+ */
845
+ async mergeEntities(entityIds, primaryId) {
846
+ const primary = await this.getEntity(primaryId);
847
+ if (!primary) {
848
+ throw new Error(`Primary entity not found: ${primaryId}`);
849
+ }
850
+ const othersIds = entityIds.filter((id) => id !== primaryId);
851
+ const mergeTx = this.brain.db.transaction(() => {
852
+ for (const otherId of othersIds) {
853
+ // Re-link outgoing edges: source_id = otherId → primaryId.
854
+ this.brain.db
855
+ .prepare('UPDATE knowledge_edges SET source_id = ? WHERE source_id = ?')
856
+ .run(primaryId, otherId);
857
+ // Re-link incoming edges: target_id = otherId → primaryId.
858
+ this.brain.db
859
+ .prepare('UPDATE knowledge_edges SET target_id = ? WHERE target_id = ?')
860
+ .run(primaryId, otherId);
861
+ // Delete the non-primary entity node.
862
+ this.brain.db
863
+ .prepare('DELETE FROM knowledge_nodes WHERE id = ?')
864
+ .run(otherId);
865
+ }
866
+ // Remove any self-referential edges that may have resulted from the merge.
867
+ this.brain.db
868
+ .prepare('DELETE FROM knowledge_edges WHERE source_id = target_id')
869
+ .run();
870
+ });
871
+ mergeTx();
872
+ // Return the surviving primary entity (re-fetch to reflect current state).
873
+ return (await this.getEntity(primaryId));
874
+ }
875
+ /**
876
+ * Decay the confidence of all memory-type nodes by a multiplicative factor.
877
+ *
878
+ * This simulates the Ebbinghaus forgetting curve — memories that are not
879
+ * accessed (reinforced) gradually fade.
880
+ *
881
+ * @param decayFactor - Multiplicative factor in (0, 1). Default 0.95.
882
+ * @returns The number of memory nodes whose confidence was reduced.
883
+ */
884
+ async decayMemories(decayFactor) {
885
+ const factor = decayFactor ?? 0.95;
886
+ const result = this.brain.db
887
+ .prepare(`UPDATE knowledge_nodes
888
+ SET confidence = confidence * ?
889
+ WHERE type = 'memory'`)
890
+ .run(factor);
891
+ return result.changes;
892
+ }
893
+ /**
894
+ * Get aggregate statistics about the knowledge graph.
895
+ *
896
+ * Returns counts of entities, relations, memories, breakdowns by type,
897
+ * average confidence, and oldest/newest entry timestamps.
898
+ */
899
+ async getStats() {
900
+ // Total non-memory entities.
901
+ const entityCount = (this.brain.db
902
+ .prepare("SELECT COUNT(*) AS cnt FROM knowledge_nodes WHERE type != 'memory'")
903
+ .get())?.cnt ?? 0;
904
+ // Total relations.
905
+ const relationCount = (this.brain.db
906
+ .prepare('SELECT COUNT(*) AS cnt FROM knowledge_edges')
907
+ .get())?.cnt ?? 0;
908
+ // Total memories.
909
+ const memoryCount = (this.brain.db
910
+ .prepare("SELECT COUNT(*) AS cnt FROM knowledge_nodes WHERE type = 'memory'")
911
+ .get())?.cnt ?? 0;
912
+ // Entity types breakdown (exclude memories).
913
+ const entityTypeRows = this.brain.db
914
+ .prepare("SELECT type, COUNT(*) AS cnt FROM knowledge_nodes WHERE type != 'memory' GROUP BY type")
915
+ .all();
916
+ const entitiesByType = {};
917
+ for (const row of entityTypeRows) {
918
+ entitiesByType[row.type] = row.cnt;
919
+ }
920
+ // Relation types breakdown.
921
+ const relTypeRows = this.brain.db
922
+ .prepare('SELECT type, COUNT(*) AS cnt FROM knowledge_edges GROUP BY type')
923
+ .all();
924
+ const relationsByType = {};
925
+ for (const row of relTypeRows) {
926
+ relationsByType[row.type] = row.cnt;
927
+ }
928
+ // Average confidence across all nodes.
929
+ const avgConf = (this.brain.db
930
+ .prepare('SELECT AVG(confidence) AS avg_conf FROM knowledge_nodes')
931
+ .get())?.avg_conf ?? 0;
932
+ // Oldest and newest entries.
933
+ const oldest = (this.brain.db
934
+ .prepare('SELECT created_at FROM knowledge_nodes ORDER BY created_at ASC LIMIT 1')
935
+ .get());
936
+ const newest = (this.brain.db
937
+ .prepare('SELECT created_at FROM knowledge_nodes ORDER BY created_at DESC LIMIT 1')
938
+ .get());
939
+ return {
940
+ totalEntities: entityCount,
941
+ totalRelations: relationCount,
942
+ totalMemories: memoryCount,
943
+ entitiesByType,
944
+ relationsByType,
945
+ avgConfidence: avgConf,
946
+ oldestEntry: oldest ? new Date(oldest.created_at).toISOString() : '',
947
+ newestEntry: newest ? new Date(newest.created_at).toISOString() : '',
948
+ };
949
+ }
950
+ /**
951
+ * Delete all rows from knowledge_nodes and knowledge_edges.
952
+ * Wipes the knowledge graph completely.
953
+ */
954
+ async clear() {
955
+ const clearTx = this.brain.db.transaction(() => {
956
+ // Edges first (FK constraint on source_id / target_id → knowledge_nodes).
957
+ this.brain.db.exec('DELETE FROM knowledge_edges');
958
+ this.brain.db.exec('DELETE FROM knowledge_nodes');
959
+ });
960
+ clearTx();
961
+ }
962
+ // =========================================================================
963
+ // Private: Row ↔ Domain Object Converters
964
+ // =========================================================================
965
+ /**
966
+ * Convert a raw `knowledge_nodes` row into a `KnowledgeEntity` domain object.
967
+ * Unpacks extended fields from the `properties` JSON column.
968
+ */
969
+ _rowToEntity(row) {
970
+ let extended;
971
+ try {
972
+ extended = JSON.parse(row.properties);
973
+ }
974
+ catch {
975
+ extended = { _props: {} };
976
+ }
977
+ const source = (() => {
978
+ try {
979
+ return JSON.parse(row.source);
980
+ }
981
+ catch {
982
+ return { type: 'system', timestamp: new Date(row.created_at).toISOString() };
983
+ }
984
+ })();
985
+ return {
986
+ id: row.id,
987
+ type: row.type,
988
+ label: row.label,
989
+ properties: extended._props ?? {},
990
+ embedding: row.embedding ? blobToEmbedding(row.embedding) : undefined,
991
+ confidence: row.confidence,
992
+ source,
993
+ createdAt: new Date(row.created_at).toISOString(),
994
+ updatedAt: extended._updatedAt ?? new Date(row.created_at).toISOString(),
995
+ ownerId: extended._ownerId,
996
+ tags: extended._tags,
997
+ metadata: extended._metadata,
998
+ };
999
+ }
1000
+ /**
1001
+ * Convert a raw `knowledge_edges` row into a `KnowledgeRelation` domain object.
1002
+ * Unpacks extended fields from the `metadata` JSON column.
1003
+ */
1004
+ _rowToRelation(row) {
1005
+ let extended;
1006
+ try {
1007
+ extended = JSON.parse(row.metadata);
1008
+ }
1009
+ catch {
1010
+ extended = {};
1011
+ }
1012
+ return {
1013
+ id: row.id,
1014
+ sourceId: row.source_id,
1015
+ targetId: row.target_id,
1016
+ type: row.type,
1017
+ label: extended._label ?? row.type,
1018
+ properties: extended._properties,
1019
+ weight: row.weight,
1020
+ bidirectional: row.bidirectional === 1,
1021
+ confidence: extended._confidence ?? 1,
1022
+ source: extended._source ?? { type: 'system', timestamp: new Date(row.created_at).toISOString() },
1023
+ createdAt: new Date(row.created_at).toISOString(),
1024
+ validFrom: extended._validFrom,
1025
+ validTo: extended._validTo,
1026
+ };
1027
+ }
1028
+ /**
1029
+ * Convert a raw `knowledge_nodes` row (with `type = 'memory'`) into an
1030
+ * `EpisodicMemory` domain object. Unpacks memory-specific fields from the
1031
+ * `properties` JSON column.
1032
+ */
1033
+ _rowToMemory(row) {
1034
+ let fields;
1035
+ try {
1036
+ fields = JSON.parse(row.properties);
1037
+ }
1038
+ catch {
1039
+ fields = {};
1040
+ }
1041
+ return {
1042
+ id: row.id,
1043
+ type: (fields._memoryType ?? 'interaction'),
1044
+ summary: fields._summary ?? row.label,
1045
+ description: fields._description,
1046
+ participants: fields._participants ?? [],
1047
+ valence: fields._valence,
1048
+ importance: row.confidence,
1049
+ entityIds: fields._entityIds ?? [],
1050
+ embedding: row.embedding ? blobToEmbedding(row.embedding) : undefined,
1051
+ occurredAt: fields._occurredAt ?? new Date(row.created_at).toISOString(),
1052
+ durationMs: fields._durationMs,
1053
+ outcome: fields._outcome,
1054
+ insights: fields._insights,
1055
+ context: fields._context,
1056
+ createdAt: new Date(row.created_at).toISOString(),
1057
+ accessCount: fields._accessCount ?? 0,
1058
+ lastAccessedAt: fields._lastAccessedAt ?? new Date(row.created_at).toISOString(),
1059
+ };
1060
+ }
1061
+ }
1062
+ //# sourceMappingURL=SqliteKnowledgeGraph.js.map