@smyslenny/agent-memory 2.0.0 → 2.2.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/.github/workflows/test.yml +22 -0
- package/CHANGELOG.md +20 -0
- package/README.md +46 -6
- package/README.zh-CN.md +6 -6
- package/dist/bin/agent-memory.js +1118 -301
- package/dist/bin/agent-memory.js.map +1 -1
- package/dist/db-DsY3zz8f.d.ts +16 -0
- package/dist/index.d.ts +148 -18
- package/dist/index.js +968 -130
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +940 -181
- package/dist/mcp/server.js.map +1 -1
- package/docs/design/0004-agent-memory-integration.md +316 -0
- package/docs/design/0005-reranker-api-integration.md +276 -0
- package/docs/design/0006-multi-provider-embedding.md +196 -0
- package/docs/roadmap/integration-plan-v1.md +139 -0
- package/docs/roadmap/memory-architecture.md +168 -0
- package/docs/roadmap/warm-boot.md +135 -0
- package/package.json +3 -1
- package/dist/db-CMsKtBt0.d.ts +0 -9
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/core/db.ts
|
|
4
4
|
import Database from "better-sqlite3";
|
|
5
5
|
import { randomUUID } from "crypto";
|
|
6
|
-
var SCHEMA_VERSION =
|
|
6
|
+
var SCHEMA_VERSION = 3;
|
|
7
7
|
var SCHEMA_SQL = `
|
|
8
8
|
-- Memory entries
|
|
9
9
|
CREATE TABLE IF NOT EXISTS memories (
|
|
@@ -28,20 +28,23 @@ CREATE TABLE IF NOT EXISTS memories (
|
|
|
28
28
|
CREATE TABLE IF NOT EXISTS paths (
|
|
29
29
|
id TEXT PRIMARY KEY,
|
|
30
30
|
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
31
|
-
|
|
31
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
32
|
+
uri TEXT NOT NULL,
|
|
32
33
|
alias TEXT,
|
|
33
34
|
domain TEXT NOT NULL,
|
|
34
|
-
created_at TEXT NOT NULL
|
|
35
|
+
created_at TEXT NOT NULL,
|
|
36
|
+
UNIQUE(agent_id, uri)
|
|
35
37
|
);
|
|
36
38
|
|
|
37
39
|
-- Association network (knowledge graph)
|
|
38
40
|
CREATE TABLE IF NOT EXISTS links (
|
|
41
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
39
42
|
source_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
40
43
|
target_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
41
44
|
relation TEXT NOT NULL,
|
|
42
45
|
weight REAL NOT NULL DEFAULT 1.0,
|
|
43
46
|
created_at TEXT NOT NULL,
|
|
44
|
-
PRIMARY KEY (source_id, target_id)
|
|
47
|
+
PRIMARY KEY (agent_id, source_id, target_id)
|
|
45
48
|
);
|
|
46
49
|
|
|
47
50
|
-- Snapshots (version control, from nocturne + Memory Palace)
|
|
@@ -61,6 +64,18 @@ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
|
61
64
|
tokenize='unicode61'
|
|
62
65
|
);
|
|
63
66
|
|
|
67
|
+
-- Embeddings (optional semantic layer)
|
|
68
|
+
CREATE TABLE IF NOT EXISTS embeddings (
|
|
69
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
70
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
71
|
+
model TEXT NOT NULL,
|
|
72
|
+
dim INTEGER NOT NULL,
|
|
73
|
+
vector BLOB NOT NULL,
|
|
74
|
+
created_at TEXT NOT NULL,
|
|
75
|
+
updated_at TEXT NOT NULL,
|
|
76
|
+
PRIMARY KEY (agent_id, memory_id, model)
|
|
77
|
+
);
|
|
78
|
+
|
|
64
79
|
-- Schema version tracking
|
|
65
80
|
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
66
81
|
key TEXT PRIMARY KEY,
|
|
@@ -75,9 +90,10 @@ CREATE INDEX IF NOT EXISTS idx_memories_vitality ON memories(vitality);
|
|
|
75
90
|
CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(hash);
|
|
76
91
|
CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
|
|
77
92
|
CREATE INDEX IF NOT EXISTS idx_paths_domain ON paths(domain);
|
|
78
|
-
CREATE INDEX IF NOT EXISTS idx_links_source ON links(source_id);
|
|
79
|
-
CREATE INDEX IF NOT EXISTS idx_links_target ON links(target_id);
|
|
80
93
|
`;
|
|
94
|
+
function isCountRow(row) {
|
|
95
|
+
return row !== null && typeof row === "object" && "c" in row && typeof row.c === "number";
|
|
96
|
+
}
|
|
81
97
|
function openDatabase(opts) {
|
|
82
98
|
const db = new Database(opts.path);
|
|
83
99
|
if (opts.walMode !== false) {
|
|
@@ -86,13 +102,17 @@ function openDatabase(opts) {
|
|
|
86
102
|
db.pragma("foreign_keys = ON");
|
|
87
103
|
db.pragma("busy_timeout = 5000");
|
|
88
104
|
db.exec(SCHEMA_SQL);
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
105
|
+
const currentVersion = getSchemaVersion(db);
|
|
106
|
+
if (currentVersion === null) {
|
|
107
|
+
const inferred = inferSchemaVersion(db);
|
|
108
|
+
if (inferred < SCHEMA_VERSION) {
|
|
109
|
+
migrateDatabase(db, inferred, SCHEMA_VERSION);
|
|
110
|
+
}
|
|
111
|
+
db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(SCHEMA_VERSION));
|
|
112
|
+
} else if (currentVersion < SCHEMA_VERSION) {
|
|
113
|
+
migrateDatabase(db, currentVersion, SCHEMA_VERSION);
|
|
95
114
|
}
|
|
115
|
+
ensureIndexes(db);
|
|
96
116
|
return db;
|
|
97
117
|
}
|
|
98
118
|
function now() {
|
|
@@ -101,9 +121,256 @@ function now() {
|
|
|
101
121
|
function newId() {
|
|
102
122
|
return randomUUID();
|
|
103
123
|
}
|
|
124
|
+
function getSchemaVersion(db) {
|
|
125
|
+
try {
|
|
126
|
+
const row = db.prepare("SELECT value FROM schema_meta WHERE key = 'version'").get();
|
|
127
|
+
if (!row) return null;
|
|
128
|
+
const n = Number.parseInt(row.value, 10);
|
|
129
|
+
return Number.isFinite(n) ? n : null;
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function tableHasColumn(db, table, column) {
|
|
135
|
+
try {
|
|
136
|
+
const cols = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
137
|
+
return cols.some((c) => c.name === column);
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function migrateDatabase(db, from, to) {
|
|
143
|
+
let v = from;
|
|
144
|
+
while (v < to) {
|
|
145
|
+
if (v === 1) {
|
|
146
|
+
migrateV1ToV2(db);
|
|
147
|
+
v = 2;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (v === 2) {
|
|
151
|
+
migrateV2ToV3(db);
|
|
152
|
+
v = 3;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
throw new Error(`Unsupported schema migration path: v${from} \u2192 v${to} (stuck at v${v})`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function migrateV1ToV2(db) {
|
|
159
|
+
const pathsMigrated = tableHasColumn(db, "paths", "agent_id");
|
|
160
|
+
const linksMigrated = tableHasColumn(db, "links", "agent_id");
|
|
161
|
+
const alreadyMigrated = pathsMigrated && linksMigrated;
|
|
162
|
+
if (alreadyMigrated) {
|
|
163
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(2));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
db.pragma("foreign_keys = OFF");
|
|
167
|
+
try {
|
|
168
|
+
db.exec("BEGIN");
|
|
169
|
+
if (!pathsMigrated) {
|
|
170
|
+
db.exec(`
|
|
171
|
+
CREATE TABLE IF NOT EXISTS paths_v2 (
|
|
172
|
+
id TEXT PRIMARY KEY,
|
|
173
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
174
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
175
|
+
uri TEXT NOT NULL,
|
|
176
|
+
alias TEXT,
|
|
177
|
+
domain TEXT NOT NULL,
|
|
178
|
+
created_at TEXT NOT NULL,
|
|
179
|
+
UNIQUE(agent_id, uri)
|
|
180
|
+
);
|
|
181
|
+
`);
|
|
182
|
+
db.exec(`
|
|
183
|
+
INSERT INTO paths_v2 (id, memory_id, agent_id, uri, alias, domain, created_at)
|
|
184
|
+
SELECT p.id, p.memory_id, COALESCE(m.agent_id, 'default'), p.uri, p.alias, p.domain, p.created_at
|
|
185
|
+
FROM paths p
|
|
186
|
+
LEFT JOIN memories m ON m.id = p.memory_id;
|
|
187
|
+
`);
|
|
188
|
+
db.exec("DROP TABLE paths;");
|
|
189
|
+
db.exec("ALTER TABLE paths_v2 RENAME TO paths;");
|
|
190
|
+
}
|
|
191
|
+
if (!linksMigrated) {
|
|
192
|
+
db.exec(`
|
|
193
|
+
CREATE TABLE IF NOT EXISTS links_v2 (
|
|
194
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
195
|
+
source_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
196
|
+
target_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
197
|
+
relation TEXT NOT NULL,
|
|
198
|
+
weight REAL NOT NULL DEFAULT 1.0,
|
|
199
|
+
created_at TEXT NOT NULL,
|
|
200
|
+
PRIMARY KEY (agent_id, source_id, target_id)
|
|
201
|
+
);
|
|
202
|
+
`);
|
|
203
|
+
db.exec(`
|
|
204
|
+
INSERT INTO links_v2 (agent_id, source_id, target_id, relation, weight, created_at)
|
|
205
|
+
SELECT COALESCE(ms.agent_id, 'default'), l.source_id, l.target_id, l.relation, l.weight, l.created_at
|
|
206
|
+
FROM links l
|
|
207
|
+
LEFT JOIN memories ms ON ms.id = l.source_id;
|
|
208
|
+
`);
|
|
209
|
+
db.exec(`
|
|
210
|
+
DELETE FROM links_v2
|
|
211
|
+
WHERE EXISTS (SELECT 1 FROM memories s WHERE s.id = links_v2.source_id AND s.agent_id != links_v2.agent_id)
|
|
212
|
+
OR EXISTS (SELECT 1 FROM memories t WHERE t.id = links_v2.target_id AND t.agent_id != links_v2.agent_id);
|
|
213
|
+
`);
|
|
214
|
+
db.exec("DROP TABLE links;");
|
|
215
|
+
db.exec("ALTER TABLE links_v2 RENAME TO links;");
|
|
216
|
+
}
|
|
217
|
+
db.exec(`
|
|
218
|
+
CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
|
|
219
|
+
CREATE INDEX IF NOT EXISTS idx_paths_domain ON paths(domain);
|
|
220
|
+
`);
|
|
221
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(2));
|
|
222
|
+
db.exec("COMMIT");
|
|
223
|
+
} catch (e) {
|
|
224
|
+
try {
|
|
225
|
+
db.exec("ROLLBACK");
|
|
226
|
+
} catch {
|
|
227
|
+
}
|
|
228
|
+
throw e;
|
|
229
|
+
} finally {
|
|
230
|
+
db.pragma("foreign_keys = ON");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function inferSchemaVersion(db) {
|
|
234
|
+
const hasAgentScopedPaths = tableHasColumn(db, "paths", "agent_id");
|
|
235
|
+
const hasAgentScopedLinks = tableHasColumn(db, "links", "agent_id");
|
|
236
|
+
const hasEmbeddings = (() => {
|
|
237
|
+
try {
|
|
238
|
+
const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'").get();
|
|
239
|
+
return Boolean(row);
|
|
240
|
+
} catch {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
})();
|
|
244
|
+
if (hasAgentScopedPaths && hasAgentScopedLinks && hasEmbeddings) return 3;
|
|
245
|
+
if (hasAgentScopedPaths && hasAgentScopedLinks) return 2;
|
|
246
|
+
return 1;
|
|
247
|
+
}
|
|
248
|
+
function ensureIndexes(db) {
|
|
249
|
+
if (tableHasColumn(db, "paths", "agent_id")) {
|
|
250
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_paths_agent_uri ON paths(agent_id, uri);");
|
|
251
|
+
}
|
|
252
|
+
if (tableHasColumn(db, "links", "agent_id")) {
|
|
253
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_links_agent_source ON links(agent_id, source_id);");
|
|
254
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_links_agent_target ON links(agent_id, target_id);");
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'").get();
|
|
258
|
+
if (row) {
|
|
259
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_agent_model ON embeddings(agent_id, model);");
|
|
260
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_memory ON embeddings(memory_id);");
|
|
261
|
+
}
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function migrateV2ToV3(db) {
|
|
266
|
+
try {
|
|
267
|
+
db.exec("BEGIN");
|
|
268
|
+
db.exec(`
|
|
269
|
+
CREATE TABLE IF NOT EXISTS embeddings (
|
|
270
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
271
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
272
|
+
model TEXT NOT NULL,
|
|
273
|
+
dim INTEGER NOT NULL,
|
|
274
|
+
vector BLOB NOT NULL,
|
|
275
|
+
created_at TEXT NOT NULL,
|
|
276
|
+
updated_at TEXT NOT NULL,
|
|
277
|
+
PRIMARY KEY (agent_id, memory_id, model)
|
|
278
|
+
);
|
|
279
|
+
`);
|
|
280
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(3));
|
|
281
|
+
db.exec("COMMIT");
|
|
282
|
+
} catch (e) {
|
|
283
|
+
try {
|
|
284
|
+
db.exec("ROLLBACK");
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
throw e;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
104
290
|
|
|
105
291
|
// src/core/memory.ts
|
|
106
292
|
import { createHash } from "crypto";
|
|
293
|
+
|
|
294
|
+
// src/search/tokenizer.ts
|
|
295
|
+
import { readFileSync } from "fs";
|
|
296
|
+
import { createRequire } from "module";
|
|
297
|
+
var _jieba;
|
|
298
|
+
function getJieba() {
|
|
299
|
+
if (_jieba !== void 0) return _jieba;
|
|
300
|
+
try {
|
|
301
|
+
const req = createRequire(import.meta.url);
|
|
302
|
+
const { Jieba } = req("@node-rs/jieba");
|
|
303
|
+
const dictPath = req.resolve("@node-rs/jieba/dict.txt");
|
|
304
|
+
const dictBuf = readFileSync(dictPath);
|
|
305
|
+
_jieba = Jieba.withDict(dictBuf);
|
|
306
|
+
} catch {
|
|
307
|
+
_jieba = null;
|
|
308
|
+
}
|
|
309
|
+
return _jieba;
|
|
310
|
+
}
|
|
311
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
312
|
+
"\u7684",
|
|
313
|
+
"\u4E86",
|
|
314
|
+
"\u5728",
|
|
315
|
+
"\u662F",
|
|
316
|
+
"\u6211",
|
|
317
|
+
"\u6709",
|
|
318
|
+
"\u548C",
|
|
319
|
+
"\u5C31",
|
|
320
|
+
"\u4E0D",
|
|
321
|
+
"\u4EBA",
|
|
322
|
+
"\u90FD",
|
|
323
|
+
"\u4E00",
|
|
324
|
+
"\u4E2A",
|
|
325
|
+
"\u4E0A",
|
|
326
|
+
"\u4E5F",
|
|
327
|
+
"\u5230",
|
|
328
|
+
"\u4ED6",
|
|
329
|
+
"\u6CA1",
|
|
330
|
+
"\u8FD9",
|
|
331
|
+
"\u8981",
|
|
332
|
+
"\u4F1A",
|
|
333
|
+
"\u5BF9",
|
|
334
|
+
"\u8BF4",
|
|
335
|
+
"\u800C",
|
|
336
|
+
"\u53BB",
|
|
337
|
+
"\u4E4B",
|
|
338
|
+
"\u88AB",
|
|
339
|
+
"\u5979",
|
|
340
|
+
"\u628A",
|
|
341
|
+
"\u90A3"
|
|
342
|
+
]);
|
|
343
|
+
function tokenize(text) {
|
|
344
|
+
const cleaned = text.replace(/[^\w\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af\s]/g, " ");
|
|
345
|
+
const tokens = [];
|
|
346
|
+
const latinWords = cleaned.replace(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/g, " ").split(/\s+/).filter((w) => w.length > 1);
|
|
347
|
+
tokens.push(...latinWords);
|
|
348
|
+
const cjkChunks = cleaned.match(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]+/g);
|
|
349
|
+
if (cjkChunks && cjkChunks.length > 0) {
|
|
350
|
+
const jieba = getJieba();
|
|
351
|
+
for (const chunk of cjkChunks) {
|
|
352
|
+
if (jieba) {
|
|
353
|
+
const words = jieba.cutForSearch(chunk).filter((w) => w.length >= 1);
|
|
354
|
+
tokens.push(...words);
|
|
355
|
+
} else {
|
|
356
|
+
for (const ch of chunk) {
|
|
357
|
+
tokens.push(ch);
|
|
358
|
+
}
|
|
359
|
+
for (let i = 0; i < chunk.length - 1; i++) {
|
|
360
|
+
tokens.push(chunk[i] + chunk[i + 1]);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const unique = [...new Set(tokens)].filter((t) => t.length > 0 && !STOPWORDS.has(t)).slice(0, 30);
|
|
366
|
+
return unique;
|
|
367
|
+
}
|
|
368
|
+
function tokenizeForIndex(text) {
|
|
369
|
+
const tokens = tokenize(text);
|
|
370
|
+
return tokens.join(" ");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/core/memory.ts
|
|
107
374
|
function contentHash(content) {
|
|
108
375
|
return createHash("sha256").update(content.trim()).digest("hex").slice(0, 16);
|
|
109
376
|
}
|
|
@@ -151,7 +418,7 @@ function createMemory(db, input) {
|
|
|
151
418
|
agentId,
|
|
152
419
|
hash
|
|
153
420
|
);
|
|
154
|
-
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, input.content);
|
|
421
|
+
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
|
|
155
422
|
return getMemory(db, id);
|
|
156
423
|
}
|
|
157
424
|
function getMemory(db, id) {
|
|
@@ -196,7 +463,7 @@ function updateMemory(db, id, input) {
|
|
|
196
463
|
db.prepare(`UPDATE memories SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
197
464
|
if (input.content !== void 0) {
|
|
198
465
|
db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
|
|
199
|
-
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, input.content);
|
|
466
|
+
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
|
|
200
467
|
}
|
|
201
468
|
return getMemory(db, id);
|
|
202
469
|
}
|
|
@@ -256,36 +523,42 @@ function parseUri(uri) {
|
|
|
256
523
|
if (!match) throw new Error(`Invalid URI: ${uri}. Expected format: domain://path`);
|
|
257
524
|
return { domain: match[1], path: match[2] };
|
|
258
525
|
}
|
|
259
|
-
function createPath(db, memoryId, uri, alias, validDomains) {
|
|
526
|
+
function createPath(db, memoryId, uri, alias, validDomains, agent_id) {
|
|
260
527
|
const { domain } = parseUri(uri);
|
|
261
528
|
const domains = validDomains ?? DEFAULT_DOMAINS;
|
|
262
529
|
if (!domains.has(domain)) {
|
|
263
530
|
throw new Error(`Invalid domain "${domain}". Valid: ${[...domains].join(", ")}`);
|
|
264
531
|
}
|
|
265
|
-
const
|
|
532
|
+
const memoryAgent = db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(memoryId)?.agent_id;
|
|
533
|
+
if (!memoryAgent) throw new Error(`Memory not found: ${memoryId}`);
|
|
534
|
+
if (agent_id && agent_id !== memoryAgent) {
|
|
535
|
+
throw new Error(`Agent mismatch for path: memory agent_id=${memoryAgent}, requested agent_id=${agent_id}`);
|
|
536
|
+
}
|
|
537
|
+
const agentId = agent_id ?? memoryAgent;
|
|
538
|
+
const existing = db.prepare("SELECT id FROM paths WHERE agent_id = ? AND uri = ?").get(agentId, uri);
|
|
266
539
|
if (existing) {
|
|
267
540
|
throw new Error(`URI already exists: ${uri}`);
|
|
268
541
|
}
|
|
269
542
|
const id = newId();
|
|
270
543
|
db.prepare(
|
|
271
|
-
"INSERT INTO paths (id, memory_id, uri, alias, domain, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
272
|
-
).run(id, memoryId, uri, alias ?? null, domain, now());
|
|
544
|
+
"INSERT INTO paths (id, memory_id, agent_id, uri, alias, domain, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
545
|
+
).run(id, memoryId, agentId, uri, alias ?? null, domain, now());
|
|
273
546
|
return getPath(db, id);
|
|
274
547
|
}
|
|
275
548
|
function getPath(db, id) {
|
|
276
549
|
return db.prepare("SELECT * FROM paths WHERE id = ?").get(id) ?? null;
|
|
277
550
|
}
|
|
278
|
-
function getPathByUri(db, uri) {
|
|
279
|
-
return db.prepare("SELECT * FROM paths WHERE uri = ?").get(uri) ?? null;
|
|
551
|
+
function getPathByUri(db, uri, agent_id = "default") {
|
|
552
|
+
return db.prepare("SELECT * FROM paths WHERE agent_id = ? AND uri = ?").get(agent_id, uri) ?? null;
|
|
280
553
|
}
|
|
281
554
|
function getPathsByMemory(db, memoryId) {
|
|
282
555
|
return db.prepare("SELECT * FROM paths WHERE memory_id = ?").all(memoryId);
|
|
283
556
|
}
|
|
284
|
-
function getPathsByDomain(db, domain) {
|
|
285
|
-
return db.prepare("SELECT * FROM paths WHERE domain = ? ORDER BY uri").all(domain);
|
|
557
|
+
function getPathsByDomain(db, domain, agent_id = "default") {
|
|
558
|
+
return db.prepare("SELECT * FROM paths WHERE agent_id = ? AND domain = ? ORDER BY uri").all(agent_id, domain);
|
|
286
559
|
}
|
|
287
|
-
function getPathsByPrefix(db, prefix) {
|
|
288
|
-
return db.prepare("SELECT * FROM paths WHERE uri LIKE ? ORDER BY uri").all(`${prefix}%`);
|
|
560
|
+
function getPathsByPrefix(db, prefix, agent_id = "default") {
|
|
561
|
+
return db.prepare("SELECT * FROM paths WHERE agent_id = ? AND uri LIKE ? ORDER BY uri").all(agent_id, `${prefix}%`);
|
|
289
562
|
}
|
|
290
563
|
function deletePath(db, id) {
|
|
291
564
|
const result = db.prepare("DELETE FROM paths WHERE id = ?").run(id);
|
|
@@ -293,20 +566,30 @@ function deletePath(db, id) {
|
|
|
293
566
|
}
|
|
294
567
|
|
|
295
568
|
// src/core/link.ts
|
|
296
|
-
function createLink(db, sourceId, targetId, relation, weight = 1) {
|
|
569
|
+
function createLink(db, sourceId, targetId, relation, weight = 1, agent_id) {
|
|
570
|
+
const sourceAgent = db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(sourceId)?.agent_id;
|
|
571
|
+
const targetAgent = db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(targetId)?.agent_id;
|
|
572
|
+
if (!sourceAgent) throw new Error(`Source memory not found: ${sourceId}`);
|
|
573
|
+
if (!targetAgent) throw new Error(`Target memory not found: ${targetId}`);
|
|
574
|
+
if (sourceAgent !== targetAgent) throw new Error("Cross-agent links are not allowed");
|
|
575
|
+
if (agent_id && agent_id !== sourceAgent) throw new Error("Agent mismatch for link");
|
|
576
|
+
const agentId = agent_id ?? sourceAgent;
|
|
297
577
|
db.prepare(
|
|
298
|
-
`INSERT OR REPLACE INTO links (source_id, target_id, relation, weight, created_at)
|
|
299
|
-
VALUES (?, ?, ?, ?, ?)`
|
|
300
|
-
).run(sourceId, targetId, relation, weight, now());
|
|
301
|
-
return { source_id: sourceId, target_id: targetId, relation, weight, created_at: now() };
|
|
578
|
+
`INSERT OR REPLACE INTO links (agent_id, source_id, target_id, relation, weight, created_at)
|
|
579
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
580
|
+
).run(agentId, sourceId, targetId, relation, weight, now());
|
|
581
|
+
return { agent_id: agentId, source_id: sourceId, target_id: targetId, relation, weight, created_at: now() };
|
|
302
582
|
}
|
|
303
|
-
function getLinks(db, memoryId) {
|
|
304
|
-
|
|
583
|
+
function getLinks(db, memoryId, agent_id) {
|
|
584
|
+
const agentId = agent_id ?? db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(memoryId)?.agent_id ?? "default";
|
|
585
|
+
return db.prepare("SELECT * FROM links WHERE agent_id = ? AND (source_id = ? OR target_id = ?)").all(agentId, memoryId, memoryId);
|
|
305
586
|
}
|
|
306
|
-
function getOutgoingLinks(db, sourceId) {
|
|
307
|
-
|
|
587
|
+
function getOutgoingLinks(db, sourceId, agent_id) {
|
|
588
|
+
const agentId = agent_id ?? db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(sourceId)?.agent_id ?? "default";
|
|
589
|
+
return db.prepare("SELECT * FROM links WHERE agent_id = ? AND source_id = ?").all(agentId, sourceId);
|
|
308
590
|
}
|
|
309
|
-
function traverse(db, startId, maxHops = 2) {
|
|
591
|
+
function traverse(db, startId, maxHops = 2, agent_id) {
|
|
592
|
+
const agentId = agent_id ?? db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(startId)?.agent_id ?? "default";
|
|
310
593
|
const visited = /* @__PURE__ */ new Set();
|
|
311
594
|
const results = [];
|
|
312
595
|
const queue = [
|
|
@@ -320,7 +603,7 @@ function traverse(db, startId, maxHops = 2) {
|
|
|
320
603
|
results.push(current);
|
|
321
604
|
}
|
|
322
605
|
if (current.hop < maxHops) {
|
|
323
|
-
const links = db.prepare("SELECT target_id, relation FROM links WHERE source_id = ?").all(current.id);
|
|
606
|
+
const links = db.prepare("SELECT target_id, relation FROM links WHERE agent_id = ? AND source_id = ?").all(agentId, current.id);
|
|
324
607
|
for (const link of links) {
|
|
325
608
|
if (!visited.has(link.target_id)) {
|
|
326
609
|
queue.push({
|
|
@@ -330,7 +613,7 @@ function traverse(db, startId, maxHops = 2) {
|
|
|
330
613
|
});
|
|
331
614
|
}
|
|
332
615
|
}
|
|
333
|
-
const reverseLinks = db.prepare("SELECT source_id, relation FROM links WHERE target_id = ?").all(current.id);
|
|
616
|
+
const reverseLinks = db.prepare("SELECT source_id, relation FROM links WHERE agent_id = ? AND target_id = ?").all(agentId, current.id);
|
|
334
617
|
for (const link of reverseLinks) {
|
|
335
618
|
if (!visited.has(link.source_id)) {
|
|
336
619
|
queue.push({
|
|
@@ -344,8 +627,9 @@ function traverse(db, startId, maxHops = 2) {
|
|
|
344
627
|
}
|
|
345
628
|
return results;
|
|
346
629
|
}
|
|
347
|
-
function deleteLink(db, sourceId, targetId) {
|
|
348
|
-
const
|
|
630
|
+
function deleteLink(db, sourceId, targetId, agent_id) {
|
|
631
|
+
const agentId = agent_id ?? db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(sourceId)?.agent_id ?? "default";
|
|
632
|
+
const result = db.prepare("DELETE FROM links WHERE agent_id = ? AND source_id = ? AND target_id = ?").run(agentId, sourceId, targetId);
|
|
349
633
|
return result.changes > 0;
|
|
350
634
|
}
|
|
351
635
|
|
|
@@ -378,7 +662,7 @@ function rollback(db, snapshotId) {
|
|
|
378
662
|
db.prepare("DELETE FROM memories_fts WHERE id = ?").run(snapshot.memory_id);
|
|
379
663
|
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(
|
|
380
664
|
snapshot.memory_id,
|
|
381
|
-
snapshot.content
|
|
665
|
+
tokenizeForIndex(snapshot.content)
|
|
382
666
|
);
|
|
383
667
|
return true;
|
|
384
668
|
}
|
|
@@ -392,7 +676,7 @@ function guard(db, input) {
|
|
|
392
676
|
return { action: "skip", reason: "Exact duplicate (hash match)", existingId: exactMatch.id };
|
|
393
677
|
}
|
|
394
678
|
if (input.uri) {
|
|
395
|
-
const existingPath = getPathByUri(db, input.uri);
|
|
679
|
+
const existingPath = getPathByUri(db, input.uri, agentId);
|
|
396
680
|
if (existingPath) {
|
|
397
681
|
return {
|
|
398
682
|
action: "update",
|
|
@@ -401,40 +685,148 @@ function guard(db, input) {
|
|
|
401
685
|
};
|
|
402
686
|
}
|
|
403
687
|
}
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
688
|
+
const ftsTokens = tokenize(input.content.slice(0, 200));
|
|
689
|
+
const ftsQuery = ftsTokens.length > 0 ? ftsTokens.slice(0, 8).map((w) => `"${w}"`).join(" OR ") : null;
|
|
690
|
+
if (ftsQuery) {
|
|
691
|
+
try {
|
|
692
|
+
const similar = db.prepare(
|
|
693
|
+
`SELECT m.id, m.content, m.type, rank
|
|
694
|
+
FROM memories_fts f
|
|
695
|
+
JOIN memories m ON m.id = f.id
|
|
696
|
+
WHERE memories_fts MATCH ? AND m.agent_id = ?
|
|
697
|
+
ORDER BY rank
|
|
698
|
+
LIMIT 3`
|
|
699
|
+
).all(ftsQuery, agentId);
|
|
700
|
+
if (similar.length > 0) {
|
|
701
|
+
const topRank = Math.abs(similar[0].rank);
|
|
702
|
+
const tokenCount = ftsTokens.length;
|
|
703
|
+
const dynamicThreshold = tokenCount * 1.5;
|
|
704
|
+
if (topRank > dynamicThreshold) {
|
|
705
|
+
const existing = similar[0];
|
|
706
|
+
if (existing.type === input.type) {
|
|
707
|
+
const merged = `${existing.content}
|
|
416
708
|
|
|
417
709
|
[Updated] ${input.content}`;
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
710
|
+
return {
|
|
711
|
+
action: "merge",
|
|
712
|
+
reason: `Similar content found (score=${topRank.toFixed(1)}, threshold=${dynamicThreshold.toFixed(1)}), merging`,
|
|
713
|
+
existingId: existing.id,
|
|
714
|
+
mergedContent: merged
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
} catch {
|
|
424
720
|
}
|
|
425
721
|
}
|
|
426
|
-
const
|
|
427
|
-
if (
|
|
428
|
-
|
|
429
|
-
return { action: "skip", reason: "Empty content rejected by gate" };
|
|
430
|
-
}
|
|
722
|
+
const gateResult = fourCriterionGate(input);
|
|
723
|
+
if (!gateResult.pass) {
|
|
724
|
+
return { action: "skip", reason: `Gate rejected: ${gateResult.failedCriteria.join(", ")}` };
|
|
431
725
|
}
|
|
432
726
|
return { action: "add", reason: "Passed all guard checks" };
|
|
433
727
|
}
|
|
434
|
-
function
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
728
|
+
function fourCriterionGate(input) {
|
|
729
|
+
const content = input.content.trim();
|
|
730
|
+
const failed = [];
|
|
731
|
+
const priority = input.priority ?? (input.type === "identity" ? 0 : input.type === "emotion" ? 1 : input.type === "knowledge" ? 2 : 3);
|
|
732
|
+
const minLength = priority <= 1 ? 4 : 8;
|
|
733
|
+
const specificity = content.length >= minLength ? Math.min(1, content.length / 50) : 0;
|
|
734
|
+
if (specificity === 0) failed.push(`specificity (too short: ${content.length} < ${minLength} chars)`);
|
|
735
|
+
const tokens = tokenize(content);
|
|
736
|
+
const novelty = tokens.length >= 1 ? Math.min(1, tokens.length / 5) : 0;
|
|
737
|
+
if (novelty === 0) failed.push("novelty (no meaningful tokens after filtering)");
|
|
738
|
+
const hasCJK = /[\u4e00-\u9fff]/.test(content);
|
|
739
|
+
const hasCapitalized = /[A-Z][a-z]+/.test(content);
|
|
740
|
+
const hasNumbers = /\d+/.test(content);
|
|
741
|
+
const hasURI = /\w+:\/\//.test(content);
|
|
742
|
+
const hasEntityMarkers = /[@#]/.test(content);
|
|
743
|
+
const hasMeaningfulLength = content.length >= 15;
|
|
744
|
+
const topicSignals = [hasCJK, hasCapitalized, hasNumbers, hasURI, hasEntityMarkers, hasMeaningfulLength].filter(Boolean).length;
|
|
745
|
+
const relevance = topicSignals >= 1 ? Math.min(1, topicSignals / 3) : 0;
|
|
746
|
+
if (relevance === 0) failed.push("relevance (no identifiable topics/entities)");
|
|
747
|
+
const allCaps = content === content.toUpperCase() && content.length > 20 && /^[A-Z\s]+$/.test(content);
|
|
748
|
+
const hasWhitespaceOrPunctuation = /[\s,。!?,.!?;;::]/.test(content) || content.length < 30;
|
|
749
|
+
const excessiveRepetition = /(.)\1{9,}/.test(content);
|
|
750
|
+
let coherence = 1;
|
|
751
|
+
if (allCaps) {
|
|
752
|
+
coherence -= 0.5;
|
|
753
|
+
}
|
|
754
|
+
if (!hasWhitespaceOrPunctuation) {
|
|
755
|
+
coherence -= 0.3;
|
|
756
|
+
}
|
|
757
|
+
if (excessiveRepetition) {
|
|
758
|
+
coherence -= 0.5;
|
|
759
|
+
}
|
|
760
|
+
coherence = Math.max(0, coherence);
|
|
761
|
+
if (coherence < 0.3) failed.push("coherence (garbled or malformed content)");
|
|
762
|
+
return {
|
|
763
|
+
pass: failed.length === 0,
|
|
764
|
+
scores: { specificity, novelty, relevance, coherence },
|
|
765
|
+
failedCriteria: failed
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// src/core/export.ts
|
|
770
|
+
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
771
|
+
import { join } from "path";
|
|
772
|
+
function exportMemories(db, dirPath, opts) {
|
|
773
|
+
const agentId = opts?.agent_id ?? "default";
|
|
774
|
+
if (!existsSync(dirPath)) mkdirSync(dirPath, { recursive: true });
|
|
775
|
+
let exported = 0;
|
|
776
|
+
const files = [];
|
|
777
|
+
const identities = listMemories(db, { agent_id: agentId, type: "identity" });
|
|
778
|
+
const knowledge = listMemories(db, { agent_id: agentId, type: "knowledge" });
|
|
779
|
+
const emotions = listMemories(db, { agent_id: agentId, type: "emotion" });
|
|
780
|
+
if (identities.length || knowledge.length || emotions.length) {
|
|
781
|
+
const sections = ["# Agent Memory Export\n"];
|
|
782
|
+
if (identities.length) {
|
|
783
|
+
sections.push("## Identity\n");
|
|
784
|
+
for (const m of identities) {
|
|
785
|
+
sections.push(`- ${m.content}
|
|
786
|
+
`);
|
|
787
|
+
exported++;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (emotions.length) {
|
|
791
|
+
sections.push("\n## Emotions\n");
|
|
792
|
+
for (const m of emotions) {
|
|
793
|
+
sections.push(`- ${m.content}
|
|
794
|
+
`);
|
|
795
|
+
exported++;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (knowledge.length) {
|
|
799
|
+
sections.push("\n## Knowledge\n");
|
|
800
|
+
for (const m of knowledge) {
|
|
801
|
+
sections.push(`- ${m.content}
|
|
802
|
+
`);
|
|
803
|
+
exported++;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
const memoryPath = join(dirPath, "MEMORY.md");
|
|
807
|
+
writeFileSync(memoryPath, sections.join("\n"));
|
|
808
|
+
files.push(memoryPath);
|
|
809
|
+
}
|
|
810
|
+
const events = listMemories(db, { agent_id: agentId, type: "event", limit: 1e4 });
|
|
811
|
+
const byDate = /* @__PURE__ */ new Map();
|
|
812
|
+
for (const ev of events) {
|
|
813
|
+
const date = ev.created_at.slice(0, 10);
|
|
814
|
+
if (!byDate.has(date)) byDate.set(date, []);
|
|
815
|
+
byDate.get(date).push(ev);
|
|
816
|
+
}
|
|
817
|
+
for (const [date, mems] of byDate) {
|
|
818
|
+
const lines = [`# ${date}
|
|
819
|
+
`];
|
|
820
|
+
for (const m of mems) {
|
|
821
|
+
lines.push(`- ${m.content}
|
|
822
|
+
`);
|
|
823
|
+
exported++;
|
|
824
|
+
}
|
|
825
|
+
const filePath = join(dirPath, `${date}.md`);
|
|
826
|
+
writeFileSync(filePath, lines.join("\n"));
|
|
827
|
+
files.push(filePath);
|
|
828
|
+
}
|
|
829
|
+
return { exported, files };
|
|
438
830
|
}
|
|
439
831
|
|
|
440
832
|
// src/search/bm25.ts
|
|
@@ -455,12 +847,15 @@ function searchBM25(db, query, opts) {
|
|
|
455
847
|
ORDER BY rank
|
|
456
848
|
LIMIT ?`
|
|
457
849
|
).all(ftsQuery, agentId, minVitality, limit);
|
|
458
|
-
return rows.map((row) =>
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
850
|
+
return rows.map((row) => {
|
|
851
|
+
const { score: _score, ...memoryFields } = row;
|
|
852
|
+
return {
|
|
853
|
+
memory: memoryFields,
|
|
854
|
+
score: Math.abs(row.score),
|
|
855
|
+
// FTS5 rank is negative (lower = better)
|
|
856
|
+
matchReason: "bm25"
|
|
857
|
+
};
|
|
858
|
+
});
|
|
464
859
|
} catch {
|
|
465
860
|
return searchSimple(db, query, agentId, minVitality, limit);
|
|
466
861
|
}
|
|
@@ -480,52 +875,76 @@ function searchSimple(db, query, agentId, minVitality, limit) {
|
|
|
480
875
|
}));
|
|
481
876
|
}
|
|
482
877
|
function buildFtsQuery(text) {
|
|
483
|
-
const
|
|
484
|
-
if (
|
|
485
|
-
return
|
|
878
|
+
const tokens = tokenize(text);
|
|
879
|
+
if (tokens.length === 0) return null;
|
|
880
|
+
return tokens.map((w) => `"${w}"`).join(" OR ");
|
|
486
881
|
}
|
|
487
882
|
|
|
488
883
|
// src/search/intent.ts
|
|
489
884
|
var INTENT_PATTERNS = {
|
|
490
885
|
factual: [
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
886
|
+
// English
|
|
887
|
+
/^(what|who|where|which|how much|how many)\b/i,
|
|
888
|
+
/\b(name|address|number|password|config|setting)\b/i,
|
|
889
|
+
// Chinese - questions about facts
|
|
890
|
+
/是(什么|谁|哪|啥)/,
|
|
891
|
+
/叫(什么|啥)/,
|
|
892
|
+
/(名字|地址|号码|密码|配置|设置|账号|邮箱|链接|版本)/,
|
|
893
|
+
/(多少|几个|哪个|哪些|哪里)/,
|
|
894
|
+
// Chinese - lookup patterns
|
|
895
|
+
/(查一下|找一下|看看|搜一下)/,
|
|
896
|
+
/(.+)是什么$/
|
|
500
897
|
],
|
|
501
898
|
temporal: [
|
|
502
|
-
|
|
503
|
-
|
|
899
|
+
// English
|
|
900
|
+
/^(when|what time|how long)\b/i,
|
|
901
|
+
/\b(yesterday|today|tomorrow|last week|recently|ago|before|after)\b/i,
|
|
902
|
+
/\b(first|latest|newest|oldest|previous|next)\b/i,
|
|
903
|
+
// Chinese - time expressions
|
|
504
904
|
/什么时候/,
|
|
505
|
-
/(
|
|
506
|
-
|
|
507
|
-
/(
|
|
905
|
+
/(昨天|今天|明天|上周|下周|最近|以前|之前|之后|刚才|刚刚)/,
|
|
906
|
+
/(几月|几号|几点|多久|多长时间)/,
|
|
907
|
+
/(上次|下次|第一次|最后一次|那天|那时)/,
|
|
908
|
+
// Date patterns
|
|
909
|
+
/\d{4}[-/.]\d{1,2}/,
|
|
910
|
+
/\d{1,2}月\d{1,2}[日号]/,
|
|
911
|
+
// Chinese - temporal context
|
|
912
|
+
/(历史|记录|日志|以来|至今|期间)/
|
|
508
913
|
],
|
|
509
914
|
causal: [
|
|
510
|
-
|
|
511
|
-
/^(
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
915
|
+
// English
|
|
916
|
+
/^(why|how come|what caused)\b/i,
|
|
917
|
+
/\b(because|due to|reason|cause|result)\b/i,
|
|
918
|
+
// Chinese - causal questions
|
|
919
|
+
/为(什么|啥|何)/,
|
|
920
|
+
/(原因|导致|造成|引起|因为|所以|结果)/,
|
|
921
|
+
/(怎么回事|怎么了|咋回事|咋了)/,
|
|
922
|
+
/(为啥|凭啥|凭什么)/,
|
|
923
|
+
// Chinese - problem/diagnosis
|
|
924
|
+
/(出(了|了什么)?问题|报错|失败|出错|bug)/
|
|
517
925
|
],
|
|
518
926
|
exploratory: [
|
|
519
|
-
|
|
520
|
-
/^(
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
927
|
+
// English
|
|
928
|
+
/^(how|tell me about|explain|describe|show me)\b/i,
|
|
929
|
+
/^(what do you think|what about|any)\b/i,
|
|
930
|
+
/\b(overview|summary|list|compare)\b/i,
|
|
931
|
+
// Chinese - exploratory
|
|
932
|
+
/(怎么样|怎样|如何)/,
|
|
933
|
+
/(介绍|说说|讲讲|聊聊|谈谈)/,
|
|
934
|
+
/(有哪些|有什么|有没有)/,
|
|
935
|
+
/(关于|对于|至于|关联)/,
|
|
936
|
+
/(总结|概括|梳理|回顾|盘点)/,
|
|
937
|
+
// Chinese - opinion/analysis
|
|
938
|
+
/(看法|想法|意见|建议|评价|感觉|觉得)/,
|
|
939
|
+
/(对比|比较|区别|差异|优缺点)/
|
|
527
940
|
]
|
|
528
941
|
};
|
|
942
|
+
var CN_STRUCTURE_BOOSTS = {
|
|
943
|
+
factual: [/^.{1,6}(是什么|叫什么|在哪)/, /^(谁|哪)/],
|
|
944
|
+
temporal: [/^(什么时候|上次|最近)/, /(时间|日期)$/],
|
|
945
|
+
causal: [/^(为什么|为啥)/, /(为什么|怎么回事)$/],
|
|
946
|
+
exploratory: [/^(怎么|如何|说说)/, /(哪些|什么样)$/]
|
|
947
|
+
};
|
|
529
948
|
function classifyIntent(query) {
|
|
530
949
|
const scores = {
|
|
531
950
|
factual: 0,
|
|
@@ -540,6 +959,18 @@ function classifyIntent(query) {
|
|
|
540
959
|
}
|
|
541
960
|
}
|
|
542
961
|
}
|
|
962
|
+
for (const [intent, patterns] of Object.entries(CN_STRUCTURE_BOOSTS)) {
|
|
963
|
+
for (const pattern of patterns) {
|
|
964
|
+
if (pattern.test(query)) {
|
|
965
|
+
scores[intent] += 0.5;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
const tokens = tokenize(query);
|
|
970
|
+
const totalPatternScore = Object.values(scores).reduce((a, b) => a + b, 0);
|
|
971
|
+
if (totalPatternScore === 0 && tokens.length <= 3) {
|
|
972
|
+
scores.factual += 1;
|
|
973
|
+
}
|
|
543
974
|
let maxIntent = "factual";
|
|
544
975
|
let maxScore = 0;
|
|
545
976
|
for (const [intent, score] of Object.entries(scores)) {
|
|
@@ -549,7 +980,7 @@ function classifyIntent(query) {
|
|
|
549
980
|
}
|
|
550
981
|
}
|
|
551
982
|
const totalScore = Object.values(scores).reduce((a, b) => a + b, 0);
|
|
552
|
-
const confidence = totalScore > 0 ? maxScore / totalScore : 0.5;
|
|
983
|
+
const confidence = totalScore > 0 ? Math.min(0.95, maxScore / totalScore) : 0.5;
|
|
553
984
|
return { intent: maxIntent, confidence };
|
|
554
985
|
}
|
|
555
986
|
function getStrategy(intent) {
|
|
@@ -566,6 +997,26 @@ function getStrategy(intent) {
|
|
|
566
997
|
}
|
|
567
998
|
|
|
568
999
|
// src/search/rerank.ts
|
|
1000
|
+
async function rerankWithProvider(results, query, provider) {
|
|
1001
|
+
if (results.length === 0) return results;
|
|
1002
|
+
const documents = results.map((r) => r.memory.content);
|
|
1003
|
+
try {
|
|
1004
|
+
const apiResults = await provider.rerank(query, documents);
|
|
1005
|
+
const scoreMap = new Map(apiResults.map((r) => [r.index, r.relevance_score]));
|
|
1006
|
+
return results.map((r, i) => {
|
|
1007
|
+
const score = scoreMap.get(i);
|
|
1008
|
+
if (score === void 0) return r;
|
|
1009
|
+
return {
|
|
1010
|
+
...r,
|
|
1011
|
+
score,
|
|
1012
|
+
matchReason: `${r.matchReason}+rerank`
|
|
1013
|
+
};
|
|
1014
|
+
});
|
|
1015
|
+
} catch (err) {
|
|
1016
|
+
console.warn("[agent-memory] External rerank failed, falling back:", err);
|
|
1017
|
+
return results;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
569
1020
|
function rerank(results, opts) {
|
|
570
1021
|
const now2 = Date.now();
|
|
571
1022
|
const scored = results.map((r) => {
|
|
@@ -587,6 +1038,347 @@ function rerank(results, opts) {
|
|
|
587
1038
|
return scored.slice(0, opts.limit);
|
|
588
1039
|
}
|
|
589
1040
|
|
|
1041
|
+
// src/search/embeddings.ts
|
|
1042
|
+
function encodeEmbedding(vector) {
|
|
1043
|
+
const arr = vector instanceof Float32Array ? vector : Float32Array.from(vector);
|
|
1044
|
+
return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
1045
|
+
}
|
|
1046
|
+
function decodeEmbedding(buf) {
|
|
1047
|
+
const copy = Buffer.from(buf);
|
|
1048
|
+
return new Float32Array(copy.buffer, copy.byteOffset, Math.floor(copy.byteLength / 4));
|
|
1049
|
+
}
|
|
1050
|
+
function upsertEmbedding(db, input) {
|
|
1051
|
+
const ts = now();
|
|
1052
|
+
const vec = input.vector instanceof Float32Array ? input.vector : Float32Array.from(input.vector);
|
|
1053
|
+
const blob = encodeEmbedding(vec);
|
|
1054
|
+
db.prepare(
|
|
1055
|
+
`INSERT INTO embeddings (agent_id, memory_id, model, dim, vector, created_at, updated_at)
|
|
1056
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1057
|
+
ON CONFLICT(agent_id, memory_id, model) DO UPDATE SET
|
|
1058
|
+
dim = excluded.dim,
|
|
1059
|
+
vector = excluded.vector,
|
|
1060
|
+
updated_at = excluded.updated_at`
|
|
1061
|
+
).run(input.agent_id, input.memory_id, input.model, vec.length, blob, ts, ts);
|
|
1062
|
+
}
|
|
1063
|
+
function getEmbedding(db, agent_id, memory_id, model) {
|
|
1064
|
+
const row = db.prepare(
|
|
1065
|
+
"SELECT agent_id, memory_id, model, dim, vector, created_at, updated_at FROM embeddings WHERE agent_id = ? AND memory_id = ? AND model = ?"
|
|
1066
|
+
).get(agent_id, memory_id, model);
|
|
1067
|
+
if (!row) return null;
|
|
1068
|
+
return { ...row, vector: decodeEmbedding(row.vector) };
|
|
1069
|
+
}
|
|
1070
|
+
function listEmbeddings(db, agent_id, model) {
|
|
1071
|
+
const rows = db.prepare(
|
|
1072
|
+
"SELECT memory_id, vector FROM embeddings WHERE agent_id = ? AND model = ?"
|
|
1073
|
+
).all(agent_id, model);
|
|
1074
|
+
return rows.map((r) => ({ memory_id: r.memory_id, vector: decodeEmbedding(r.vector) }));
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// src/search/hybrid.ts
|
|
1078
|
+
function cosine(a, b) {
|
|
1079
|
+
const n = Math.min(a.length, b.length);
|
|
1080
|
+
let dot = 0;
|
|
1081
|
+
let na = 0;
|
|
1082
|
+
let nb = 0;
|
|
1083
|
+
for (let i = 0; i < n; i++) {
|
|
1084
|
+
const x = a[i];
|
|
1085
|
+
const y = b[i];
|
|
1086
|
+
dot += x * y;
|
|
1087
|
+
na += x * x;
|
|
1088
|
+
nb += y * y;
|
|
1089
|
+
}
|
|
1090
|
+
if (na === 0 || nb === 0) return 0;
|
|
1091
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
1092
|
+
}
|
|
1093
|
+
function rrfScore(rank, k) {
|
|
1094
|
+
return 1 / (k + rank);
|
|
1095
|
+
}
|
|
1096
|
+
function fuseRrf(lists, k) {
|
|
1097
|
+
const out = /* @__PURE__ */ new Map();
|
|
1098
|
+
for (const list of lists) {
|
|
1099
|
+
for (let i = 0; i < list.items.length; i++) {
|
|
1100
|
+
const it = list.items[i];
|
|
1101
|
+
const rank = i + 1;
|
|
1102
|
+
const add = rrfScore(rank, k);
|
|
1103
|
+
const prev = out.get(it.id);
|
|
1104
|
+
if (!prev) out.set(it.id, { score: add, sources: [list.name] });
|
|
1105
|
+
else {
|
|
1106
|
+
prev.score += add;
|
|
1107
|
+
if (!prev.sources.includes(list.name)) prev.sources.push(list.name);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return out;
|
|
1112
|
+
}
|
|
1113
|
+
function fetchMemories(db, ids, agentId) {
|
|
1114
|
+
if (ids.length === 0) return [];
|
|
1115
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
1116
|
+
const sql = agentId ? `SELECT * FROM memories WHERE id IN (${placeholders}) AND agent_id = ?` : `SELECT * FROM memories WHERE id IN (${placeholders})`;
|
|
1117
|
+
const rows = db.prepare(sql).all(...agentId ? [...ids, agentId] : ids);
|
|
1118
|
+
return rows;
|
|
1119
|
+
}
|
|
1120
|
+
async function searchHybrid(db, query, opts) {
|
|
1121
|
+
const agentId = opts?.agent_id ?? "default";
|
|
1122
|
+
const limit = opts?.limit ?? 10;
|
|
1123
|
+
const bm25Mult = opts?.bm25CandidateMultiplier ?? 3;
|
|
1124
|
+
const semanticCandidates = opts?.semanticCandidates ?? 50;
|
|
1125
|
+
const rrfK = opts?.rrfK ?? 60;
|
|
1126
|
+
const bm25 = searchBM25(db, query, {
|
|
1127
|
+
agent_id: agentId,
|
|
1128
|
+
limit: limit * bm25Mult
|
|
1129
|
+
});
|
|
1130
|
+
const provider = opts?.embeddingProvider ?? null;
|
|
1131
|
+
const model = opts?.embeddingModel ?? provider?.model;
|
|
1132
|
+
if (!provider || !model) {
|
|
1133
|
+
return bm25.slice(0, limit);
|
|
1134
|
+
}
|
|
1135
|
+
const embedFn = provider.embedQuery ?? provider.embed;
|
|
1136
|
+
const qVec = Float32Array.from(await embedFn.call(provider, query));
|
|
1137
|
+
const embeddings = listEmbeddings(db, agentId, model);
|
|
1138
|
+
const scored = [];
|
|
1139
|
+
for (const e of embeddings) {
|
|
1140
|
+
scored.push({ id: e.memory_id, score: cosine(qVec, e.vector) });
|
|
1141
|
+
}
|
|
1142
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1143
|
+
const semanticTop = scored.slice(0, semanticCandidates);
|
|
1144
|
+
const fused = fuseRrf(
|
|
1145
|
+
[
|
|
1146
|
+
{ name: "bm25", items: bm25.map((r) => ({ id: r.memory.id, score: r.score })) },
|
|
1147
|
+
{ name: "semantic", items: semanticTop }
|
|
1148
|
+
],
|
|
1149
|
+
rrfK
|
|
1150
|
+
);
|
|
1151
|
+
const ids = [...fused.keys()];
|
|
1152
|
+
const memories = fetchMemories(db, ids, agentId);
|
|
1153
|
+
const byId = new Map(memories.map((m) => [m.id, m]));
|
|
1154
|
+
const out = [];
|
|
1155
|
+
for (const [id, meta] of fused) {
|
|
1156
|
+
const mem = byId.get(id);
|
|
1157
|
+
if (!mem) continue;
|
|
1158
|
+
out.push({
|
|
1159
|
+
memory: mem,
|
|
1160
|
+
score: meta.score,
|
|
1161
|
+
matchReason: meta.sources.sort().join("+")
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
out.sort((a, b) => b.score - a.score);
|
|
1165
|
+
return out.slice(0, limit);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// src/search/providers.ts
|
|
1169
|
+
var QWEN_DEFAULT_INSTRUCTION = "Given a query, retrieve the most semantically relevant document";
|
|
1170
|
+
function getDefaultInstruction(model) {
|
|
1171
|
+
const m = model.toLowerCase();
|
|
1172
|
+
if (m.includes("qwen")) return QWEN_DEFAULT_INSTRUCTION;
|
|
1173
|
+
if (m.includes("gemini")) return null;
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
function resolveInstruction(model) {
|
|
1177
|
+
const override = process.env.AGENT_MEMORY_EMBEDDINGS_INSTRUCTION;
|
|
1178
|
+
if (override !== void 0) {
|
|
1179
|
+
const normalized = override.trim();
|
|
1180
|
+
if (!normalized) return null;
|
|
1181
|
+
const lowered = normalized.toLowerCase();
|
|
1182
|
+
if (lowered === "none" || lowered === "off" || lowered === "false" || lowered === "null") return null;
|
|
1183
|
+
return normalized;
|
|
1184
|
+
}
|
|
1185
|
+
return getDefaultInstruction(model);
|
|
1186
|
+
}
|
|
1187
|
+
function buildQueryInput(query, instructionPrefix) {
|
|
1188
|
+
if (!instructionPrefix) return query;
|
|
1189
|
+
return `Instruct: ${instructionPrefix}
|
|
1190
|
+
Query: ${query}`;
|
|
1191
|
+
}
|
|
1192
|
+
function getEmbeddingProviderFromEnv() {
|
|
1193
|
+
const provider = (process.env.AGENT_MEMORY_EMBEDDINGS_PROVIDER ?? "none").toLowerCase();
|
|
1194
|
+
if (provider === "none" || provider === "off" || provider === "false") return null;
|
|
1195
|
+
if (provider === "openai") {
|
|
1196
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
1197
|
+
const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-3-small";
|
|
1198
|
+
const baseUrl = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1";
|
|
1199
|
+
if (!apiKey) return null;
|
|
1200
|
+
const instruction = resolveInstruction(model);
|
|
1201
|
+
return createOpenAIProvider({ apiKey, model, baseUrl, instruction });
|
|
1202
|
+
}
|
|
1203
|
+
if (provider === "gemini" || provider === "google") {
|
|
1204
|
+
const apiKey = process.env.GEMINI_API_KEY ?? process.env.OPENAI_API_KEY;
|
|
1205
|
+
const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "gemini-embedding-001";
|
|
1206
|
+
const baseUrl = process.env.GEMINI_BASE_URL ?? process.env.OPENAI_BASE_URL ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
1207
|
+
if (!apiKey) return null;
|
|
1208
|
+
const instruction = resolveInstruction(model);
|
|
1209
|
+
return createOpenAIProvider({ id: "gemini", apiKey, model, baseUrl, instruction });
|
|
1210
|
+
}
|
|
1211
|
+
if (provider === "qwen" || provider === "dashscope" || provider === "tongyi") {
|
|
1212
|
+
const apiKey = process.env.DASHSCOPE_API_KEY;
|
|
1213
|
+
const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-v3";
|
|
1214
|
+
const baseUrl = process.env.DASHSCOPE_BASE_URL ?? "https://dashscope.aliyuncs.com";
|
|
1215
|
+
if (!apiKey) return null;
|
|
1216
|
+
const instruction = resolveInstruction(model);
|
|
1217
|
+
return createDashScopeProvider({ apiKey, model, baseUrl, instruction });
|
|
1218
|
+
}
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
function authHeader(apiKey) {
|
|
1222
|
+
return apiKey.startsWith("Bearer ") ? apiKey : `Bearer ${apiKey}`;
|
|
1223
|
+
}
|
|
1224
|
+
function normalizeEmbedding(e) {
|
|
1225
|
+
if (!Array.isArray(e)) throw new Error("Invalid embedding: not an array");
|
|
1226
|
+
if (e.length === 0) throw new Error("Invalid embedding: empty");
|
|
1227
|
+
return e.map((x) => {
|
|
1228
|
+
if (typeof x !== "number" || !Number.isFinite(x)) throw new Error("Invalid embedding: non-numeric value");
|
|
1229
|
+
return x;
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
function createOpenAIProvider(opts) {
|
|
1233
|
+
const baseUrl = opts.baseUrl ?? "https://api.openai.com/v1";
|
|
1234
|
+
const instructionPrefix = opts.instruction ?? null;
|
|
1235
|
+
async function requestEmbedding(input) {
|
|
1236
|
+
const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/embeddings`, {
|
|
1237
|
+
method: "POST",
|
|
1238
|
+
headers: {
|
|
1239
|
+
"content-type": "application/json",
|
|
1240
|
+
authorization: authHeader(opts.apiKey)
|
|
1241
|
+
},
|
|
1242
|
+
body: JSON.stringify({ model: opts.model, input })
|
|
1243
|
+
});
|
|
1244
|
+
if (!resp.ok) {
|
|
1245
|
+
const body = await resp.text().catch(() => "");
|
|
1246
|
+
throw new Error(`OpenAI embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
|
|
1247
|
+
}
|
|
1248
|
+
const data = await resp.json();
|
|
1249
|
+
return normalizeEmbedding(data.data?.[0]?.embedding);
|
|
1250
|
+
}
|
|
1251
|
+
return {
|
|
1252
|
+
id: opts.id ?? "openai",
|
|
1253
|
+
model: opts.model,
|
|
1254
|
+
instructionPrefix,
|
|
1255
|
+
async embed(text) {
|
|
1256
|
+
return requestEmbedding(text);
|
|
1257
|
+
},
|
|
1258
|
+
async embedQuery(query) {
|
|
1259
|
+
return requestEmbedding(buildQueryInput(query, instructionPrefix));
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
function createDashScopeProvider(opts) {
|
|
1264
|
+
const baseUrl = opts.baseUrl ?? "https://dashscope.aliyuncs.com";
|
|
1265
|
+
const instructionPrefix = opts.instruction ?? null;
|
|
1266
|
+
async function requestEmbedding(text) {
|
|
1267
|
+
const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/services/embeddings/text-embedding/text-embedding`, {
|
|
1268
|
+
method: "POST",
|
|
1269
|
+
headers: {
|
|
1270
|
+
"content-type": "application/json",
|
|
1271
|
+
authorization: authHeader(opts.apiKey)
|
|
1272
|
+
},
|
|
1273
|
+
body: JSON.stringify({
|
|
1274
|
+
model: opts.model,
|
|
1275
|
+
input: { texts: [text] }
|
|
1276
|
+
})
|
|
1277
|
+
});
|
|
1278
|
+
if (!resp.ok) {
|
|
1279
|
+
const body = await resp.text().catch(() => "");
|
|
1280
|
+
throw new Error(`DashScope embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
|
|
1281
|
+
}
|
|
1282
|
+
const data = await resp.json();
|
|
1283
|
+
const emb = data.output?.embeddings?.[0]?.embedding ?? data.output?.embeddings?.[0]?.vector ?? data.output?.embedding ?? data.data?.[0]?.embedding;
|
|
1284
|
+
return normalizeEmbedding(emb);
|
|
1285
|
+
}
|
|
1286
|
+
return {
|
|
1287
|
+
id: "dashscope",
|
|
1288
|
+
model: opts.model,
|
|
1289
|
+
instructionPrefix,
|
|
1290
|
+
async embed(text) {
|
|
1291
|
+
return requestEmbedding(text);
|
|
1292
|
+
},
|
|
1293
|
+
async embedQuery(query) {
|
|
1294
|
+
return requestEmbedding(buildQueryInput(query, instructionPrefix));
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// src/search/rerank-provider.ts
|
|
1300
|
+
function authHeader2(apiKey) {
|
|
1301
|
+
return apiKey.startsWith("Bearer ") ? apiKey : `Bearer ${apiKey}`;
|
|
1302
|
+
}
|
|
1303
|
+
function getRerankerProviderFromEnv() {
|
|
1304
|
+
const provider = (process.env.AGENT_MEMORY_RERANK_PROVIDER ?? "none").toLowerCase();
|
|
1305
|
+
if (provider === "none" || provider === "off") return null;
|
|
1306
|
+
if (provider === "openai" || provider === "jina" || provider === "cohere") {
|
|
1307
|
+
const apiKey = process.env.AGENT_MEMORY_RERANK_API_KEY ?? process.env.OPENAI_API_KEY;
|
|
1308
|
+
const model = process.env.AGENT_MEMORY_RERANK_MODEL ?? "Qwen/Qwen3-Reranker-8B";
|
|
1309
|
+
const baseUrl = process.env.AGENT_MEMORY_RERANK_BASE_URL ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1";
|
|
1310
|
+
if (!apiKey) return null;
|
|
1311
|
+
return createOpenAIRerankProvider({ apiKey, model, baseUrl });
|
|
1312
|
+
}
|
|
1313
|
+
return null;
|
|
1314
|
+
}
|
|
1315
|
+
function createOpenAIRerankProvider(opts) {
|
|
1316
|
+
const baseUrl = opts.baseUrl ?? "https://api.openai.com/v1";
|
|
1317
|
+
return {
|
|
1318
|
+
id: "openai-rerank",
|
|
1319
|
+
model: opts.model,
|
|
1320
|
+
async rerank(query, documents) {
|
|
1321
|
+
const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/rerank`, {
|
|
1322
|
+
method: "POST",
|
|
1323
|
+
headers: {
|
|
1324
|
+
"content-type": "application/json",
|
|
1325
|
+
authorization: authHeader2(opts.apiKey)
|
|
1326
|
+
},
|
|
1327
|
+
body: JSON.stringify({ model: opts.model, query, documents })
|
|
1328
|
+
});
|
|
1329
|
+
if (!resp.ok) {
|
|
1330
|
+
const body = await resp.text().catch(() => "");
|
|
1331
|
+
throw new Error(`Rerank API failed: ${resp.status} ${resp.statusText} ${body}`.trim());
|
|
1332
|
+
}
|
|
1333
|
+
const data = await resp.json();
|
|
1334
|
+
const results = data.results ?? [];
|
|
1335
|
+
return results.map((r) => {
|
|
1336
|
+
const index = typeof r.index === "number" ? r.index : Number.NaN;
|
|
1337
|
+
const relevance = typeof r.relevance_score === "number" ? r.relevance_score : Number.NaN;
|
|
1338
|
+
return { index, relevance_score: relevance };
|
|
1339
|
+
}).filter((r) => Number.isInteger(r.index) && Number.isFinite(r.relevance_score));
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// src/search/embed.ts
|
|
1345
|
+
async function embedMemory(db, memoryId, provider, opts) {
|
|
1346
|
+
const row = db.prepare("SELECT id, agent_id, content FROM memories WHERE id = ?").get(memoryId);
|
|
1347
|
+
if (!row) return false;
|
|
1348
|
+
if (opts?.agent_id && row.agent_id !== opts.agent_id) return false;
|
|
1349
|
+
const model = opts?.model ?? provider.model;
|
|
1350
|
+
const maxChars = opts?.maxChars ?? 2e3;
|
|
1351
|
+
const text = row.content.length > maxChars ? row.content.slice(0, maxChars) : row.content;
|
|
1352
|
+
const vector = await provider.embed(text);
|
|
1353
|
+
upsertEmbedding(db, {
|
|
1354
|
+
agent_id: row.agent_id,
|
|
1355
|
+
memory_id: row.id,
|
|
1356
|
+
model,
|
|
1357
|
+
vector
|
|
1358
|
+
});
|
|
1359
|
+
return true;
|
|
1360
|
+
}
|
|
1361
|
+
async function embedMissingForAgent(db, provider, opts) {
|
|
1362
|
+
const agentId = opts?.agent_id ?? "default";
|
|
1363
|
+
const model = opts?.model ?? provider.model;
|
|
1364
|
+
const limit = opts?.limit ?? 1e3;
|
|
1365
|
+
const rows = db.prepare(
|
|
1366
|
+
`SELECT m.id
|
|
1367
|
+
FROM memories m
|
|
1368
|
+
LEFT JOIN embeddings e
|
|
1369
|
+
ON e.memory_id = m.id AND e.agent_id = m.agent_id AND e.model = ?
|
|
1370
|
+
WHERE m.agent_id = ? AND e.memory_id IS NULL
|
|
1371
|
+
ORDER BY m.updated_at DESC
|
|
1372
|
+
LIMIT ?`
|
|
1373
|
+
).all(model, agentId, limit);
|
|
1374
|
+
let embedded = 0;
|
|
1375
|
+
for (const r of rows) {
|
|
1376
|
+
const ok = await embedMemory(db, r.id, provider, { agent_id: agentId, model, maxChars: opts?.maxChars });
|
|
1377
|
+
if (ok) embedded++;
|
|
1378
|
+
}
|
|
1379
|
+
return { embedded, scanned: rows.length };
|
|
1380
|
+
}
|
|
1381
|
+
|
|
590
1382
|
// src/sleep/decay.ts
|
|
591
1383
|
var MIN_VITALITY = {
|
|
592
1384
|
0: 1,
|
|
@@ -598,25 +1390,28 @@ var MIN_VITALITY = {
|
|
|
598
1390
|
3: 0
|
|
599
1391
|
// P3: event — full decay
|
|
600
1392
|
};
|
|
601
|
-
function calculateVitality(stability,
|
|
1393
|
+
function calculateVitality(stability, daysSinceLastAccess, priority) {
|
|
602
1394
|
if (priority === 0) return 1;
|
|
603
1395
|
const S = Math.max(0.01, stability);
|
|
604
|
-
const retention = Math.exp(-
|
|
1396
|
+
const retention = Math.exp(-daysSinceLastAccess / S);
|
|
605
1397
|
const minVit = MIN_VITALITY[priority] ?? 0;
|
|
606
1398
|
return Math.max(minVit, retention);
|
|
607
1399
|
}
|
|
608
|
-
function runDecay(db) {
|
|
1400
|
+
function runDecay(db, opts) {
|
|
609
1401
|
const currentTime = now();
|
|
610
1402
|
const currentMs = new Date(currentTime).getTime();
|
|
611
|
-
const
|
|
1403
|
+
const agentId = opts?.agent_id;
|
|
1404
|
+
const query = agentId ? "SELECT id, priority, stability, created_at, last_accessed, vitality FROM memories WHERE priority > 0 AND agent_id = ?" : "SELECT id, priority, stability, created_at, last_accessed, vitality FROM memories WHERE priority > 0";
|
|
1405
|
+
const memories = db.prepare(query).all(...agentId ? [agentId] : []);
|
|
612
1406
|
let updated = 0;
|
|
613
1407
|
let decayed = 0;
|
|
614
1408
|
let belowThreshold = 0;
|
|
615
1409
|
const updateStmt = db.prepare("UPDATE memories SET vitality = ?, updated_at = ? WHERE id = ?");
|
|
616
1410
|
const transaction = db.transaction(() => {
|
|
617
1411
|
for (const mem of memories) {
|
|
618
|
-
const
|
|
619
|
-
const
|
|
1412
|
+
const referenceTime = mem.last_accessed ?? mem.created_at;
|
|
1413
|
+
const referenceMs = new Date(referenceTime).getTime();
|
|
1414
|
+
const daysSince = (currentMs - referenceMs) / (1e3 * 60 * 60 * 24);
|
|
620
1415
|
const newVitality = calculateVitality(mem.stability, daysSince, mem.priority);
|
|
621
1416
|
if (Math.abs(newVitality - mem.vitality) > 1e-3) {
|
|
622
1417
|
updateStmt.run(newVitality, currentTime, mem.id);
|
|
@@ -633,12 +1428,15 @@ function runDecay(db) {
|
|
|
633
1428
|
transaction();
|
|
634
1429
|
return { updated, decayed, belowThreshold };
|
|
635
1430
|
}
|
|
636
|
-
function getDecayedMemories(db, threshold = 0.05) {
|
|
1431
|
+
function getDecayedMemories(db, threshold = 0.05, opts) {
|
|
1432
|
+
const agentId = opts?.agent_id;
|
|
637
1433
|
return db.prepare(
|
|
638
|
-
`SELECT id, content, vitality, priority FROM memories
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
1434
|
+
agentId ? `SELECT id, content, vitality, priority FROM memories
|
|
1435
|
+
WHERE vitality < ? AND priority >= 3 AND agent_id = ?
|
|
1436
|
+
ORDER BY vitality ASC` : `SELECT id, content, vitality, priority FROM memories
|
|
1437
|
+
WHERE vitality < ? AND priority >= 3
|
|
1438
|
+
ORDER BY vitality ASC`
|
|
1439
|
+
).all(...agentId ? [threshold, agentId] : [threshold]);
|
|
642
1440
|
}
|
|
643
1441
|
|
|
644
1442
|
// src/sleep/sync.ts
|
|
@@ -698,11 +1496,12 @@ function syncBatch(db, inputs) {
|
|
|
698
1496
|
function runTidy(db, opts) {
|
|
699
1497
|
const threshold = opts?.vitalityThreshold ?? 0.05;
|
|
700
1498
|
const maxSnapshots = opts?.maxSnapshotsPerMemory ?? 10;
|
|
1499
|
+
const agentId = opts?.agent_id;
|
|
701
1500
|
let archived = 0;
|
|
702
1501
|
let orphansCleaned = 0;
|
|
703
1502
|
let snapshotsPruned = 0;
|
|
704
1503
|
const transaction = db.transaction(() => {
|
|
705
|
-
const decayed = getDecayedMemories(db, threshold);
|
|
1504
|
+
const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
|
|
706
1505
|
for (const mem of decayed) {
|
|
707
1506
|
try {
|
|
708
1507
|
createSnapshot(db, mem.id, "delete", "tidy");
|
|
@@ -711,13 +1510,23 @@ function runTidy(db, opts) {
|
|
|
711
1510
|
deleteMemory(db, mem.id);
|
|
712
1511
|
archived++;
|
|
713
1512
|
}
|
|
714
|
-
const orphans = db.prepare(
|
|
715
|
-
`DELETE FROM paths
|
|
1513
|
+
const orphans = agentId ? db.prepare(
|
|
1514
|
+
`DELETE FROM paths
|
|
1515
|
+
WHERE agent_id = ?
|
|
1516
|
+
AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
|
|
1517
|
+
).run(agentId, agentId) : db.prepare(
|
|
1518
|
+
"DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)"
|
|
716
1519
|
).run();
|
|
717
1520
|
orphansCleaned = orphans.changes;
|
|
718
|
-
const memoriesWithSnapshots = db.prepare(
|
|
1521
|
+
const memoriesWithSnapshots = agentId ? db.prepare(
|
|
1522
|
+
`SELECT s.memory_id, COUNT(*) as cnt
|
|
1523
|
+
FROM snapshots s
|
|
1524
|
+
JOIN memories m ON m.id = s.memory_id
|
|
1525
|
+
WHERE m.agent_id = ?
|
|
1526
|
+
GROUP BY s.memory_id HAVING cnt > ?`
|
|
1527
|
+
).all(agentId, maxSnapshots) : db.prepare(
|
|
719
1528
|
`SELECT memory_id, COUNT(*) as cnt FROM snapshots
|
|
720
|
-
|
|
1529
|
+
GROUP BY memory_id HAVING cnt > ?`
|
|
721
1530
|
).all(maxSnapshots);
|
|
722
1531
|
for (const { memory_id } of memoriesWithSnapshots) {
|
|
723
1532
|
const pruned = db.prepare(
|
|
@@ -734,20 +1543,31 @@ function runTidy(db, opts) {
|
|
|
734
1543
|
}
|
|
735
1544
|
|
|
736
1545
|
// src/sleep/govern.ts
|
|
737
|
-
function runGovern(db) {
|
|
1546
|
+
function runGovern(db, opts) {
|
|
1547
|
+
const agentId = opts?.agent_id;
|
|
738
1548
|
let orphanPaths = 0;
|
|
739
1549
|
let orphanLinks = 0;
|
|
740
1550
|
let emptyMemories = 0;
|
|
741
1551
|
const transaction = db.transaction(() => {
|
|
742
|
-
const pathResult = db.prepare(
|
|
1552
|
+
const pathResult = agentId ? db.prepare(
|
|
1553
|
+
`DELETE FROM paths
|
|
1554
|
+
WHERE agent_id = ?
|
|
1555
|
+
AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
|
|
1556
|
+
).run(agentId, agentId) : db.prepare("DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)").run();
|
|
743
1557
|
orphanPaths = pathResult.changes;
|
|
744
|
-
const linkResult = db.prepare(
|
|
1558
|
+
const linkResult = agentId ? db.prepare(
|
|
1559
|
+
`DELETE FROM links WHERE
|
|
1560
|
+
agent_id = ? AND (
|
|
1561
|
+
source_id NOT IN (SELECT id FROM memories WHERE agent_id = ?) OR
|
|
1562
|
+
target_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)
|
|
1563
|
+
)`
|
|
1564
|
+
).run(agentId, agentId, agentId) : db.prepare(
|
|
745
1565
|
`DELETE FROM links WHERE
|
|
746
1566
|
source_id NOT IN (SELECT id FROM memories) OR
|
|
747
1567
|
target_id NOT IN (SELECT id FROM memories)`
|
|
748
1568
|
).run();
|
|
749
1569
|
orphanLinks = linkResult.changes;
|
|
750
|
-
const emptyResult = db.prepare("DELETE FROM memories WHERE TRIM(content) = ''").run();
|
|
1570
|
+
const emptyResult = agentId ? db.prepare("DELETE FROM memories WHERE agent_id = ? AND TRIM(content) = ''").run(agentId) : db.prepare("DELETE FROM memories WHERE TRIM(content) = ''").run();
|
|
751
1571
|
emptyMemories = emptyResult.changes;
|
|
752
1572
|
});
|
|
753
1573
|
transaction();
|
|
@@ -771,7 +1591,7 @@ function boot(db, opts) {
|
|
|
771
1591
|
}
|
|
772
1592
|
const bootPaths = [];
|
|
773
1593
|
for (const uri of corePaths) {
|
|
774
|
-
const path = getPathByUri(db, uri);
|
|
1594
|
+
const path = getPathByUri(db, uri, agentId);
|
|
775
1595
|
if (path) {
|
|
776
1596
|
bootPaths.push(uri);
|
|
777
1597
|
if (!memories.has(path.memory_id)) {
|
|
@@ -783,13 +1603,13 @@ function boot(db, opts) {
|
|
|
783
1603
|
}
|
|
784
1604
|
}
|
|
785
1605
|
}
|
|
786
|
-
const bootEntry = getPathByUri(db, "system://boot");
|
|
1606
|
+
const bootEntry = getPathByUri(db, "system://boot", agentId);
|
|
787
1607
|
if (bootEntry) {
|
|
788
1608
|
const bootMem = getMemory(db, bootEntry.memory_id);
|
|
789
1609
|
if (bootMem) {
|
|
790
1610
|
const additionalUris = bootMem.content.split("\n").map((l) => l.trim()).filter((l) => l.match(/^[a-z]+:\/\//));
|
|
791
1611
|
for (const uri of additionalUris) {
|
|
792
|
-
const path = getPathByUri(db, uri);
|
|
1612
|
+
const path = getPathByUri(db, uri, agentId);
|
|
793
1613
|
if (path && !memories.has(path.memory_id)) {
|
|
794
1614
|
const mem = getMemory(db, path.memory_id);
|
|
795
1615
|
if (mem) {
|
|
@@ -811,14 +1631,25 @@ export {
|
|
|
811
1631
|
classifyIntent,
|
|
812
1632
|
contentHash,
|
|
813
1633
|
countMemories,
|
|
1634
|
+
createDashScopeProvider,
|
|
814
1635
|
createLink,
|
|
815
1636
|
createMemory,
|
|
1637
|
+
createOpenAIProvider,
|
|
1638
|
+
createOpenAIRerankProvider,
|
|
816
1639
|
createPath,
|
|
817
1640
|
createSnapshot,
|
|
1641
|
+
decodeEmbedding,
|
|
818
1642
|
deleteLink,
|
|
819
1643
|
deleteMemory,
|
|
820
1644
|
deletePath,
|
|
1645
|
+
embedMemory,
|
|
1646
|
+
embedMissingForAgent,
|
|
1647
|
+
encodeEmbedding,
|
|
1648
|
+
exportMemories,
|
|
821
1649
|
getDecayedMemories,
|
|
1650
|
+
getDefaultInstruction,
|
|
1651
|
+
getEmbedding,
|
|
1652
|
+
getEmbeddingProviderFromEnv,
|
|
822
1653
|
getLinks,
|
|
823
1654
|
getMemory,
|
|
824
1655
|
getOutgoingLinks,
|
|
@@ -827,23 +1658,30 @@ export {
|
|
|
827
1658
|
getPathsByDomain,
|
|
828
1659
|
getPathsByMemory,
|
|
829
1660
|
getPathsByPrefix,
|
|
1661
|
+
getRerankerProviderFromEnv,
|
|
830
1662
|
getSnapshot,
|
|
831
1663
|
getSnapshots,
|
|
832
1664
|
getStrategy,
|
|
833
1665
|
guard,
|
|
1666
|
+
isCountRow,
|
|
1667
|
+
listEmbeddings,
|
|
834
1668
|
listMemories,
|
|
835
1669
|
openDatabase,
|
|
836
1670
|
parseUri,
|
|
837
1671
|
recordAccess,
|
|
838
1672
|
rerank,
|
|
1673
|
+
rerankWithProvider,
|
|
839
1674
|
rollback,
|
|
840
1675
|
runDecay,
|
|
841
1676
|
runGovern,
|
|
842
1677
|
runTidy,
|
|
843
1678
|
searchBM25,
|
|
1679
|
+
searchHybrid,
|
|
844
1680
|
syncBatch,
|
|
845
1681
|
syncOne,
|
|
1682
|
+
tokenize,
|
|
846
1683
|
traverse,
|
|
847
|
-
updateMemory
|
|
1684
|
+
updateMemory,
|
|
1685
|
+
upsertEmbedding
|
|
848
1686
|
};
|
|
849
1687
|
//# sourceMappingURL=index.js.map
|