@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/mcp/server.js
CHANGED
|
@@ -20,13 +20,17 @@ function openDatabase(opts) {
|
|
|
20
20
|
db.pragma("foreign_keys = ON");
|
|
21
21
|
db.pragma("busy_timeout = 5000");
|
|
22
22
|
db.exec(SCHEMA_SQL);
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
const currentVersion = getSchemaVersion(db);
|
|
24
|
+
if (currentVersion === null) {
|
|
25
|
+
const inferred = inferSchemaVersion(db);
|
|
26
|
+
if (inferred < SCHEMA_VERSION) {
|
|
27
|
+
migrateDatabase(db, inferred, SCHEMA_VERSION);
|
|
28
|
+
}
|
|
29
|
+
db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(SCHEMA_VERSION));
|
|
30
|
+
} else if (currentVersion < SCHEMA_VERSION) {
|
|
31
|
+
migrateDatabase(db, currentVersion, SCHEMA_VERSION);
|
|
29
32
|
}
|
|
33
|
+
ensureIndexes(db);
|
|
30
34
|
return db;
|
|
31
35
|
}
|
|
32
36
|
function now() {
|
|
@@ -35,11 +39,177 @@ function now() {
|
|
|
35
39
|
function newId() {
|
|
36
40
|
return randomUUID();
|
|
37
41
|
}
|
|
42
|
+
function getSchemaVersion(db) {
|
|
43
|
+
try {
|
|
44
|
+
const row = db.prepare("SELECT value FROM schema_meta WHERE key = 'version'").get();
|
|
45
|
+
if (!row) return null;
|
|
46
|
+
const n = Number.parseInt(row.value, 10);
|
|
47
|
+
return Number.isFinite(n) ? n : null;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function tableHasColumn(db, table, column) {
|
|
53
|
+
try {
|
|
54
|
+
const cols = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
55
|
+
return cols.some((c) => c.name === column);
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function migrateDatabase(db, from, to) {
|
|
61
|
+
let v = from;
|
|
62
|
+
while (v < to) {
|
|
63
|
+
if (v === 1) {
|
|
64
|
+
migrateV1ToV2(db);
|
|
65
|
+
v = 2;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (v === 2) {
|
|
69
|
+
migrateV2ToV3(db);
|
|
70
|
+
v = 3;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
throw new Error(`Unsupported schema migration path: v${from} \u2192 v${to} (stuck at v${v})`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function migrateV1ToV2(db) {
|
|
77
|
+
const pathsMigrated = tableHasColumn(db, "paths", "agent_id");
|
|
78
|
+
const linksMigrated = tableHasColumn(db, "links", "agent_id");
|
|
79
|
+
const alreadyMigrated = pathsMigrated && linksMigrated;
|
|
80
|
+
if (alreadyMigrated) {
|
|
81
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(2));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
db.pragma("foreign_keys = OFF");
|
|
85
|
+
try {
|
|
86
|
+
db.exec("BEGIN");
|
|
87
|
+
if (!pathsMigrated) {
|
|
88
|
+
db.exec(`
|
|
89
|
+
CREATE TABLE IF NOT EXISTS paths_v2 (
|
|
90
|
+
id TEXT PRIMARY KEY,
|
|
91
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
92
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
93
|
+
uri TEXT NOT NULL,
|
|
94
|
+
alias TEXT,
|
|
95
|
+
domain TEXT NOT NULL,
|
|
96
|
+
created_at TEXT NOT NULL,
|
|
97
|
+
UNIQUE(agent_id, uri)
|
|
98
|
+
);
|
|
99
|
+
`);
|
|
100
|
+
db.exec(`
|
|
101
|
+
INSERT INTO paths_v2 (id, memory_id, agent_id, uri, alias, domain, created_at)
|
|
102
|
+
SELECT p.id, p.memory_id, COALESCE(m.agent_id, 'default'), p.uri, p.alias, p.domain, p.created_at
|
|
103
|
+
FROM paths p
|
|
104
|
+
LEFT JOIN memories m ON m.id = p.memory_id;
|
|
105
|
+
`);
|
|
106
|
+
db.exec("DROP TABLE paths;");
|
|
107
|
+
db.exec("ALTER TABLE paths_v2 RENAME TO paths;");
|
|
108
|
+
}
|
|
109
|
+
if (!linksMigrated) {
|
|
110
|
+
db.exec(`
|
|
111
|
+
CREATE TABLE IF NOT EXISTS links_v2 (
|
|
112
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
113
|
+
source_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
114
|
+
target_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
115
|
+
relation TEXT NOT NULL,
|
|
116
|
+
weight REAL NOT NULL DEFAULT 1.0,
|
|
117
|
+
created_at TEXT NOT NULL,
|
|
118
|
+
PRIMARY KEY (agent_id, source_id, target_id)
|
|
119
|
+
);
|
|
120
|
+
`);
|
|
121
|
+
db.exec(`
|
|
122
|
+
INSERT INTO links_v2 (agent_id, source_id, target_id, relation, weight, created_at)
|
|
123
|
+
SELECT COALESCE(ms.agent_id, 'default'), l.source_id, l.target_id, l.relation, l.weight, l.created_at
|
|
124
|
+
FROM links l
|
|
125
|
+
LEFT JOIN memories ms ON ms.id = l.source_id;
|
|
126
|
+
`);
|
|
127
|
+
db.exec(`
|
|
128
|
+
DELETE FROM links_v2
|
|
129
|
+
WHERE EXISTS (SELECT 1 FROM memories s WHERE s.id = links_v2.source_id AND s.agent_id != links_v2.agent_id)
|
|
130
|
+
OR EXISTS (SELECT 1 FROM memories t WHERE t.id = links_v2.target_id AND t.agent_id != links_v2.agent_id);
|
|
131
|
+
`);
|
|
132
|
+
db.exec("DROP TABLE links;");
|
|
133
|
+
db.exec("ALTER TABLE links_v2 RENAME TO links;");
|
|
134
|
+
}
|
|
135
|
+
db.exec(`
|
|
136
|
+
CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
|
|
137
|
+
CREATE INDEX IF NOT EXISTS idx_paths_domain ON paths(domain);
|
|
138
|
+
`);
|
|
139
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(2));
|
|
140
|
+
db.exec("COMMIT");
|
|
141
|
+
} catch (e) {
|
|
142
|
+
try {
|
|
143
|
+
db.exec("ROLLBACK");
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
throw e;
|
|
147
|
+
} finally {
|
|
148
|
+
db.pragma("foreign_keys = ON");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function inferSchemaVersion(db) {
|
|
152
|
+
const hasAgentScopedPaths = tableHasColumn(db, "paths", "agent_id");
|
|
153
|
+
const hasAgentScopedLinks = tableHasColumn(db, "links", "agent_id");
|
|
154
|
+
const hasEmbeddings = (() => {
|
|
155
|
+
try {
|
|
156
|
+
const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'").get();
|
|
157
|
+
return Boolean(row);
|
|
158
|
+
} catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
})();
|
|
162
|
+
if (hasAgentScopedPaths && hasAgentScopedLinks && hasEmbeddings) return 3;
|
|
163
|
+
if (hasAgentScopedPaths && hasAgentScopedLinks) return 2;
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
function ensureIndexes(db) {
|
|
167
|
+
if (tableHasColumn(db, "paths", "agent_id")) {
|
|
168
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_paths_agent_uri ON paths(agent_id, uri);");
|
|
169
|
+
}
|
|
170
|
+
if (tableHasColumn(db, "links", "agent_id")) {
|
|
171
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_links_agent_source ON links(agent_id, source_id);");
|
|
172
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_links_agent_target ON links(agent_id, target_id);");
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'").get();
|
|
176
|
+
if (row) {
|
|
177
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_agent_model ON embeddings(agent_id, model);");
|
|
178
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_memory ON embeddings(memory_id);");
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function migrateV2ToV3(db) {
|
|
184
|
+
try {
|
|
185
|
+
db.exec("BEGIN");
|
|
186
|
+
db.exec(`
|
|
187
|
+
CREATE TABLE IF NOT EXISTS embeddings (
|
|
188
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
189
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
190
|
+
model TEXT NOT NULL,
|
|
191
|
+
dim INTEGER NOT NULL,
|
|
192
|
+
vector BLOB NOT NULL,
|
|
193
|
+
created_at TEXT NOT NULL,
|
|
194
|
+
updated_at TEXT NOT NULL,
|
|
195
|
+
PRIMARY KEY (agent_id, memory_id, model)
|
|
196
|
+
);
|
|
197
|
+
`);
|
|
198
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(3));
|
|
199
|
+
db.exec("COMMIT");
|
|
200
|
+
} catch (e) {
|
|
201
|
+
try {
|
|
202
|
+
db.exec("ROLLBACK");
|
|
203
|
+
} catch {
|
|
204
|
+
}
|
|
205
|
+
throw e;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
38
208
|
var SCHEMA_VERSION, SCHEMA_SQL;
|
|
39
209
|
var init_db = __esm({
|
|
40
210
|
"src/core/db.ts"() {
|
|
41
211
|
"use strict";
|
|
42
|
-
SCHEMA_VERSION =
|
|
212
|
+
SCHEMA_VERSION = 3;
|
|
43
213
|
SCHEMA_SQL = `
|
|
44
214
|
-- Memory entries
|
|
45
215
|
CREATE TABLE IF NOT EXISTS memories (
|
|
@@ -64,20 +234,23 @@ CREATE TABLE IF NOT EXISTS memories (
|
|
|
64
234
|
CREATE TABLE IF NOT EXISTS paths (
|
|
65
235
|
id TEXT PRIMARY KEY,
|
|
66
236
|
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
67
|
-
|
|
237
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
238
|
+
uri TEXT NOT NULL,
|
|
68
239
|
alias TEXT,
|
|
69
240
|
domain TEXT NOT NULL,
|
|
70
|
-
created_at TEXT NOT NULL
|
|
241
|
+
created_at TEXT NOT NULL,
|
|
242
|
+
UNIQUE(agent_id, uri)
|
|
71
243
|
);
|
|
72
244
|
|
|
73
245
|
-- Association network (knowledge graph)
|
|
74
246
|
CREATE TABLE IF NOT EXISTS links (
|
|
247
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
75
248
|
source_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
76
249
|
target_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
77
250
|
relation TEXT NOT NULL,
|
|
78
251
|
weight REAL NOT NULL DEFAULT 1.0,
|
|
79
252
|
created_at TEXT NOT NULL,
|
|
80
|
-
PRIMARY KEY (source_id, target_id)
|
|
253
|
+
PRIMARY KEY (agent_id, source_id, target_id)
|
|
81
254
|
);
|
|
82
255
|
|
|
83
256
|
-- Snapshots (version control, from nocturne + Memory Palace)
|
|
@@ -97,6 +270,18 @@ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
|
97
270
|
tokenize='unicode61'
|
|
98
271
|
);
|
|
99
272
|
|
|
273
|
+
-- Embeddings (optional semantic layer)
|
|
274
|
+
CREATE TABLE IF NOT EXISTS embeddings (
|
|
275
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
276
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
277
|
+
model TEXT NOT NULL,
|
|
278
|
+
dim INTEGER NOT NULL,
|
|
279
|
+
vector BLOB NOT NULL,
|
|
280
|
+
created_at TEXT NOT NULL,
|
|
281
|
+
updated_at TEXT NOT NULL,
|
|
282
|
+
PRIMARY KEY (agent_id, memory_id, model)
|
|
283
|
+
);
|
|
284
|
+
|
|
100
285
|
-- Schema version tracking
|
|
101
286
|
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
102
287
|
key TEXT PRIMARY KEY,
|
|
@@ -111,12 +296,94 @@ CREATE INDEX IF NOT EXISTS idx_memories_vitality ON memories(vitality);
|
|
|
111
296
|
CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(hash);
|
|
112
297
|
CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
|
|
113
298
|
CREATE INDEX IF NOT EXISTS idx_paths_domain ON paths(domain);
|
|
114
|
-
CREATE INDEX IF NOT EXISTS idx_links_source ON links(source_id);
|
|
115
|
-
CREATE INDEX IF NOT EXISTS idx_links_target ON links(target_id);
|
|
116
299
|
`;
|
|
117
300
|
}
|
|
118
301
|
});
|
|
119
302
|
|
|
303
|
+
// src/search/tokenizer.ts
|
|
304
|
+
import { readFileSync } from "fs";
|
|
305
|
+
import { createRequire } from "module";
|
|
306
|
+
function getJieba() {
|
|
307
|
+
if (_jieba !== void 0) return _jieba;
|
|
308
|
+
try {
|
|
309
|
+
const req = createRequire(import.meta.url);
|
|
310
|
+
const { Jieba } = req("@node-rs/jieba");
|
|
311
|
+
const dictPath = req.resolve("@node-rs/jieba/dict.txt");
|
|
312
|
+
const dictBuf = readFileSync(dictPath);
|
|
313
|
+
_jieba = Jieba.withDict(dictBuf);
|
|
314
|
+
} catch {
|
|
315
|
+
_jieba = null;
|
|
316
|
+
}
|
|
317
|
+
return _jieba;
|
|
318
|
+
}
|
|
319
|
+
function tokenize(text) {
|
|
320
|
+
const cleaned = text.replace(/[^\w\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af\s]/g, " ");
|
|
321
|
+
const tokens = [];
|
|
322
|
+
const latinWords = cleaned.replace(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/g, " ").split(/\s+/).filter((w) => w.length > 1);
|
|
323
|
+
tokens.push(...latinWords);
|
|
324
|
+
const cjkChunks = cleaned.match(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]+/g);
|
|
325
|
+
if (cjkChunks && cjkChunks.length > 0) {
|
|
326
|
+
const jieba = getJieba();
|
|
327
|
+
for (const chunk of cjkChunks) {
|
|
328
|
+
if (jieba) {
|
|
329
|
+
const words = jieba.cutForSearch(chunk).filter((w) => w.length >= 1);
|
|
330
|
+
tokens.push(...words);
|
|
331
|
+
} else {
|
|
332
|
+
for (const ch of chunk) {
|
|
333
|
+
tokens.push(ch);
|
|
334
|
+
}
|
|
335
|
+
for (let i = 0; i < chunk.length - 1; i++) {
|
|
336
|
+
tokens.push(chunk[i] + chunk[i + 1]);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const unique = [...new Set(tokens)].filter((t) => t.length > 0 && !STOPWORDS.has(t)).slice(0, 30);
|
|
342
|
+
return unique;
|
|
343
|
+
}
|
|
344
|
+
function tokenizeForIndex(text) {
|
|
345
|
+
const tokens = tokenize(text);
|
|
346
|
+
return tokens.join(" ");
|
|
347
|
+
}
|
|
348
|
+
var _jieba, STOPWORDS;
|
|
349
|
+
var init_tokenizer = __esm({
|
|
350
|
+
"src/search/tokenizer.ts"() {
|
|
351
|
+
"use strict";
|
|
352
|
+
STOPWORDS = /* @__PURE__ */ new Set([
|
|
353
|
+
"\u7684",
|
|
354
|
+
"\u4E86",
|
|
355
|
+
"\u5728",
|
|
356
|
+
"\u662F",
|
|
357
|
+
"\u6211",
|
|
358
|
+
"\u6709",
|
|
359
|
+
"\u548C",
|
|
360
|
+
"\u5C31",
|
|
361
|
+
"\u4E0D",
|
|
362
|
+
"\u4EBA",
|
|
363
|
+
"\u90FD",
|
|
364
|
+
"\u4E00",
|
|
365
|
+
"\u4E2A",
|
|
366
|
+
"\u4E0A",
|
|
367
|
+
"\u4E5F",
|
|
368
|
+
"\u5230",
|
|
369
|
+
"\u4ED6",
|
|
370
|
+
"\u6CA1",
|
|
371
|
+
"\u8FD9",
|
|
372
|
+
"\u8981",
|
|
373
|
+
"\u4F1A",
|
|
374
|
+
"\u5BF9",
|
|
375
|
+
"\u8BF4",
|
|
376
|
+
"\u800C",
|
|
377
|
+
"\u53BB",
|
|
378
|
+
"\u4E4B",
|
|
379
|
+
"\u88AB",
|
|
380
|
+
"\u5979",
|
|
381
|
+
"\u628A",
|
|
382
|
+
"\u90A3"
|
|
383
|
+
]);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
120
387
|
// src/core/memory.ts
|
|
121
388
|
var memory_exports = {};
|
|
122
389
|
__export(memory_exports, {
|
|
@@ -161,7 +428,7 @@ function createMemory(db, input) {
|
|
|
161
428
|
agentId,
|
|
162
429
|
hash2
|
|
163
430
|
);
|
|
164
|
-
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, input.content);
|
|
431
|
+
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
|
|
165
432
|
return getMemory(db, id);
|
|
166
433
|
}
|
|
167
434
|
function getMemory(db, id) {
|
|
@@ -206,7 +473,7 @@ function updateMemory(db, id, input) {
|
|
|
206
473
|
db.prepare(`UPDATE memories SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
207
474
|
if (input.content !== void 0) {
|
|
208
475
|
db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
|
|
209
|
-
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, input.content);
|
|
476
|
+
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
|
|
210
477
|
}
|
|
211
478
|
return getMemory(db, id);
|
|
212
479
|
}
|
|
@@ -263,6 +530,7 @@ var init_memory = __esm({
|
|
|
263
530
|
"src/core/memory.ts"() {
|
|
264
531
|
"use strict";
|
|
265
532
|
init_db();
|
|
533
|
+
init_tokenizer();
|
|
266
534
|
TYPE_PRIORITY = {
|
|
267
535
|
identity: 0,
|
|
268
536
|
emotion: 1,
|
|
@@ -14066,45 +14334,60 @@ function parseUri(uri) {
|
|
|
14066
14334
|
if (!match) throw new Error(`Invalid URI: ${uri}. Expected format: domain://path`);
|
|
14067
14335
|
return { domain: match[1], path: match[2] };
|
|
14068
14336
|
}
|
|
14069
|
-
function createPath(db, memoryId, uri, alias, validDomains) {
|
|
14337
|
+
function createPath(db, memoryId, uri, alias, validDomains, agent_id) {
|
|
14070
14338
|
const { domain: domain2 } = parseUri(uri);
|
|
14071
14339
|
const domains = validDomains ?? DEFAULT_DOMAINS;
|
|
14072
14340
|
if (!domains.has(domain2)) {
|
|
14073
14341
|
throw new Error(`Invalid domain "${domain2}". Valid: ${[...domains].join(", ")}`);
|
|
14074
14342
|
}
|
|
14075
|
-
const
|
|
14343
|
+
const memoryAgent = db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(memoryId)?.agent_id;
|
|
14344
|
+
if (!memoryAgent) throw new Error(`Memory not found: ${memoryId}`);
|
|
14345
|
+
if (agent_id && agent_id !== memoryAgent) {
|
|
14346
|
+
throw new Error(`Agent mismatch for path: memory agent_id=${memoryAgent}, requested agent_id=${agent_id}`);
|
|
14347
|
+
}
|
|
14348
|
+
const agentId = agent_id ?? memoryAgent;
|
|
14349
|
+
const existing = db.prepare("SELECT id FROM paths WHERE agent_id = ? AND uri = ?").get(agentId, uri);
|
|
14076
14350
|
if (existing) {
|
|
14077
14351
|
throw new Error(`URI already exists: ${uri}`);
|
|
14078
14352
|
}
|
|
14079
14353
|
const id = newId();
|
|
14080
14354
|
db.prepare(
|
|
14081
|
-
"INSERT INTO paths (id, memory_id, uri, alias, domain, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
14082
|
-
).run(id, memoryId, uri, alias ?? null, domain2, now());
|
|
14355
|
+
"INSERT INTO paths (id, memory_id, agent_id, uri, alias, domain, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
14356
|
+
).run(id, memoryId, agentId, uri, alias ?? null, domain2, now());
|
|
14083
14357
|
return getPath(db, id);
|
|
14084
14358
|
}
|
|
14085
14359
|
function getPath(db, id) {
|
|
14086
14360
|
return db.prepare("SELECT * FROM paths WHERE id = ?").get(id) ?? null;
|
|
14087
14361
|
}
|
|
14088
|
-
function getPathByUri(db, uri) {
|
|
14089
|
-
return db.prepare("SELECT * FROM paths WHERE uri = ?").get(uri) ?? null;
|
|
14362
|
+
function getPathByUri(db, uri, agent_id = "default") {
|
|
14363
|
+
return db.prepare("SELECT * FROM paths WHERE agent_id = ? AND uri = ?").get(agent_id, uri) ?? null;
|
|
14090
14364
|
}
|
|
14091
|
-
function getPathsByPrefix(db, prefix) {
|
|
14092
|
-
return db.prepare("SELECT * FROM paths WHERE uri LIKE ? ORDER BY uri").all(`${prefix}%`);
|
|
14365
|
+
function getPathsByPrefix(db, prefix, agent_id = "default") {
|
|
14366
|
+
return db.prepare("SELECT * FROM paths WHERE agent_id = ? AND uri LIKE ? ORDER BY uri").all(agent_id, `${prefix}%`);
|
|
14093
14367
|
}
|
|
14094
14368
|
|
|
14095
14369
|
// src/core/link.ts
|
|
14096
14370
|
init_db();
|
|
14097
|
-
function createLink(db, sourceId, targetId, relation, weight = 1) {
|
|
14371
|
+
function createLink(db, sourceId, targetId, relation, weight = 1, agent_id) {
|
|
14372
|
+
const sourceAgent = db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(sourceId)?.agent_id;
|
|
14373
|
+
const targetAgent = db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(targetId)?.agent_id;
|
|
14374
|
+
if (!sourceAgent) throw new Error(`Source memory not found: ${sourceId}`);
|
|
14375
|
+
if (!targetAgent) throw new Error(`Target memory not found: ${targetId}`);
|
|
14376
|
+
if (sourceAgent !== targetAgent) throw new Error("Cross-agent links are not allowed");
|
|
14377
|
+
if (agent_id && agent_id !== sourceAgent) throw new Error("Agent mismatch for link");
|
|
14378
|
+
const agentId = agent_id ?? sourceAgent;
|
|
14098
14379
|
db.prepare(
|
|
14099
|
-
`INSERT OR REPLACE INTO links (source_id, target_id, relation, weight, created_at)
|
|
14100
|
-
VALUES (?, ?, ?, ?, ?)`
|
|
14101
|
-
).run(sourceId, targetId, relation, weight, now());
|
|
14102
|
-
return { source_id: sourceId, target_id: targetId, relation, weight, created_at: now() };
|
|
14380
|
+
`INSERT OR REPLACE INTO links (agent_id, source_id, target_id, relation, weight, created_at)
|
|
14381
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
14382
|
+
).run(agentId, sourceId, targetId, relation, weight, now());
|
|
14383
|
+
return { agent_id: agentId, source_id: sourceId, target_id: targetId, relation, weight, created_at: now() };
|
|
14103
14384
|
}
|
|
14104
|
-
function getLinks(db, memoryId) {
|
|
14105
|
-
|
|
14385
|
+
function getLinks(db, memoryId, agent_id) {
|
|
14386
|
+
const agentId = agent_id ?? db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(memoryId)?.agent_id ?? "default";
|
|
14387
|
+
return db.prepare("SELECT * FROM links WHERE agent_id = ? AND (source_id = ? OR target_id = ?)").all(agentId, memoryId, memoryId);
|
|
14106
14388
|
}
|
|
14107
|
-
function traverse(db, startId, maxHops = 2) {
|
|
14389
|
+
function traverse(db, startId, maxHops = 2, agent_id) {
|
|
14390
|
+
const agentId = agent_id ?? db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(startId)?.agent_id ?? "default";
|
|
14108
14391
|
const visited = /* @__PURE__ */ new Set();
|
|
14109
14392
|
const results = [];
|
|
14110
14393
|
const queue = [
|
|
@@ -14118,7 +14401,7 @@ function traverse(db, startId, maxHops = 2) {
|
|
|
14118
14401
|
results.push(current);
|
|
14119
14402
|
}
|
|
14120
14403
|
if (current.hop < maxHops) {
|
|
14121
|
-
const links = db.prepare("SELECT target_id, relation FROM links WHERE source_id = ?").all(current.id);
|
|
14404
|
+
const links = db.prepare("SELECT target_id, relation FROM links WHERE agent_id = ? AND source_id = ?").all(agentId, current.id);
|
|
14122
14405
|
for (const link of links) {
|
|
14123
14406
|
if (!visited.has(link.target_id)) {
|
|
14124
14407
|
queue.push({
|
|
@@ -14128,7 +14411,7 @@ function traverse(db, startId, maxHops = 2) {
|
|
|
14128
14411
|
});
|
|
14129
14412
|
}
|
|
14130
14413
|
}
|
|
14131
|
-
const reverseLinks = db.prepare("SELECT source_id, relation FROM links WHERE target_id = ?").all(current.id);
|
|
14414
|
+
const reverseLinks = db.prepare("SELECT source_id, relation FROM links WHERE agent_id = ? AND target_id = ?").all(agentId, current.id);
|
|
14132
14415
|
for (const link of reverseLinks) {
|
|
14133
14416
|
if (!visited.has(link.source_id)) {
|
|
14134
14417
|
queue.push({
|
|
@@ -14145,6 +14428,7 @@ function traverse(db, startId, maxHops = 2) {
|
|
|
14145
14428
|
|
|
14146
14429
|
// src/core/snapshot.ts
|
|
14147
14430
|
init_db();
|
|
14431
|
+
init_tokenizer();
|
|
14148
14432
|
function createSnapshot(db, memoryId, action, changedBy) {
|
|
14149
14433
|
const memory = db.prepare("SELECT content FROM memories WHERE id = ?").get(memoryId);
|
|
14150
14434
|
if (!memory) throw new Error(`Memory not found: ${memoryId}`);
|
|
@@ -14173,100 +14457,77 @@ function rollback(db, snapshotId) {
|
|
|
14173
14457
|
db.prepare("DELETE FROM memories_fts WHERE id = ?").run(snapshot.memory_id);
|
|
14174
14458
|
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(
|
|
14175
14459
|
snapshot.memory_id,
|
|
14176
|
-
snapshot.content
|
|
14460
|
+
tokenizeForIndex(snapshot.content)
|
|
14177
14461
|
);
|
|
14178
14462
|
return true;
|
|
14179
14463
|
}
|
|
14180
14464
|
|
|
14181
|
-
// src/search/bm25.ts
|
|
14182
|
-
function searchBM25(db, query, opts) {
|
|
14183
|
-
const limit = opts?.limit ?? 20;
|
|
14184
|
-
const agentId = opts?.agent_id ?? "default";
|
|
14185
|
-
const minVitality = opts?.min_vitality ?? 0;
|
|
14186
|
-
const ftsQuery = buildFtsQuery(query);
|
|
14187
|
-
if (!ftsQuery) return [];
|
|
14188
|
-
try {
|
|
14189
|
-
const rows = db.prepare(
|
|
14190
|
-
`SELECT m.*, rank AS score
|
|
14191
|
-
FROM memories_fts f
|
|
14192
|
-
JOIN memories m ON m.id = f.id
|
|
14193
|
-
WHERE memories_fts MATCH ?
|
|
14194
|
-
AND m.agent_id = ?
|
|
14195
|
-
AND m.vitality >= ?
|
|
14196
|
-
ORDER BY rank
|
|
14197
|
-
LIMIT ?`
|
|
14198
|
-
).all(ftsQuery, agentId, minVitality, limit);
|
|
14199
|
-
return rows.map((row) => ({
|
|
14200
|
-
memory: { ...row, score: void 0 },
|
|
14201
|
-
score: Math.abs(row.score),
|
|
14202
|
-
// FTS5 rank is negative (lower = better)
|
|
14203
|
-
matchReason: "bm25"
|
|
14204
|
-
}));
|
|
14205
|
-
} catch {
|
|
14206
|
-
return searchSimple(db, query, agentId, minVitality, limit);
|
|
14207
|
-
}
|
|
14208
|
-
}
|
|
14209
|
-
function searchSimple(db, query, agentId, minVitality, limit) {
|
|
14210
|
-
const rows = db.prepare(
|
|
14211
|
-
`SELECT * FROM memories
|
|
14212
|
-
WHERE agent_id = ? AND vitality >= ? AND content LIKE ?
|
|
14213
|
-
ORDER BY priority ASC, updated_at DESC
|
|
14214
|
-
LIMIT ?`
|
|
14215
|
-
).all(agentId, minVitality, `%${query}%`, limit);
|
|
14216
|
-
return rows.map((m, i) => ({
|
|
14217
|
-
memory: m,
|
|
14218
|
-
score: 1 / (i + 1),
|
|
14219
|
-
// Simple rank by position
|
|
14220
|
-
matchReason: "like"
|
|
14221
|
-
}));
|
|
14222
|
-
}
|
|
14223
|
-
function buildFtsQuery(text) {
|
|
14224
|
-
const words = text.replace(/[^\w\u4e00-\u9fff\u3040-\u30ff\s]/g, " ").split(/\s+/).filter((w) => w.length > 1).slice(0, 10);
|
|
14225
|
-
if (words.length === 0) return null;
|
|
14226
|
-
return words.map((w) => `"${w}"`).join(" OR ");
|
|
14227
|
-
}
|
|
14228
|
-
|
|
14229
14465
|
// src/search/intent.ts
|
|
14466
|
+
init_tokenizer();
|
|
14230
14467
|
var INTENT_PATTERNS = {
|
|
14231
14468
|
factual: [
|
|
14232
|
-
|
|
14233
|
-
|
|
14234
|
-
|
|
14235
|
-
|
|
14236
|
-
|
|
14237
|
-
|
|
14238
|
-
|
|
14239
|
-
|
|
14240
|
-
|
|
14469
|
+
// English
|
|
14470
|
+
/^(what|who|where|which|how much|how many)\b/i,
|
|
14471
|
+
/\b(name|address|number|password|config|setting)\b/i,
|
|
14472
|
+
// Chinese - questions about facts
|
|
14473
|
+
/是(什么|谁|哪|啥)/,
|
|
14474
|
+
/叫(什么|啥)/,
|
|
14475
|
+
/(名字|地址|号码|密码|配置|设置|账号|邮箱|链接|版本)/,
|
|
14476
|
+
/(多少|几个|哪个|哪些|哪里)/,
|
|
14477
|
+
// Chinese - lookup patterns
|
|
14478
|
+
/(查一下|找一下|看看|搜一下)/,
|
|
14479
|
+
/(.+)是什么$/
|
|
14241
14480
|
],
|
|
14242
14481
|
temporal: [
|
|
14243
|
-
|
|
14244
|
-
|
|
14482
|
+
// English
|
|
14483
|
+
/^(when|what time|how long)\b/i,
|
|
14484
|
+
/\b(yesterday|today|tomorrow|last week|recently|ago|before|after)\b/i,
|
|
14485
|
+
/\b(first|latest|newest|oldest|previous|next)\b/i,
|
|
14486
|
+
// Chinese - time expressions
|
|
14245
14487
|
/什么时候/,
|
|
14246
|
-
/(
|
|
14247
|
-
|
|
14248
|
-
/(
|
|
14488
|
+
/(昨天|今天|明天|上周|下周|最近|以前|之前|之后|刚才|刚刚)/,
|
|
14489
|
+
/(几月|几号|几点|多久|多长时间)/,
|
|
14490
|
+
/(上次|下次|第一次|最后一次|那天|那时)/,
|
|
14491
|
+
// Date patterns
|
|
14492
|
+
/\d{4}[-/.]\d{1,2}/,
|
|
14493
|
+
/\d{1,2}月\d{1,2}[日号]/,
|
|
14494
|
+
// Chinese - temporal context
|
|
14495
|
+
/(历史|记录|日志|以来|至今|期间)/
|
|
14249
14496
|
],
|
|
14250
14497
|
causal: [
|
|
14251
|
-
|
|
14252
|
-
/^(
|
|
14253
|
-
|
|
14254
|
-
|
|
14255
|
-
|
|
14256
|
-
|
|
14257
|
-
|
|
14498
|
+
// English
|
|
14499
|
+
/^(why|how come|what caused)\b/i,
|
|
14500
|
+
/\b(because|due to|reason|cause|result)\b/i,
|
|
14501
|
+
// Chinese - causal questions
|
|
14502
|
+
/为(什么|啥|何)/,
|
|
14503
|
+
/(原因|导致|造成|引起|因为|所以|结果)/,
|
|
14504
|
+
/(怎么回事|怎么了|咋回事|咋了)/,
|
|
14505
|
+
/(为啥|凭啥|凭什么)/,
|
|
14506
|
+
// Chinese - problem/diagnosis
|
|
14507
|
+
/(出(了|了什么)?问题|报错|失败|出错|bug)/
|
|
14258
14508
|
],
|
|
14259
14509
|
exploratory: [
|
|
14260
|
-
|
|
14261
|
-
/^(
|
|
14262
|
-
|
|
14263
|
-
|
|
14264
|
-
|
|
14265
|
-
|
|
14266
|
-
|
|
14267
|
-
|
|
14510
|
+
// English
|
|
14511
|
+
/^(how|tell me about|explain|describe|show me)\b/i,
|
|
14512
|
+
/^(what do you think|what about|any)\b/i,
|
|
14513
|
+
/\b(overview|summary|list|compare)\b/i,
|
|
14514
|
+
// Chinese - exploratory
|
|
14515
|
+
/(怎么样|怎样|如何)/,
|
|
14516
|
+
/(介绍|说说|讲讲|聊聊|谈谈)/,
|
|
14517
|
+
/(有哪些|有什么|有没有)/,
|
|
14518
|
+
/(关于|对于|至于|关联)/,
|
|
14519
|
+
/(总结|概括|梳理|回顾|盘点)/,
|
|
14520
|
+
// Chinese - opinion/analysis
|
|
14521
|
+
/(看法|想法|意见|建议|评价|感觉|觉得)/,
|
|
14522
|
+
/(对比|比较|区别|差异|优缺点)/
|
|
14268
14523
|
]
|
|
14269
14524
|
};
|
|
14525
|
+
var CN_STRUCTURE_BOOSTS = {
|
|
14526
|
+
factual: [/^.{1,6}(是什么|叫什么|在哪)/, /^(谁|哪)/],
|
|
14527
|
+
temporal: [/^(什么时候|上次|最近)/, /(时间|日期)$/],
|
|
14528
|
+
causal: [/^(为什么|为啥)/, /(为什么|怎么回事)$/],
|
|
14529
|
+
exploratory: [/^(怎么|如何|说说)/, /(哪些|什么样)$/]
|
|
14530
|
+
};
|
|
14270
14531
|
function classifyIntent(query) {
|
|
14271
14532
|
const scores = {
|
|
14272
14533
|
factual: 0,
|
|
@@ -14281,6 +14542,18 @@ function classifyIntent(query) {
|
|
|
14281
14542
|
}
|
|
14282
14543
|
}
|
|
14283
14544
|
}
|
|
14545
|
+
for (const [intent, patterns] of Object.entries(CN_STRUCTURE_BOOSTS)) {
|
|
14546
|
+
for (const pattern of patterns) {
|
|
14547
|
+
if (pattern.test(query)) {
|
|
14548
|
+
scores[intent] += 0.5;
|
|
14549
|
+
}
|
|
14550
|
+
}
|
|
14551
|
+
}
|
|
14552
|
+
const tokens = tokenize(query);
|
|
14553
|
+
const totalPatternScore = Object.values(scores).reduce((a, b) => a + b, 0);
|
|
14554
|
+
if (totalPatternScore === 0 && tokens.length <= 3) {
|
|
14555
|
+
scores.factual += 1;
|
|
14556
|
+
}
|
|
14284
14557
|
let maxIntent = "factual";
|
|
14285
14558
|
let maxScore = 0;
|
|
14286
14559
|
for (const [intent, score] of Object.entries(scores)) {
|
|
@@ -14290,7 +14563,7 @@ function classifyIntent(query) {
|
|
|
14290
14563
|
}
|
|
14291
14564
|
}
|
|
14292
14565
|
const totalScore = Object.values(scores).reduce((a, b) => a + b, 0);
|
|
14293
|
-
const confidence = totalScore > 0 ? maxScore / totalScore : 0.5;
|
|
14566
|
+
const confidence = totalScore > 0 ? Math.min(0.95, maxScore / totalScore) : 0.5;
|
|
14294
14567
|
return { intent: maxIntent, confidence };
|
|
14295
14568
|
}
|
|
14296
14569
|
function getStrategy(intent) {
|
|
@@ -14307,6 +14580,26 @@ function getStrategy(intent) {
|
|
|
14307
14580
|
}
|
|
14308
14581
|
|
|
14309
14582
|
// src/search/rerank.ts
|
|
14583
|
+
async function rerankWithProvider(results, query, provider) {
|
|
14584
|
+
if (results.length === 0) return results;
|
|
14585
|
+
const documents = results.map((r) => r.memory.content);
|
|
14586
|
+
try {
|
|
14587
|
+
const apiResults = await provider.rerank(query, documents);
|
|
14588
|
+
const scoreMap = new Map(apiResults.map((r) => [r.index, r.relevance_score]));
|
|
14589
|
+
return results.map((r, i) => {
|
|
14590
|
+
const score = scoreMap.get(i);
|
|
14591
|
+
if (score === void 0) return r;
|
|
14592
|
+
return {
|
|
14593
|
+
...r,
|
|
14594
|
+
score,
|
|
14595
|
+
matchReason: `${r.matchReason}+rerank`
|
|
14596
|
+
};
|
|
14597
|
+
});
|
|
14598
|
+
} catch (err) {
|
|
14599
|
+
console.warn("[agent-memory] External rerank failed, falling back:", err);
|
|
14600
|
+
return results;
|
|
14601
|
+
}
|
|
14602
|
+
}
|
|
14310
14603
|
function rerank(results, opts) {
|
|
14311
14604
|
const now2 = Date.now();
|
|
14312
14605
|
const scored = results.map((r) => {
|
|
@@ -14328,11 +14621,379 @@ function rerank(results, opts) {
|
|
|
14328
14621
|
return scored.slice(0, opts.limit);
|
|
14329
14622
|
}
|
|
14330
14623
|
|
|
14624
|
+
// src/search/bm25.ts
|
|
14625
|
+
init_tokenizer();
|
|
14626
|
+
function searchBM25(db, query, opts) {
|
|
14627
|
+
const limit = opts?.limit ?? 20;
|
|
14628
|
+
const agentId = opts?.agent_id ?? "default";
|
|
14629
|
+
const minVitality = opts?.min_vitality ?? 0;
|
|
14630
|
+
const ftsQuery = buildFtsQuery(query);
|
|
14631
|
+
if (!ftsQuery) return [];
|
|
14632
|
+
try {
|
|
14633
|
+
const rows = db.prepare(
|
|
14634
|
+
`SELECT m.*, rank AS score
|
|
14635
|
+
FROM memories_fts f
|
|
14636
|
+
JOIN memories m ON m.id = f.id
|
|
14637
|
+
WHERE memories_fts MATCH ?
|
|
14638
|
+
AND m.agent_id = ?
|
|
14639
|
+
AND m.vitality >= ?
|
|
14640
|
+
ORDER BY rank
|
|
14641
|
+
LIMIT ?`
|
|
14642
|
+
).all(ftsQuery, agentId, minVitality, limit);
|
|
14643
|
+
return rows.map((row) => {
|
|
14644
|
+
const { score: _score, ...memoryFields } = row;
|
|
14645
|
+
return {
|
|
14646
|
+
memory: memoryFields,
|
|
14647
|
+
score: Math.abs(row.score),
|
|
14648
|
+
// FTS5 rank is negative (lower = better)
|
|
14649
|
+
matchReason: "bm25"
|
|
14650
|
+
};
|
|
14651
|
+
});
|
|
14652
|
+
} catch {
|
|
14653
|
+
return searchSimple(db, query, agentId, minVitality, limit);
|
|
14654
|
+
}
|
|
14655
|
+
}
|
|
14656
|
+
function searchSimple(db, query, agentId, minVitality, limit) {
|
|
14657
|
+
const rows = db.prepare(
|
|
14658
|
+
`SELECT * FROM memories
|
|
14659
|
+
WHERE agent_id = ? AND vitality >= ? AND content LIKE ?
|
|
14660
|
+
ORDER BY priority ASC, updated_at DESC
|
|
14661
|
+
LIMIT ?`
|
|
14662
|
+
).all(agentId, minVitality, `%${query}%`, limit);
|
|
14663
|
+
return rows.map((m, i) => ({
|
|
14664
|
+
memory: m,
|
|
14665
|
+
score: 1 / (i + 1),
|
|
14666
|
+
// Simple rank by position
|
|
14667
|
+
matchReason: "like"
|
|
14668
|
+
}));
|
|
14669
|
+
}
|
|
14670
|
+
function buildFtsQuery(text) {
|
|
14671
|
+
const tokens = tokenize(text);
|
|
14672
|
+
if (tokens.length === 0) return null;
|
|
14673
|
+
return tokens.map((w) => `"${w}"`).join(" OR ");
|
|
14674
|
+
}
|
|
14675
|
+
|
|
14676
|
+
// src/search/embeddings.ts
|
|
14677
|
+
init_db();
|
|
14678
|
+
function encodeEmbedding(vector) {
|
|
14679
|
+
const arr = vector instanceof Float32Array ? vector : Float32Array.from(vector);
|
|
14680
|
+
return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
14681
|
+
}
|
|
14682
|
+
function decodeEmbedding(buf) {
|
|
14683
|
+
const copy = Buffer.from(buf);
|
|
14684
|
+
return new Float32Array(copy.buffer, copy.byteOffset, Math.floor(copy.byteLength / 4));
|
|
14685
|
+
}
|
|
14686
|
+
function upsertEmbedding(db, input) {
|
|
14687
|
+
const ts = now();
|
|
14688
|
+
const vec = input.vector instanceof Float32Array ? input.vector : Float32Array.from(input.vector);
|
|
14689
|
+
const blob = encodeEmbedding(vec);
|
|
14690
|
+
db.prepare(
|
|
14691
|
+
`INSERT INTO embeddings (agent_id, memory_id, model, dim, vector, created_at, updated_at)
|
|
14692
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
14693
|
+
ON CONFLICT(agent_id, memory_id, model) DO UPDATE SET
|
|
14694
|
+
dim = excluded.dim,
|
|
14695
|
+
vector = excluded.vector,
|
|
14696
|
+
updated_at = excluded.updated_at`
|
|
14697
|
+
).run(input.agent_id, input.memory_id, input.model, vec.length, blob, ts, ts);
|
|
14698
|
+
}
|
|
14699
|
+
function listEmbeddings(db, agent_id, model) {
|
|
14700
|
+
const rows = db.prepare(
|
|
14701
|
+
"SELECT memory_id, vector FROM embeddings WHERE agent_id = ? AND model = ?"
|
|
14702
|
+
).all(agent_id, model);
|
|
14703
|
+
return rows.map((r) => ({ memory_id: r.memory_id, vector: decodeEmbedding(r.vector) }));
|
|
14704
|
+
}
|
|
14705
|
+
|
|
14706
|
+
// src/search/hybrid.ts
|
|
14707
|
+
function cosine(a, b) {
|
|
14708
|
+
const n = Math.min(a.length, b.length);
|
|
14709
|
+
let dot = 0;
|
|
14710
|
+
let na = 0;
|
|
14711
|
+
let nb = 0;
|
|
14712
|
+
for (let i = 0; i < n; i++) {
|
|
14713
|
+
const x = a[i];
|
|
14714
|
+
const y = b[i];
|
|
14715
|
+
dot += x * y;
|
|
14716
|
+
na += x * x;
|
|
14717
|
+
nb += y * y;
|
|
14718
|
+
}
|
|
14719
|
+
if (na === 0 || nb === 0) return 0;
|
|
14720
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
14721
|
+
}
|
|
14722
|
+
function rrfScore(rank, k) {
|
|
14723
|
+
return 1 / (k + rank);
|
|
14724
|
+
}
|
|
14725
|
+
function fuseRrf(lists, k) {
|
|
14726
|
+
const out = /* @__PURE__ */ new Map();
|
|
14727
|
+
for (const list of lists) {
|
|
14728
|
+
for (let i = 0; i < list.items.length; i++) {
|
|
14729
|
+
const it = list.items[i];
|
|
14730
|
+
const rank = i + 1;
|
|
14731
|
+
const add = rrfScore(rank, k);
|
|
14732
|
+
const prev = out.get(it.id);
|
|
14733
|
+
if (!prev) out.set(it.id, { score: add, sources: [list.name] });
|
|
14734
|
+
else {
|
|
14735
|
+
prev.score += add;
|
|
14736
|
+
if (!prev.sources.includes(list.name)) prev.sources.push(list.name);
|
|
14737
|
+
}
|
|
14738
|
+
}
|
|
14739
|
+
}
|
|
14740
|
+
return out;
|
|
14741
|
+
}
|
|
14742
|
+
function fetchMemories(db, ids, agentId) {
|
|
14743
|
+
if (ids.length === 0) return [];
|
|
14744
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
14745
|
+
const sql = agentId ? `SELECT * FROM memories WHERE id IN (${placeholders}) AND agent_id = ?` : `SELECT * FROM memories WHERE id IN (${placeholders})`;
|
|
14746
|
+
const rows = db.prepare(sql).all(...agentId ? [...ids, agentId] : ids);
|
|
14747
|
+
return rows;
|
|
14748
|
+
}
|
|
14749
|
+
async function searchHybrid(db, query, opts) {
|
|
14750
|
+
const agentId = opts?.agent_id ?? "default";
|
|
14751
|
+
const limit = opts?.limit ?? 10;
|
|
14752
|
+
const bm25Mult = opts?.bm25CandidateMultiplier ?? 3;
|
|
14753
|
+
const semanticCandidates = opts?.semanticCandidates ?? 50;
|
|
14754
|
+
const rrfK = opts?.rrfK ?? 60;
|
|
14755
|
+
const bm25 = searchBM25(db, query, {
|
|
14756
|
+
agent_id: agentId,
|
|
14757
|
+
limit: limit * bm25Mult
|
|
14758
|
+
});
|
|
14759
|
+
const provider = opts?.embeddingProvider ?? null;
|
|
14760
|
+
const model = opts?.embeddingModel ?? provider?.model;
|
|
14761
|
+
if (!provider || !model) {
|
|
14762
|
+
return bm25.slice(0, limit);
|
|
14763
|
+
}
|
|
14764
|
+
const embedFn = provider.embedQuery ?? provider.embed;
|
|
14765
|
+
const qVec = Float32Array.from(await embedFn.call(provider, query));
|
|
14766
|
+
const embeddings = listEmbeddings(db, agentId, model);
|
|
14767
|
+
const scored = [];
|
|
14768
|
+
for (const e of embeddings) {
|
|
14769
|
+
scored.push({ id: e.memory_id, score: cosine(qVec, e.vector) });
|
|
14770
|
+
}
|
|
14771
|
+
scored.sort((a, b) => b.score - a.score);
|
|
14772
|
+
const semanticTop = scored.slice(0, semanticCandidates);
|
|
14773
|
+
const fused = fuseRrf(
|
|
14774
|
+
[
|
|
14775
|
+
{ name: "bm25", items: bm25.map((r) => ({ id: r.memory.id, score: r.score })) },
|
|
14776
|
+
{ name: "semantic", items: semanticTop }
|
|
14777
|
+
],
|
|
14778
|
+
rrfK
|
|
14779
|
+
);
|
|
14780
|
+
const ids = [...fused.keys()];
|
|
14781
|
+
const memories = fetchMemories(db, ids, agentId);
|
|
14782
|
+
const byId = new Map(memories.map((m) => [m.id, m]));
|
|
14783
|
+
const out = [];
|
|
14784
|
+
for (const [id, meta3] of fused) {
|
|
14785
|
+
const mem = byId.get(id);
|
|
14786
|
+
if (!mem) continue;
|
|
14787
|
+
out.push({
|
|
14788
|
+
memory: mem,
|
|
14789
|
+
score: meta3.score,
|
|
14790
|
+
matchReason: meta3.sources.sort().join("+")
|
|
14791
|
+
});
|
|
14792
|
+
}
|
|
14793
|
+
out.sort((a, b) => b.score - a.score);
|
|
14794
|
+
return out.slice(0, limit);
|
|
14795
|
+
}
|
|
14796
|
+
|
|
14797
|
+
// src/search/providers.ts
|
|
14798
|
+
var QWEN_DEFAULT_INSTRUCTION = "Given a query, retrieve the most semantically relevant document";
|
|
14799
|
+
function getDefaultInstruction(model) {
|
|
14800
|
+
const m = model.toLowerCase();
|
|
14801
|
+
if (m.includes("qwen")) return QWEN_DEFAULT_INSTRUCTION;
|
|
14802
|
+
if (m.includes("gemini")) return null;
|
|
14803
|
+
return null;
|
|
14804
|
+
}
|
|
14805
|
+
function resolveInstruction(model) {
|
|
14806
|
+
const override = process.env.AGENT_MEMORY_EMBEDDINGS_INSTRUCTION;
|
|
14807
|
+
if (override !== void 0) {
|
|
14808
|
+
const normalized = override.trim();
|
|
14809
|
+
if (!normalized) return null;
|
|
14810
|
+
const lowered = normalized.toLowerCase();
|
|
14811
|
+
if (lowered === "none" || lowered === "off" || lowered === "false" || lowered === "null") return null;
|
|
14812
|
+
return normalized;
|
|
14813
|
+
}
|
|
14814
|
+
return getDefaultInstruction(model);
|
|
14815
|
+
}
|
|
14816
|
+
function buildQueryInput(query, instructionPrefix) {
|
|
14817
|
+
if (!instructionPrefix) return query;
|
|
14818
|
+
return `Instruct: ${instructionPrefix}
|
|
14819
|
+
Query: ${query}`;
|
|
14820
|
+
}
|
|
14821
|
+
function getEmbeddingProviderFromEnv() {
|
|
14822
|
+
const provider = (process.env.AGENT_MEMORY_EMBEDDINGS_PROVIDER ?? "none").toLowerCase();
|
|
14823
|
+
if (provider === "none" || provider === "off" || provider === "false") return null;
|
|
14824
|
+
if (provider === "openai") {
|
|
14825
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
14826
|
+
const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-3-small";
|
|
14827
|
+
const baseUrl = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1";
|
|
14828
|
+
if (!apiKey) return null;
|
|
14829
|
+
const instruction = resolveInstruction(model);
|
|
14830
|
+
return createOpenAIProvider({ apiKey, model, baseUrl, instruction });
|
|
14831
|
+
}
|
|
14832
|
+
if (provider === "gemini" || provider === "google") {
|
|
14833
|
+
const apiKey = process.env.GEMINI_API_KEY ?? process.env.OPENAI_API_KEY;
|
|
14834
|
+
const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "gemini-embedding-001";
|
|
14835
|
+
const baseUrl = process.env.GEMINI_BASE_URL ?? process.env.OPENAI_BASE_URL ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
14836
|
+
if (!apiKey) return null;
|
|
14837
|
+
const instruction = resolveInstruction(model);
|
|
14838
|
+
return createOpenAIProvider({ id: "gemini", apiKey, model, baseUrl, instruction });
|
|
14839
|
+
}
|
|
14840
|
+
if (provider === "qwen" || provider === "dashscope" || provider === "tongyi") {
|
|
14841
|
+
const apiKey = process.env.DASHSCOPE_API_KEY;
|
|
14842
|
+
const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-v3";
|
|
14843
|
+
const baseUrl = process.env.DASHSCOPE_BASE_URL ?? "https://dashscope.aliyuncs.com";
|
|
14844
|
+
if (!apiKey) return null;
|
|
14845
|
+
const instruction = resolveInstruction(model);
|
|
14846
|
+
return createDashScopeProvider({ apiKey, model, baseUrl, instruction });
|
|
14847
|
+
}
|
|
14848
|
+
return null;
|
|
14849
|
+
}
|
|
14850
|
+
function authHeader(apiKey) {
|
|
14851
|
+
return apiKey.startsWith("Bearer ") ? apiKey : `Bearer ${apiKey}`;
|
|
14852
|
+
}
|
|
14853
|
+
function normalizeEmbedding(e) {
|
|
14854
|
+
if (!Array.isArray(e)) throw new Error("Invalid embedding: not an array");
|
|
14855
|
+
if (e.length === 0) throw new Error("Invalid embedding: empty");
|
|
14856
|
+
return e.map((x) => {
|
|
14857
|
+
if (typeof x !== "number" || !Number.isFinite(x)) throw new Error("Invalid embedding: non-numeric value");
|
|
14858
|
+
return x;
|
|
14859
|
+
});
|
|
14860
|
+
}
|
|
14861
|
+
function createOpenAIProvider(opts) {
|
|
14862
|
+
const baseUrl = opts.baseUrl ?? "https://api.openai.com/v1";
|
|
14863
|
+
const instructionPrefix = opts.instruction ?? null;
|
|
14864
|
+
async function requestEmbedding(input) {
|
|
14865
|
+
const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/embeddings`, {
|
|
14866
|
+
method: "POST",
|
|
14867
|
+
headers: {
|
|
14868
|
+
"content-type": "application/json",
|
|
14869
|
+
authorization: authHeader(opts.apiKey)
|
|
14870
|
+
},
|
|
14871
|
+
body: JSON.stringify({ model: opts.model, input })
|
|
14872
|
+
});
|
|
14873
|
+
if (!resp.ok) {
|
|
14874
|
+
const body = await resp.text().catch(() => "");
|
|
14875
|
+
throw new Error(`OpenAI embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
|
|
14876
|
+
}
|
|
14877
|
+
const data = await resp.json();
|
|
14878
|
+
return normalizeEmbedding(data.data?.[0]?.embedding);
|
|
14879
|
+
}
|
|
14880
|
+
return {
|
|
14881
|
+
id: opts.id ?? "openai",
|
|
14882
|
+
model: opts.model,
|
|
14883
|
+
instructionPrefix,
|
|
14884
|
+
async embed(text) {
|
|
14885
|
+
return requestEmbedding(text);
|
|
14886
|
+
},
|
|
14887
|
+
async embedQuery(query) {
|
|
14888
|
+
return requestEmbedding(buildQueryInput(query, instructionPrefix));
|
|
14889
|
+
}
|
|
14890
|
+
};
|
|
14891
|
+
}
|
|
14892
|
+
function createDashScopeProvider(opts) {
|
|
14893
|
+
const baseUrl = opts.baseUrl ?? "https://dashscope.aliyuncs.com";
|
|
14894
|
+
const instructionPrefix = opts.instruction ?? null;
|
|
14895
|
+
async function requestEmbedding(text) {
|
|
14896
|
+
const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/services/embeddings/text-embedding/text-embedding`, {
|
|
14897
|
+
method: "POST",
|
|
14898
|
+
headers: {
|
|
14899
|
+
"content-type": "application/json",
|
|
14900
|
+
authorization: authHeader(opts.apiKey)
|
|
14901
|
+
},
|
|
14902
|
+
body: JSON.stringify({
|
|
14903
|
+
model: opts.model,
|
|
14904
|
+
input: { texts: [text] }
|
|
14905
|
+
})
|
|
14906
|
+
});
|
|
14907
|
+
if (!resp.ok) {
|
|
14908
|
+
const body = await resp.text().catch(() => "");
|
|
14909
|
+
throw new Error(`DashScope embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
|
|
14910
|
+
}
|
|
14911
|
+
const data = await resp.json();
|
|
14912
|
+
const emb = data.output?.embeddings?.[0]?.embedding ?? data.output?.embeddings?.[0]?.vector ?? data.output?.embedding ?? data.data?.[0]?.embedding;
|
|
14913
|
+
return normalizeEmbedding(emb);
|
|
14914
|
+
}
|
|
14915
|
+
return {
|
|
14916
|
+
id: "dashscope",
|
|
14917
|
+
model: opts.model,
|
|
14918
|
+
instructionPrefix,
|
|
14919
|
+
async embed(text) {
|
|
14920
|
+
return requestEmbedding(text);
|
|
14921
|
+
},
|
|
14922
|
+
async embedQuery(query) {
|
|
14923
|
+
return requestEmbedding(buildQueryInput(query, instructionPrefix));
|
|
14924
|
+
}
|
|
14925
|
+
};
|
|
14926
|
+
}
|
|
14927
|
+
|
|
14928
|
+
// src/search/rerank-provider.ts
|
|
14929
|
+
function authHeader2(apiKey) {
|
|
14930
|
+
return apiKey.startsWith("Bearer ") ? apiKey : `Bearer ${apiKey}`;
|
|
14931
|
+
}
|
|
14932
|
+
function getRerankerProviderFromEnv() {
|
|
14933
|
+
const provider = (process.env.AGENT_MEMORY_RERANK_PROVIDER ?? "none").toLowerCase();
|
|
14934
|
+
if (provider === "none" || provider === "off") return null;
|
|
14935
|
+
if (provider === "openai" || provider === "jina" || provider === "cohere") {
|
|
14936
|
+
const apiKey = process.env.AGENT_MEMORY_RERANK_API_KEY ?? process.env.OPENAI_API_KEY;
|
|
14937
|
+
const model = process.env.AGENT_MEMORY_RERANK_MODEL ?? "Qwen/Qwen3-Reranker-8B";
|
|
14938
|
+
const baseUrl = process.env.AGENT_MEMORY_RERANK_BASE_URL ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1";
|
|
14939
|
+
if (!apiKey) return null;
|
|
14940
|
+
return createOpenAIRerankProvider({ apiKey, model, baseUrl });
|
|
14941
|
+
}
|
|
14942
|
+
return null;
|
|
14943
|
+
}
|
|
14944
|
+
function createOpenAIRerankProvider(opts) {
|
|
14945
|
+
const baseUrl = opts.baseUrl ?? "https://api.openai.com/v1";
|
|
14946
|
+
return {
|
|
14947
|
+
id: "openai-rerank",
|
|
14948
|
+
model: opts.model,
|
|
14949
|
+
async rerank(query, documents) {
|
|
14950
|
+
const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/rerank`, {
|
|
14951
|
+
method: "POST",
|
|
14952
|
+
headers: {
|
|
14953
|
+
"content-type": "application/json",
|
|
14954
|
+
authorization: authHeader2(opts.apiKey)
|
|
14955
|
+
},
|
|
14956
|
+
body: JSON.stringify({ model: opts.model, query, documents })
|
|
14957
|
+
});
|
|
14958
|
+
if (!resp.ok) {
|
|
14959
|
+
const body = await resp.text().catch(() => "");
|
|
14960
|
+
throw new Error(`Rerank API failed: ${resp.status} ${resp.statusText} ${body}`.trim());
|
|
14961
|
+
}
|
|
14962
|
+
const data = await resp.json();
|
|
14963
|
+
const results = data.results ?? [];
|
|
14964
|
+
return results.map((r) => {
|
|
14965
|
+
const index = typeof r.index === "number" ? r.index : Number.NaN;
|
|
14966
|
+
const relevance = typeof r.relevance_score === "number" ? r.relevance_score : Number.NaN;
|
|
14967
|
+
return { index, relevance_score: relevance };
|
|
14968
|
+
}).filter((r) => Number.isInteger(r.index) && Number.isFinite(r.relevance_score));
|
|
14969
|
+
}
|
|
14970
|
+
};
|
|
14971
|
+
}
|
|
14972
|
+
|
|
14973
|
+
// src/search/embed.ts
|
|
14974
|
+
async function embedMemory(db, memoryId, provider, opts) {
|
|
14975
|
+
const row = db.prepare("SELECT id, agent_id, content FROM memories WHERE id = ?").get(memoryId);
|
|
14976
|
+
if (!row) return false;
|
|
14977
|
+
if (opts?.agent_id && row.agent_id !== opts.agent_id) return false;
|
|
14978
|
+
const model = opts?.model ?? provider.model;
|
|
14979
|
+
const maxChars = opts?.maxChars ?? 2e3;
|
|
14980
|
+
const text = row.content.length > maxChars ? row.content.slice(0, maxChars) : row.content;
|
|
14981
|
+
const vector = await provider.embed(text);
|
|
14982
|
+
upsertEmbedding(db, {
|
|
14983
|
+
agent_id: row.agent_id,
|
|
14984
|
+
memory_id: row.id,
|
|
14985
|
+
model,
|
|
14986
|
+
vector
|
|
14987
|
+
});
|
|
14988
|
+
return true;
|
|
14989
|
+
}
|
|
14990
|
+
|
|
14331
14991
|
// src/sleep/sync.ts
|
|
14332
14992
|
init_memory();
|
|
14333
14993
|
|
|
14334
14994
|
// src/core/guard.ts
|
|
14335
14995
|
init_memory();
|
|
14996
|
+
init_tokenizer();
|
|
14336
14997
|
function guard(db, input) {
|
|
14337
14998
|
const hash2 = contentHash(input.content);
|
|
14338
14999
|
const agentId = input.agent_id ?? "default";
|
|
@@ -14341,7 +15002,7 @@ function guard(db, input) {
|
|
|
14341
15002
|
return { action: "skip", reason: "Exact duplicate (hash match)", existingId: exactMatch.id };
|
|
14342
15003
|
}
|
|
14343
15004
|
if (input.uri) {
|
|
14344
|
-
const existingPath = getPathByUri(db, input.uri);
|
|
15005
|
+
const existingPath = getPathByUri(db, input.uri, agentId);
|
|
14345
15006
|
if (existingPath) {
|
|
14346
15007
|
return {
|
|
14347
15008
|
action: "update",
|
|
@@ -14350,40 +15011,85 @@ function guard(db, input) {
|
|
|
14350
15011
|
};
|
|
14351
15012
|
}
|
|
14352
15013
|
}
|
|
14353
|
-
const
|
|
14354
|
-
|
|
14355
|
-
|
|
14356
|
-
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
|
|
14360
|
-
|
|
14361
|
-
|
|
14362
|
-
|
|
14363
|
-
|
|
14364
|
-
|
|
15014
|
+
const ftsTokens = tokenize(input.content.slice(0, 200));
|
|
15015
|
+
const ftsQuery = ftsTokens.length > 0 ? ftsTokens.slice(0, 8).map((w) => `"${w}"`).join(" OR ") : null;
|
|
15016
|
+
if (ftsQuery) {
|
|
15017
|
+
try {
|
|
15018
|
+
const similar = db.prepare(
|
|
15019
|
+
`SELECT m.id, m.content, m.type, rank
|
|
15020
|
+
FROM memories_fts f
|
|
15021
|
+
JOIN memories m ON m.id = f.id
|
|
15022
|
+
WHERE memories_fts MATCH ? AND m.agent_id = ?
|
|
15023
|
+
ORDER BY rank
|
|
15024
|
+
LIMIT 3`
|
|
15025
|
+
).all(ftsQuery, agentId);
|
|
15026
|
+
if (similar.length > 0) {
|
|
15027
|
+
const topRank = Math.abs(similar[0].rank);
|
|
15028
|
+
const tokenCount = ftsTokens.length;
|
|
15029
|
+
const dynamicThreshold = tokenCount * 1.5;
|
|
15030
|
+
if (topRank > dynamicThreshold) {
|
|
15031
|
+
const existing = similar[0];
|
|
15032
|
+
if (existing.type === input.type) {
|
|
15033
|
+
const merged = `${existing.content}
|
|
14365
15034
|
|
|
14366
15035
|
[Updated] ${input.content}`;
|
|
14367
|
-
|
|
14368
|
-
|
|
14369
|
-
|
|
14370
|
-
|
|
14371
|
-
|
|
14372
|
-
|
|
15036
|
+
return {
|
|
15037
|
+
action: "merge",
|
|
15038
|
+
reason: `Similar content found (score=${topRank.toFixed(1)}, threshold=${dynamicThreshold.toFixed(1)}), merging`,
|
|
15039
|
+
existingId: existing.id,
|
|
15040
|
+
mergedContent: merged
|
|
15041
|
+
};
|
|
15042
|
+
}
|
|
15043
|
+
}
|
|
15044
|
+
}
|
|
15045
|
+
} catch {
|
|
14373
15046
|
}
|
|
14374
15047
|
}
|
|
14375
|
-
const
|
|
14376
|
-
if (
|
|
14377
|
-
|
|
14378
|
-
return { action: "skip", reason: "Empty content rejected by gate" };
|
|
14379
|
-
}
|
|
15048
|
+
const gateResult = fourCriterionGate(input);
|
|
15049
|
+
if (!gateResult.pass) {
|
|
15050
|
+
return { action: "skip", reason: `Gate rejected: ${gateResult.failedCriteria.join(", ")}` };
|
|
14380
15051
|
}
|
|
14381
15052
|
return { action: "add", reason: "Passed all guard checks" };
|
|
14382
15053
|
}
|
|
14383
|
-
function
|
|
14384
|
-
const
|
|
14385
|
-
|
|
14386
|
-
|
|
15054
|
+
function fourCriterionGate(input) {
|
|
15055
|
+
const content = input.content.trim();
|
|
15056
|
+
const failed = [];
|
|
15057
|
+
const priority = input.priority ?? (input.type === "identity" ? 0 : input.type === "emotion" ? 1 : input.type === "knowledge" ? 2 : 3);
|
|
15058
|
+
const minLength = priority <= 1 ? 4 : 8;
|
|
15059
|
+
const specificity = content.length >= minLength ? Math.min(1, content.length / 50) : 0;
|
|
15060
|
+
if (specificity === 0) failed.push(`specificity (too short: ${content.length} < ${minLength} chars)`);
|
|
15061
|
+
const tokens = tokenize(content);
|
|
15062
|
+
const novelty = tokens.length >= 1 ? Math.min(1, tokens.length / 5) : 0;
|
|
15063
|
+
if (novelty === 0) failed.push("novelty (no meaningful tokens after filtering)");
|
|
15064
|
+
const hasCJK = /[\u4e00-\u9fff]/.test(content);
|
|
15065
|
+
const hasCapitalized = /[A-Z][a-z]+/.test(content);
|
|
15066
|
+
const hasNumbers = /\d+/.test(content);
|
|
15067
|
+
const hasURI = /\w+:\/\//.test(content);
|
|
15068
|
+
const hasEntityMarkers = /[@#]/.test(content);
|
|
15069
|
+
const hasMeaningfulLength = content.length >= 15;
|
|
15070
|
+
const topicSignals = [hasCJK, hasCapitalized, hasNumbers, hasURI, hasEntityMarkers, hasMeaningfulLength].filter(Boolean).length;
|
|
15071
|
+
const relevance = topicSignals >= 1 ? Math.min(1, topicSignals / 3) : 0;
|
|
15072
|
+
if (relevance === 0) failed.push("relevance (no identifiable topics/entities)");
|
|
15073
|
+
const allCaps = content === content.toUpperCase() && content.length > 20 && /^[A-Z\s]+$/.test(content);
|
|
15074
|
+
const hasWhitespaceOrPunctuation = /[\s,。!?,.!?;;::]/.test(content) || content.length < 30;
|
|
15075
|
+
const excessiveRepetition = /(.)\1{9,}/.test(content);
|
|
15076
|
+
let coherence = 1;
|
|
15077
|
+
if (allCaps) {
|
|
15078
|
+
coherence -= 0.5;
|
|
15079
|
+
}
|
|
15080
|
+
if (!hasWhitespaceOrPunctuation) {
|
|
15081
|
+
coherence -= 0.3;
|
|
15082
|
+
}
|
|
15083
|
+
if (excessiveRepetition) {
|
|
15084
|
+
coherence -= 0.5;
|
|
15085
|
+
}
|
|
15086
|
+
coherence = Math.max(0, coherence);
|
|
15087
|
+
if (coherence < 0.3) failed.push("coherence (garbled or malformed content)");
|
|
15088
|
+
return {
|
|
15089
|
+
pass: failed.length === 0,
|
|
15090
|
+
scores: { specificity, novelty, relevance, coherence },
|
|
15091
|
+
failedCriteria: failed
|
|
15092
|
+
};
|
|
14387
15093
|
}
|
|
14388
15094
|
|
|
14389
15095
|
// src/sleep/sync.ts
|
|
@@ -14442,25 +15148,28 @@ var MIN_VITALITY = {
|
|
|
14442
15148
|
3: 0
|
|
14443
15149
|
// P3: event — full decay
|
|
14444
15150
|
};
|
|
14445
|
-
function calculateVitality(stability,
|
|
15151
|
+
function calculateVitality(stability, daysSinceLastAccess, priority) {
|
|
14446
15152
|
if (priority === 0) return 1;
|
|
14447
15153
|
const S = Math.max(0.01, stability);
|
|
14448
|
-
const retention = Math.exp(-
|
|
15154
|
+
const retention = Math.exp(-daysSinceLastAccess / S);
|
|
14449
15155
|
const minVit = MIN_VITALITY[priority] ?? 0;
|
|
14450
15156
|
return Math.max(minVit, retention);
|
|
14451
15157
|
}
|
|
14452
|
-
function runDecay(db) {
|
|
15158
|
+
function runDecay(db, opts) {
|
|
14453
15159
|
const currentTime = now();
|
|
14454
15160
|
const currentMs = new Date(currentTime).getTime();
|
|
14455
|
-
const
|
|
15161
|
+
const agentId = opts?.agent_id;
|
|
15162
|
+
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";
|
|
15163
|
+
const memories = db.prepare(query).all(...agentId ? [agentId] : []);
|
|
14456
15164
|
let updated = 0;
|
|
14457
15165
|
let decayed = 0;
|
|
14458
15166
|
let belowThreshold = 0;
|
|
14459
15167
|
const updateStmt = db.prepare("UPDATE memories SET vitality = ?, updated_at = ? WHERE id = ?");
|
|
14460
15168
|
const transaction = db.transaction(() => {
|
|
14461
15169
|
for (const mem of memories) {
|
|
14462
|
-
const
|
|
14463
|
-
const
|
|
15170
|
+
const referenceTime = mem.last_accessed ?? mem.created_at;
|
|
15171
|
+
const referenceMs = new Date(referenceTime).getTime();
|
|
15172
|
+
const daysSince = (currentMs - referenceMs) / (1e3 * 60 * 60 * 24);
|
|
14464
15173
|
const newVitality = calculateVitality(mem.stability, daysSince, mem.priority);
|
|
14465
15174
|
if (Math.abs(newVitality - mem.vitality) > 1e-3) {
|
|
14466
15175
|
updateStmt.run(newVitality, currentTime, mem.id);
|
|
@@ -14477,12 +15186,15 @@ function runDecay(db) {
|
|
|
14477
15186
|
transaction();
|
|
14478
15187
|
return { updated, decayed, belowThreshold };
|
|
14479
15188
|
}
|
|
14480
|
-
function getDecayedMemories(db, threshold = 0.05) {
|
|
15189
|
+
function getDecayedMemories(db, threshold = 0.05, opts) {
|
|
15190
|
+
const agentId = opts?.agent_id;
|
|
14481
15191
|
return db.prepare(
|
|
14482
|
-
`SELECT id, content, vitality, priority FROM memories
|
|
14483
|
-
|
|
14484
|
-
|
|
14485
|
-
|
|
15192
|
+
agentId ? `SELECT id, content, vitality, priority FROM memories
|
|
15193
|
+
WHERE vitality < ? AND priority >= 3 AND agent_id = ?
|
|
15194
|
+
ORDER BY vitality ASC` : `SELECT id, content, vitality, priority FROM memories
|
|
15195
|
+
WHERE vitality < ? AND priority >= 3
|
|
15196
|
+
ORDER BY vitality ASC`
|
|
15197
|
+
).all(...agentId ? [threshold, agentId] : [threshold]);
|
|
14486
15198
|
}
|
|
14487
15199
|
|
|
14488
15200
|
// src/sleep/tidy.ts
|
|
@@ -14490,11 +15202,12 @@ init_memory();
|
|
|
14490
15202
|
function runTidy(db, opts) {
|
|
14491
15203
|
const threshold = opts?.vitalityThreshold ?? 0.05;
|
|
14492
15204
|
const maxSnapshots = opts?.maxSnapshotsPerMemory ?? 10;
|
|
15205
|
+
const agentId = opts?.agent_id;
|
|
14493
15206
|
let archived = 0;
|
|
14494
15207
|
let orphansCleaned = 0;
|
|
14495
15208
|
let snapshotsPruned = 0;
|
|
14496
15209
|
const transaction = db.transaction(() => {
|
|
14497
|
-
const decayed = getDecayedMemories(db, threshold);
|
|
15210
|
+
const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
|
|
14498
15211
|
for (const mem of decayed) {
|
|
14499
15212
|
try {
|
|
14500
15213
|
createSnapshot(db, mem.id, "delete", "tidy");
|
|
@@ -14503,13 +15216,23 @@ function runTidy(db, opts) {
|
|
|
14503
15216
|
deleteMemory(db, mem.id);
|
|
14504
15217
|
archived++;
|
|
14505
15218
|
}
|
|
14506
|
-
const orphans = db.prepare(
|
|
14507
|
-
`DELETE FROM paths
|
|
15219
|
+
const orphans = agentId ? db.prepare(
|
|
15220
|
+
`DELETE FROM paths
|
|
15221
|
+
WHERE agent_id = ?
|
|
15222
|
+
AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
|
|
15223
|
+
).run(agentId, agentId) : db.prepare(
|
|
15224
|
+
"DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)"
|
|
14508
15225
|
).run();
|
|
14509
15226
|
orphansCleaned = orphans.changes;
|
|
14510
|
-
const memoriesWithSnapshots = db.prepare(
|
|
15227
|
+
const memoriesWithSnapshots = agentId ? db.prepare(
|
|
15228
|
+
`SELECT s.memory_id, COUNT(*) as cnt
|
|
15229
|
+
FROM snapshots s
|
|
15230
|
+
JOIN memories m ON m.id = s.memory_id
|
|
15231
|
+
WHERE m.agent_id = ?
|
|
15232
|
+
GROUP BY s.memory_id HAVING cnt > ?`
|
|
15233
|
+
).all(agentId, maxSnapshots) : db.prepare(
|
|
14511
15234
|
`SELECT memory_id, COUNT(*) as cnt FROM snapshots
|
|
14512
|
-
|
|
15235
|
+
GROUP BY memory_id HAVING cnt > ?`
|
|
14513
15236
|
).all(maxSnapshots);
|
|
14514
15237
|
for (const { memory_id } of memoriesWithSnapshots) {
|
|
14515
15238
|
const pruned = db.prepare(
|
|
@@ -14526,20 +15249,31 @@ function runTidy(db, opts) {
|
|
|
14526
15249
|
}
|
|
14527
15250
|
|
|
14528
15251
|
// src/sleep/govern.ts
|
|
14529
|
-
function runGovern(db) {
|
|
15252
|
+
function runGovern(db, opts) {
|
|
15253
|
+
const agentId = opts?.agent_id;
|
|
14530
15254
|
let orphanPaths = 0;
|
|
14531
15255
|
let orphanLinks = 0;
|
|
14532
15256
|
let emptyMemories = 0;
|
|
14533
15257
|
const transaction = db.transaction(() => {
|
|
14534
|
-
const pathResult = db.prepare(
|
|
15258
|
+
const pathResult = agentId ? db.prepare(
|
|
15259
|
+
`DELETE FROM paths
|
|
15260
|
+
WHERE agent_id = ?
|
|
15261
|
+
AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
|
|
15262
|
+
).run(agentId, agentId) : db.prepare("DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)").run();
|
|
14535
15263
|
orphanPaths = pathResult.changes;
|
|
14536
|
-
const linkResult = db.prepare(
|
|
15264
|
+
const linkResult = agentId ? db.prepare(
|
|
15265
|
+
`DELETE FROM links WHERE
|
|
15266
|
+
agent_id = ? AND (
|
|
15267
|
+
source_id NOT IN (SELECT id FROM memories WHERE agent_id = ?) OR
|
|
15268
|
+
target_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)
|
|
15269
|
+
)`
|
|
15270
|
+
).run(agentId, agentId, agentId) : db.prepare(
|
|
14537
15271
|
`DELETE FROM links WHERE
|
|
14538
15272
|
source_id NOT IN (SELECT id FROM memories) OR
|
|
14539
15273
|
target_id NOT IN (SELECT id FROM memories)`
|
|
14540
15274
|
).run();
|
|
14541
15275
|
orphanLinks = linkResult.changes;
|
|
14542
|
-
const emptyResult = db.prepare("DELETE FROM memories WHERE TRIM(content) = ''").run();
|
|
15276
|
+
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();
|
|
14543
15277
|
emptyMemories = emptyResult.changes;
|
|
14544
15278
|
});
|
|
14545
15279
|
transaction();
|
|
@@ -14564,7 +15298,7 @@ function boot(db, opts) {
|
|
|
14564
15298
|
}
|
|
14565
15299
|
const bootPaths = [];
|
|
14566
15300
|
for (const uri of corePaths) {
|
|
14567
|
-
const path = getPathByUri(db, uri);
|
|
15301
|
+
const path = getPathByUri(db, uri, agentId);
|
|
14568
15302
|
if (path) {
|
|
14569
15303
|
bootPaths.push(uri);
|
|
14570
15304
|
if (!memories.has(path.memory_id)) {
|
|
@@ -14576,13 +15310,13 @@ function boot(db, opts) {
|
|
|
14576
15310
|
}
|
|
14577
15311
|
}
|
|
14578
15312
|
}
|
|
14579
|
-
const bootEntry = getPathByUri(db, "system://boot");
|
|
15313
|
+
const bootEntry = getPathByUri(db, "system://boot", agentId);
|
|
14580
15314
|
if (bootEntry) {
|
|
14581
15315
|
const bootMem = getMemory(db, bootEntry.memory_id);
|
|
14582
15316
|
if (bootMem) {
|
|
14583
15317
|
const additionalUris = bootMem.content.split("\n").map((l) => l.trim()).filter((l) => l.match(/^[a-z]+:\/\//));
|
|
14584
15318
|
for (const uri of additionalUris) {
|
|
14585
|
-
const path = getPathByUri(db, uri);
|
|
15319
|
+
const path = getPathByUri(db, uri, agentId);
|
|
14586
15320
|
if (path && !memories.has(path.memory_id)) {
|
|
14587
15321
|
const mem = getMemory(db, path.memory_id);
|
|
14588
15322
|
if (mem) {
|
|
@@ -14605,9 +15339,11 @@ var AGENT_ID = process.env.AGENT_MEMORY_AGENT_ID ?? "default";
|
|
|
14605
15339
|
function createMcpServer(dbPath, agentId) {
|
|
14606
15340
|
const db = openDatabase({ path: dbPath ?? DB_PATH });
|
|
14607
15341
|
const aid = agentId ?? AGENT_ID;
|
|
15342
|
+
const embeddingProvider = getEmbeddingProviderFromEnv();
|
|
15343
|
+
const rerankerProvider = getRerankerProviderFromEnv();
|
|
14608
15344
|
const server = new McpServer({
|
|
14609
15345
|
name: "agent-memory",
|
|
14610
|
-
version: "2.
|
|
15346
|
+
version: "2.1.0"
|
|
14611
15347
|
});
|
|
14612
15348
|
server.tool(
|
|
14613
15349
|
"remember",
|
|
@@ -14621,6 +15357,12 @@ function createMcpServer(dbPath, agentId) {
|
|
|
14621
15357
|
},
|
|
14622
15358
|
async ({ content, type, uri, emotion_val, source }) => {
|
|
14623
15359
|
const result = syncOne(db, { content, type, uri, emotion_val, source, agent_id: aid });
|
|
15360
|
+
if (embeddingProvider && result.memoryId && (result.action === "added" || result.action === "updated" || result.action === "merged")) {
|
|
15361
|
+
try {
|
|
15362
|
+
await embedMemory(db, result.memoryId, embeddingProvider, { agent_id: aid });
|
|
15363
|
+
} catch {
|
|
15364
|
+
}
|
|
15365
|
+
}
|
|
14624
15366
|
return {
|
|
14625
15367
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
14626
15368
|
};
|
|
@@ -14636,7 +15378,10 @@ function createMcpServer(dbPath, agentId) {
|
|
|
14636
15378
|
async ({ query, limit }) => {
|
|
14637
15379
|
const { intent, confidence } = classifyIntent(query);
|
|
14638
15380
|
const strategy = getStrategy(intent);
|
|
14639
|
-
|
|
15381
|
+
let raw = await searchHybrid(db, query, { agent_id: aid, embeddingProvider, limit: limit * 2 });
|
|
15382
|
+
if (rerankerProvider) {
|
|
15383
|
+
raw = await rerankWithProvider(raw, query, rerankerProvider);
|
|
15384
|
+
}
|
|
14640
15385
|
const results = rerank(raw, { ...strategy, limit });
|
|
14641
15386
|
const output = {
|
|
14642
15387
|
intent,
|
|
@@ -14666,17 +15411,18 @@ function createMcpServer(dbPath, agentId) {
|
|
|
14666
15411
|
traverse_hops: external_exports.number().default(0).describe("Multi-hop graph traversal depth (0 = direct only)")
|
|
14667
15412
|
},
|
|
14668
15413
|
async ({ uri, traverse_hops }) => {
|
|
14669
|
-
const path = getPathByUri(db, uri);
|
|
15414
|
+
const path = getPathByUri(db, uri, aid);
|
|
14670
15415
|
if (path) {
|
|
14671
15416
|
const mem = getMemory(db, path.memory_id);
|
|
14672
|
-
if (mem) {
|
|
15417
|
+
if (mem && mem.agent_id === aid) {
|
|
14673
15418
|
recordAccess(db, mem.id);
|
|
14674
15419
|
let related = [];
|
|
14675
15420
|
if (traverse_hops > 0) {
|
|
14676
|
-
const hops = traverse(db, mem.id, traverse_hops);
|
|
15421
|
+
const hops = traverse(db, mem.id, traverse_hops, aid);
|
|
14677
15422
|
related = hops.map((h) => {
|
|
14678
15423
|
const m = getMemory(db, h.id);
|
|
14679
|
-
return { id: h.id, content:
|
|
15424
|
+
if (!m || m.agent_id !== aid) return { id: h.id, content: "", relation: h.relation, hop: h.hop };
|
|
15425
|
+
return { id: h.id, content: m.content, relation: h.relation, hop: h.hop };
|
|
14680
15426
|
});
|
|
14681
15427
|
}
|
|
14682
15428
|
return {
|
|
@@ -14687,11 +15433,12 @@ function createMcpServer(dbPath, agentId) {
|
|
|
14687
15433
|
};
|
|
14688
15434
|
}
|
|
14689
15435
|
}
|
|
14690
|
-
const paths = getPathsByPrefix(db, uri);
|
|
15436
|
+
const paths = getPathsByPrefix(db, uri, aid);
|
|
14691
15437
|
if (paths.length > 0) {
|
|
14692
15438
|
const memories = paths.map((p) => {
|
|
14693
15439
|
const m = getMemory(db, p.memory_id);
|
|
14694
|
-
return { uri: p.uri, content:
|
|
15440
|
+
if (!m || m.agent_id !== aid) return { uri: p.uri, content: void 0, type: void 0, priority: void 0 };
|
|
15441
|
+
return { uri: p.uri, content: m.content, type: m.type, priority: m.priority };
|
|
14695
15442
|
});
|
|
14696
15443
|
return {
|
|
14697
15444
|
content: [{
|
|
@@ -14731,7 +15478,7 @@ function createMcpServer(dbPath, agentId) {
|
|
|
14731
15478
|
},
|
|
14732
15479
|
async ({ id, hard }) => {
|
|
14733
15480
|
const mem = getMemory(db, id);
|
|
14734
|
-
if (!mem) return { content: [{ type: "text", text: '{"error": "Memory not found"}' }] };
|
|
15481
|
+
if (!mem || mem.agent_id !== aid) return { content: [{ type: "text", text: '{"error": "Memory not found"}' }] };
|
|
14735
15482
|
if (hard) {
|
|
14736
15483
|
createSnapshot(db, id, "delete", "forget");
|
|
14737
15484
|
const { deleteMemory: deleteMemory2 } = await Promise.resolve().then(() => (init_memory(), memory_exports));
|
|
@@ -14759,18 +15506,19 @@ function createMcpServer(dbPath, agentId) {
|
|
|
14759
15506
|
},
|
|
14760
15507
|
async ({ action, source_id, target_id, relation, max_hops }) => {
|
|
14761
15508
|
if (action === "create" && source_id && target_id && relation) {
|
|
14762
|
-
const link = createLink(db, source_id, target_id, relation);
|
|
15509
|
+
const link = createLink(db, source_id, target_id, relation, 1, aid);
|
|
14763
15510
|
return { content: [{ type: "text", text: JSON.stringify({ created: link }) }] };
|
|
14764
15511
|
}
|
|
14765
15512
|
if (action === "query" && source_id) {
|
|
14766
|
-
const links = getLinks(db, source_id);
|
|
15513
|
+
const links = getLinks(db, source_id, aid);
|
|
14767
15514
|
return { content: [{ type: "text", text: JSON.stringify({ links }) }] };
|
|
14768
15515
|
}
|
|
14769
15516
|
if (action === "traverse" && source_id) {
|
|
14770
|
-
const nodes = traverse(db, source_id, max_hops);
|
|
15517
|
+
const nodes = traverse(db, source_id, max_hops, aid);
|
|
14771
15518
|
const detailed = nodes.map((n) => {
|
|
14772
15519
|
const m = getMemory(db, n.id);
|
|
14773
|
-
return { ...n, content:
|
|
15520
|
+
if (!m || m.agent_id !== aid) return { ...n, content: void 0 };
|
|
15521
|
+
return { ...n, content: m.content };
|
|
14774
15522
|
});
|
|
14775
15523
|
return { content: [{ type: "text", text: JSON.stringify({ nodes: detailed }) }] };
|
|
14776
15524
|
}
|
|
@@ -14787,10 +15535,16 @@ function createMcpServer(dbPath, agentId) {
|
|
|
14787
15535
|
},
|
|
14788
15536
|
async ({ action, memory_id, snapshot_id }) => {
|
|
14789
15537
|
if (action === "list" && memory_id) {
|
|
15538
|
+
const mem = getMemory(db, memory_id);
|
|
15539
|
+
if (!mem || mem.agent_id !== aid) return { content: [{ type: "text", text: '{"error": "Memory not found"}' }] };
|
|
14790
15540
|
const snaps = getSnapshots(db, memory_id);
|
|
14791
15541
|
return { content: [{ type: "text", text: JSON.stringify({ snapshots: snaps }) }] };
|
|
14792
15542
|
}
|
|
14793
15543
|
if (action === "rollback" && snapshot_id) {
|
|
15544
|
+
const snap = getSnapshot(db, snapshot_id);
|
|
15545
|
+
if (!snap) return { content: [{ type: "text", text: '{"error": "Snapshot not found"}' }] };
|
|
15546
|
+
const mem = getMemory(db, snap.memory_id);
|
|
15547
|
+
if (!mem || mem.agent_id !== aid) return { content: [{ type: "text", text: '{"error": "Snapshot not found"}' }] };
|
|
14794
15548
|
const ok = rollback(db, snapshot_id);
|
|
14795
15549
|
return { content: [{ type: "text", text: JSON.stringify({ rolled_back: ok }) }] };
|
|
14796
15550
|
}
|
|
@@ -14806,13 +15560,13 @@ function createMcpServer(dbPath, agentId) {
|
|
|
14806
15560
|
async ({ phase }) => {
|
|
14807
15561
|
const results = {};
|
|
14808
15562
|
if (phase === "decay" || phase === "all") {
|
|
14809
|
-
results.decay = runDecay(db);
|
|
15563
|
+
results.decay = runDecay(db, { agent_id: aid });
|
|
14810
15564
|
}
|
|
14811
15565
|
if (phase === "tidy" || phase === "all") {
|
|
14812
|
-
results.tidy = runTidy(db);
|
|
15566
|
+
results.tidy = runTidy(db, { agent_id: aid });
|
|
14813
15567
|
}
|
|
14814
15568
|
if (phase === "govern" || phase === "all") {
|
|
14815
|
-
results.govern = runGovern(db);
|
|
15569
|
+
results.govern = runGovern(db, { agent_id: aid });
|
|
14816
15570
|
}
|
|
14817
15571
|
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
14818
15572
|
}
|
|
@@ -14824,9 +15578,14 @@ function createMcpServer(dbPath, agentId) {
|
|
|
14824
15578
|
async () => {
|
|
14825
15579
|
const stats = countMemories(db, aid);
|
|
14826
15580
|
const lowVitality = db.prepare("SELECT COUNT(*) as c FROM memories WHERE vitality < 0.1 AND agent_id = ?").get(aid);
|
|
14827
|
-
const totalSnapshots = db.prepare(
|
|
14828
|
-
|
|
14829
|
-
|
|
15581
|
+
const totalSnapshots = db.prepare(
|
|
15582
|
+
`SELECT COUNT(*) as c
|
|
15583
|
+
FROM snapshots s
|
|
15584
|
+
JOIN memories m ON m.id = s.memory_id
|
|
15585
|
+
WHERE m.agent_id = ?`
|
|
15586
|
+
).get(aid);
|
|
15587
|
+
const totalLinks = db.prepare("SELECT COUNT(*) as c FROM links WHERE agent_id = ?").get(aid);
|
|
15588
|
+
const totalPaths = db.prepare("SELECT COUNT(*) as c FROM paths WHERE agent_id = ?").get(aid);
|
|
14830
15589
|
return {
|
|
14831
15590
|
content: [{
|
|
14832
15591
|
type: "text",
|