@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/bin/agent-memory.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// src/core/db.ts
|
|
5
5
|
import Database from "better-sqlite3";
|
|
6
6
|
import { randomUUID } from "crypto";
|
|
7
|
-
var SCHEMA_VERSION =
|
|
7
|
+
var SCHEMA_VERSION = 3;
|
|
8
8
|
var SCHEMA_SQL = `
|
|
9
9
|
-- Memory entries
|
|
10
10
|
CREATE TABLE IF NOT EXISTS memories (
|
|
@@ -29,20 +29,23 @@ CREATE TABLE IF NOT EXISTS memories (
|
|
|
29
29
|
CREATE TABLE IF NOT EXISTS paths (
|
|
30
30
|
id TEXT PRIMARY KEY,
|
|
31
31
|
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
32
|
-
|
|
32
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
33
|
+
uri TEXT NOT NULL,
|
|
33
34
|
alias TEXT,
|
|
34
35
|
domain TEXT NOT NULL,
|
|
35
|
-
created_at TEXT NOT NULL
|
|
36
|
+
created_at TEXT NOT NULL,
|
|
37
|
+
UNIQUE(agent_id, uri)
|
|
36
38
|
);
|
|
37
39
|
|
|
38
40
|
-- Association network (knowledge graph)
|
|
39
41
|
CREATE TABLE IF NOT EXISTS links (
|
|
42
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
40
43
|
source_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
41
44
|
target_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
42
45
|
relation TEXT NOT NULL,
|
|
43
46
|
weight REAL NOT NULL DEFAULT 1.0,
|
|
44
47
|
created_at TEXT NOT NULL,
|
|
45
|
-
PRIMARY KEY (source_id, target_id)
|
|
48
|
+
PRIMARY KEY (agent_id, source_id, target_id)
|
|
46
49
|
);
|
|
47
50
|
|
|
48
51
|
-- Snapshots (version control, from nocturne + Memory Palace)
|
|
@@ -62,6 +65,18 @@ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
|
62
65
|
tokenize='unicode61'
|
|
63
66
|
);
|
|
64
67
|
|
|
68
|
+
-- Embeddings (optional semantic layer)
|
|
69
|
+
CREATE TABLE IF NOT EXISTS embeddings (
|
|
70
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
71
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
72
|
+
model TEXT NOT NULL,
|
|
73
|
+
dim INTEGER NOT NULL,
|
|
74
|
+
vector BLOB NOT NULL,
|
|
75
|
+
created_at TEXT NOT NULL,
|
|
76
|
+
updated_at TEXT NOT NULL,
|
|
77
|
+
PRIMARY KEY (agent_id, memory_id, model)
|
|
78
|
+
);
|
|
79
|
+
|
|
65
80
|
-- Schema version tracking
|
|
66
81
|
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
67
82
|
key TEXT PRIMARY KEY,
|
|
@@ -76,8 +91,6 @@ CREATE INDEX IF NOT EXISTS idx_memories_vitality ON memories(vitality);
|
|
|
76
91
|
CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(hash);
|
|
77
92
|
CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
|
|
78
93
|
CREATE INDEX IF NOT EXISTS idx_paths_domain ON paths(domain);
|
|
79
|
-
CREATE INDEX IF NOT EXISTS idx_links_source ON links(source_id);
|
|
80
|
-
CREATE INDEX IF NOT EXISTS idx_links_target ON links(target_id);
|
|
81
94
|
`;
|
|
82
95
|
function openDatabase(opts) {
|
|
83
96
|
const db = new Database(opts.path);
|
|
@@ -87,13 +100,17 @@ function openDatabase(opts) {
|
|
|
87
100
|
db.pragma("foreign_keys = ON");
|
|
88
101
|
db.pragma("busy_timeout = 5000");
|
|
89
102
|
db.exec(SCHEMA_SQL);
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
103
|
+
const currentVersion = getSchemaVersion(db);
|
|
104
|
+
if (currentVersion === null) {
|
|
105
|
+
const inferred = inferSchemaVersion(db);
|
|
106
|
+
if (inferred < SCHEMA_VERSION) {
|
|
107
|
+
migrateDatabase(db, inferred, SCHEMA_VERSION);
|
|
108
|
+
}
|
|
109
|
+
db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(SCHEMA_VERSION));
|
|
110
|
+
} else if (currentVersion < SCHEMA_VERSION) {
|
|
111
|
+
migrateDatabase(db, currentVersion, SCHEMA_VERSION);
|
|
96
112
|
}
|
|
113
|
+
ensureIndexes(db);
|
|
97
114
|
return db;
|
|
98
115
|
}
|
|
99
116
|
function now() {
|
|
@@ -102,9 +119,256 @@ function now() {
|
|
|
102
119
|
function newId() {
|
|
103
120
|
return randomUUID();
|
|
104
121
|
}
|
|
122
|
+
function getSchemaVersion(db) {
|
|
123
|
+
try {
|
|
124
|
+
const row = db.prepare("SELECT value FROM schema_meta WHERE key = 'version'").get();
|
|
125
|
+
if (!row) return null;
|
|
126
|
+
const n = Number.parseInt(row.value, 10);
|
|
127
|
+
return Number.isFinite(n) ? n : null;
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function tableHasColumn(db, table, column) {
|
|
133
|
+
try {
|
|
134
|
+
const cols = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
135
|
+
return cols.some((c) => c.name === column);
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function migrateDatabase(db, from, to) {
|
|
141
|
+
let v = from;
|
|
142
|
+
while (v < to) {
|
|
143
|
+
if (v === 1) {
|
|
144
|
+
migrateV1ToV2(db);
|
|
145
|
+
v = 2;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (v === 2) {
|
|
149
|
+
migrateV2ToV3(db);
|
|
150
|
+
v = 3;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
throw new Error(`Unsupported schema migration path: v${from} \u2192 v${to} (stuck at v${v})`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function migrateV1ToV2(db) {
|
|
157
|
+
const pathsMigrated = tableHasColumn(db, "paths", "agent_id");
|
|
158
|
+
const linksMigrated = tableHasColumn(db, "links", "agent_id");
|
|
159
|
+
const alreadyMigrated = pathsMigrated && linksMigrated;
|
|
160
|
+
if (alreadyMigrated) {
|
|
161
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(2));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
db.pragma("foreign_keys = OFF");
|
|
165
|
+
try {
|
|
166
|
+
db.exec("BEGIN");
|
|
167
|
+
if (!pathsMigrated) {
|
|
168
|
+
db.exec(`
|
|
169
|
+
CREATE TABLE IF NOT EXISTS paths_v2 (
|
|
170
|
+
id TEXT PRIMARY KEY,
|
|
171
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
172
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
173
|
+
uri TEXT NOT NULL,
|
|
174
|
+
alias TEXT,
|
|
175
|
+
domain TEXT NOT NULL,
|
|
176
|
+
created_at TEXT NOT NULL,
|
|
177
|
+
UNIQUE(agent_id, uri)
|
|
178
|
+
);
|
|
179
|
+
`);
|
|
180
|
+
db.exec(`
|
|
181
|
+
INSERT INTO paths_v2 (id, memory_id, agent_id, uri, alias, domain, created_at)
|
|
182
|
+
SELECT p.id, p.memory_id, COALESCE(m.agent_id, 'default'), p.uri, p.alias, p.domain, p.created_at
|
|
183
|
+
FROM paths p
|
|
184
|
+
LEFT JOIN memories m ON m.id = p.memory_id;
|
|
185
|
+
`);
|
|
186
|
+
db.exec("DROP TABLE paths;");
|
|
187
|
+
db.exec("ALTER TABLE paths_v2 RENAME TO paths;");
|
|
188
|
+
}
|
|
189
|
+
if (!linksMigrated) {
|
|
190
|
+
db.exec(`
|
|
191
|
+
CREATE TABLE IF NOT EXISTS links_v2 (
|
|
192
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
193
|
+
source_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
194
|
+
target_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
195
|
+
relation TEXT NOT NULL,
|
|
196
|
+
weight REAL NOT NULL DEFAULT 1.0,
|
|
197
|
+
created_at TEXT NOT NULL,
|
|
198
|
+
PRIMARY KEY (agent_id, source_id, target_id)
|
|
199
|
+
);
|
|
200
|
+
`);
|
|
201
|
+
db.exec(`
|
|
202
|
+
INSERT INTO links_v2 (agent_id, source_id, target_id, relation, weight, created_at)
|
|
203
|
+
SELECT COALESCE(ms.agent_id, 'default'), l.source_id, l.target_id, l.relation, l.weight, l.created_at
|
|
204
|
+
FROM links l
|
|
205
|
+
LEFT JOIN memories ms ON ms.id = l.source_id;
|
|
206
|
+
`);
|
|
207
|
+
db.exec(`
|
|
208
|
+
DELETE FROM links_v2
|
|
209
|
+
WHERE EXISTS (SELECT 1 FROM memories s WHERE s.id = links_v2.source_id AND s.agent_id != links_v2.agent_id)
|
|
210
|
+
OR EXISTS (SELECT 1 FROM memories t WHERE t.id = links_v2.target_id AND t.agent_id != links_v2.agent_id);
|
|
211
|
+
`);
|
|
212
|
+
db.exec("DROP TABLE links;");
|
|
213
|
+
db.exec("ALTER TABLE links_v2 RENAME TO links;");
|
|
214
|
+
}
|
|
215
|
+
db.exec(`
|
|
216
|
+
CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
|
|
217
|
+
CREATE INDEX IF NOT EXISTS idx_paths_domain ON paths(domain);
|
|
218
|
+
`);
|
|
219
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(2));
|
|
220
|
+
db.exec("COMMIT");
|
|
221
|
+
} catch (e) {
|
|
222
|
+
try {
|
|
223
|
+
db.exec("ROLLBACK");
|
|
224
|
+
} catch {
|
|
225
|
+
}
|
|
226
|
+
throw e;
|
|
227
|
+
} finally {
|
|
228
|
+
db.pragma("foreign_keys = ON");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function inferSchemaVersion(db) {
|
|
232
|
+
const hasAgentScopedPaths = tableHasColumn(db, "paths", "agent_id");
|
|
233
|
+
const hasAgentScopedLinks = tableHasColumn(db, "links", "agent_id");
|
|
234
|
+
const hasEmbeddings = (() => {
|
|
235
|
+
try {
|
|
236
|
+
const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'").get();
|
|
237
|
+
return Boolean(row);
|
|
238
|
+
} catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
})();
|
|
242
|
+
if (hasAgentScopedPaths && hasAgentScopedLinks && hasEmbeddings) return 3;
|
|
243
|
+
if (hasAgentScopedPaths && hasAgentScopedLinks) return 2;
|
|
244
|
+
return 1;
|
|
245
|
+
}
|
|
246
|
+
function ensureIndexes(db) {
|
|
247
|
+
if (tableHasColumn(db, "paths", "agent_id")) {
|
|
248
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_paths_agent_uri ON paths(agent_id, uri);");
|
|
249
|
+
}
|
|
250
|
+
if (tableHasColumn(db, "links", "agent_id")) {
|
|
251
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_links_agent_source ON links(agent_id, source_id);");
|
|
252
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_links_agent_target ON links(agent_id, target_id);");
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'").get();
|
|
256
|
+
if (row) {
|
|
257
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_agent_model ON embeddings(agent_id, model);");
|
|
258
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_memory ON embeddings(memory_id);");
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function migrateV2ToV3(db) {
|
|
264
|
+
try {
|
|
265
|
+
db.exec("BEGIN");
|
|
266
|
+
db.exec(`
|
|
267
|
+
CREATE TABLE IF NOT EXISTS embeddings (
|
|
268
|
+
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
269
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
270
|
+
model TEXT NOT NULL,
|
|
271
|
+
dim INTEGER NOT NULL,
|
|
272
|
+
vector BLOB NOT NULL,
|
|
273
|
+
created_at TEXT NOT NULL,
|
|
274
|
+
updated_at TEXT NOT NULL,
|
|
275
|
+
PRIMARY KEY (agent_id, memory_id, model)
|
|
276
|
+
);
|
|
277
|
+
`);
|
|
278
|
+
db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(3));
|
|
279
|
+
db.exec("COMMIT");
|
|
280
|
+
} catch (e) {
|
|
281
|
+
try {
|
|
282
|
+
db.exec("ROLLBACK");
|
|
283
|
+
} catch {
|
|
284
|
+
}
|
|
285
|
+
throw e;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
105
288
|
|
|
106
289
|
// src/core/memory.ts
|
|
107
290
|
import { createHash } from "crypto";
|
|
291
|
+
|
|
292
|
+
// src/search/tokenizer.ts
|
|
293
|
+
import { readFileSync } from "fs";
|
|
294
|
+
import { createRequire } from "module";
|
|
295
|
+
var _jieba;
|
|
296
|
+
function getJieba() {
|
|
297
|
+
if (_jieba !== void 0) return _jieba;
|
|
298
|
+
try {
|
|
299
|
+
const req = createRequire(import.meta.url);
|
|
300
|
+
const { Jieba } = req("@node-rs/jieba");
|
|
301
|
+
const dictPath = req.resolve("@node-rs/jieba/dict.txt");
|
|
302
|
+
const dictBuf = readFileSync(dictPath);
|
|
303
|
+
_jieba = Jieba.withDict(dictBuf);
|
|
304
|
+
} catch {
|
|
305
|
+
_jieba = null;
|
|
306
|
+
}
|
|
307
|
+
return _jieba;
|
|
308
|
+
}
|
|
309
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
310
|
+
"\u7684",
|
|
311
|
+
"\u4E86",
|
|
312
|
+
"\u5728",
|
|
313
|
+
"\u662F",
|
|
314
|
+
"\u6211",
|
|
315
|
+
"\u6709",
|
|
316
|
+
"\u548C",
|
|
317
|
+
"\u5C31",
|
|
318
|
+
"\u4E0D",
|
|
319
|
+
"\u4EBA",
|
|
320
|
+
"\u90FD",
|
|
321
|
+
"\u4E00",
|
|
322
|
+
"\u4E2A",
|
|
323
|
+
"\u4E0A",
|
|
324
|
+
"\u4E5F",
|
|
325
|
+
"\u5230",
|
|
326
|
+
"\u4ED6",
|
|
327
|
+
"\u6CA1",
|
|
328
|
+
"\u8FD9",
|
|
329
|
+
"\u8981",
|
|
330
|
+
"\u4F1A",
|
|
331
|
+
"\u5BF9",
|
|
332
|
+
"\u8BF4",
|
|
333
|
+
"\u800C",
|
|
334
|
+
"\u53BB",
|
|
335
|
+
"\u4E4B",
|
|
336
|
+
"\u88AB",
|
|
337
|
+
"\u5979",
|
|
338
|
+
"\u628A",
|
|
339
|
+
"\u90A3"
|
|
340
|
+
]);
|
|
341
|
+
function tokenize(text) {
|
|
342
|
+
const cleaned = text.replace(/[^\w\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af\s]/g, " ");
|
|
343
|
+
const tokens = [];
|
|
344
|
+
const latinWords = cleaned.replace(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/g, " ").split(/\s+/).filter((w) => w.length > 1);
|
|
345
|
+
tokens.push(...latinWords);
|
|
346
|
+
const cjkChunks = cleaned.match(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]+/g);
|
|
347
|
+
if (cjkChunks && cjkChunks.length > 0) {
|
|
348
|
+
const jieba = getJieba();
|
|
349
|
+
for (const chunk of cjkChunks) {
|
|
350
|
+
if (jieba) {
|
|
351
|
+
const words = jieba.cutForSearch(chunk).filter((w) => w.length >= 1);
|
|
352
|
+
tokens.push(...words);
|
|
353
|
+
} else {
|
|
354
|
+
for (const ch of chunk) {
|
|
355
|
+
tokens.push(ch);
|
|
356
|
+
}
|
|
357
|
+
for (let i = 0; i < chunk.length - 1; i++) {
|
|
358
|
+
tokens.push(chunk[i] + chunk[i + 1]);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const unique = [...new Set(tokens)].filter((t) => t.length > 0 && !STOPWORDS.has(t)).slice(0, 30);
|
|
364
|
+
return unique;
|
|
365
|
+
}
|
|
366
|
+
function tokenizeForIndex(text) {
|
|
367
|
+
const tokens = tokenize(text);
|
|
368
|
+
return tokens.join(" ");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/core/memory.ts
|
|
108
372
|
function contentHash(content) {
|
|
109
373
|
return createHash("sha256").update(content.trim()).digest("hex").slice(0, 16);
|
|
110
374
|
}
|
|
@@ -152,7 +416,7 @@ function createMemory(db, input) {
|
|
|
152
416
|
agentId,
|
|
153
417
|
hash
|
|
154
418
|
);
|
|
155
|
-
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, input.content);
|
|
419
|
+
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
|
|
156
420
|
return getMemory(db, id);
|
|
157
421
|
}
|
|
158
422
|
function getMemory(db, id) {
|
|
@@ -197,7 +461,7 @@ function updateMemory(db, id, input) {
|
|
|
197
461
|
db.prepare(`UPDATE memories SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
198
462
|
if (input.content !== void 0) {
|
|
199
463
|
db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
|
|
200
|
-
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, input.content);
|
|
464
|
+
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
|
|
201
465
|
}
|
|
202
466
|
return getMemory(db, id);
|
|
203
467
|
}
|
|
@@ -250,95 +514,134 @@ function countMemories(db, agent_id = "default") {
|
|
|
250
514
|
};
|
|
251
515
|
}
|
|
252
516
|
|
|
253
|
-
// src/
|
|
254
|
-
|
|
255
|
-
|
|
517
|
+
// src/core/export.ts
|
|
518
|
+
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
519
|
+
import { join } from "path";
|
|
520
|
+
function exportMemories(db, dirPath, opts) {
|
|
256
521
|
const agentId = opts?.agent_id ?? "default";
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
522
|
+
if (!existsSync(dirPath)) mkdirSync(dirPath, { recursive: true });
|
|
523
|
+
let exported = 0;
|
|
524
|
+
const files = [];
|
|
525
|
+
const identities = listMemories(db, { agent_id: agentId, type: "identity" });
|
|
526
|
+
const knowledge = listMemories(db, { agent_id: agentId, type: "knowledge" });
|
|
527
|
+
const emotions = listMemories(db, { agent_id: agentId, type: "emotion" });
|
|
528
|
+
if (identities.length || knowledge.length || emotions.length) {
|
|
529
|
+
const sections = ["# Agent Memory Export\n"];
|
|
530
|
+
if (identities.length) {
|
|
531
|
+
sections.push("## Identity\n");
|
|
532
|
+
for (const m of identities) {
|
|
533
|
+
sections.push(`- ${m.content}
|
|
534
|
+
`);
|
|
535
|
+
exported++;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (emotions.length) {
|
|
539
|
+
sections.push("\n## Emotions\n");
|
|
540
|
+
for (const m of emotions) {
|
|
541
|
+
sections.push(`- ${m.content}
|
|
542
|
+
`);
|
|
543
|
+
exported++;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (knowledge.length) {
|
|
547
|
+
sections.push("\n## Knowledge\n");
|
|
548
|
+
for (const m of knowledge) {
|
|
549
|
+
sections.push(`- ${m.content}
|
|
550
|
+
`);
|
|
551
|
+
exported++;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
const memoryPath = join(dirPath, "MEMORY.md");
|
|
555
|
+
writeFileSync(memoryPath, sections.join("\n"));
|
|
556
|
+
files.push(memoryPath);
|
|
279
557
|
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
558
|
+
const events = listMemories(db, { agent_id: agentId, type: "event", limit: 1e4 });
|
|
559
|
+
const byDate = /* @__PURE__ */ new Map();
|
|
560
|
+
for (const ev of events) {
|
|
561
|
+
const date = ev.created_at.slice(0, 10);
|
|
562
|
+
if (!byDate.has(date)) byDate.set(date, []);
|
|
563
|
+
byDate.get(date).push(ev);
|
|
564
|
+
}
|
|
565
|
+
for (const [date, mems] of byDate) {
|
|
566
|
+
const lines = [`# ${date}
|
|
567
|
+
`];
|
|
568
|
+
for (const m of mems) {
|
|
569
|
+
lines.push(`- ${m.content}
|
|
570
|
+
`);
|
|
571
|
+
exported++;
|
|
572
|
+
}
|
|
573
|
+
const filePath = join(dirPath, `${date}.md`);
|
|
574
|
+
writeFileSync(filePath, lines.join("\n"));
|
|
575
|
+
files.push(filePath);
|
|
576
|
+
}
|
|
577
|
+
return { exported, files };
|
|
299
578
|
}
|
|
300
579
|
|
|
301
580
|
// src/search/intent.ts
|
|
302
581
|
var INTENT_PATTERNS = {
|
|
303
582
|
factual: [
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
583
|
+
// English
|
|
584
|
+
/^(what|who|where|which|how much|how many)\b/i,
|
|
585
|
+
/\b(name|address|number|password|config|setting)\b/i,
|
|
586
|
+
// Chinese - questions about facts
|
|
587
|
+
/是(什么|谁|哪|啥)/,
|
|
588
|
+
/叫(什么|啥)/,
|
|
589
|
+
/(名字|地址|号码|密码|配置|设置|账号|邮箱|链接|版本)/,
|
|
590
|
+
/(多少|几个|哪个|哪些|哪里)/,
|
|
591
|
+
// Chinese - lookup patterns
|
|
592
|
+
/(查一下|找一下|看看|搜一下)/,
|
|
593
|
+
/(.+)是什么$/
|
|
313
594
|
],
|
|
314
595
|
temporal: [
|
|
315
|
-
|
|
316
|
-
|
|
596
|
+
// English
|
|
597
|
+
/^(when|what time|how long)\b/i,
|
|
598
|
+
/\b(yesterday|today|tomorrow|last week|recently|ago|before|after)\b/i,
|
|
599
|
+
/\b(first|latest|newest|oldest|previous|next)\b/i,
|
|
600
|
+
// Chinese - time expressions
|
|
317
601
|
/什么时候/,
|
|
318
|
-
/(
|
|
319
|
-
|
|
320
|
-
/(
|
|
602
|
+
/(昨天|今天|明天|上周|下周|最近|以前|之前|之后|刚才|刚刚)/,
|
|
603
|
+
/(几月|几号|几点|多久|多长时间)/,
|
|
604
|
+
/(上次|下次|第一次|最后一次|那天|那时)/,
|
|
605
|
+
// Date patterns
|
|
606
|
+
/\d{4}[-/.]\d{1,2}/,
|
|
607
|
+
/\d{1,2}月\d{1,2}[日号]/,
|
|
608
|
+
// Chinese - temporal context
|
|
609
|
+
/(历史|记录|日志|以来|至今|期间)/
|
|
321
610
|
],
|
|
322
611
|
causal: [
|
|
323
|
-
|
|
324
|
-
/^(
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
612
|
+
// English
|
|
613
|
+
/^(why|how come|what caused)\b/i,
|
|
614
|
+
/\b(because|due to|reason|cause|result)\b/i,
|
|
615
|
+
// Chinese - causal questions
|
|
616
|
+
/为(什么|啥|何)/,
|
|
617
|
+
/(原因|导致|造成|引起|因为|所以|结果)/,
|
|
618
|
+
/(怎么回事|怎么了|咋回事|咋了)/,
|
|
619
|
+
/(为啥|凭啥|凭什么)/,
|
|
620
|
+
// Chinese - problem/diagnosis
|
|
621
|
+
/(出(了|了什么)?问题|报错|失败|出错|bug)/
|
|
330
622
|
],
|
|
331
623
|
exploratory: [
|
|
332
|
-
|
|
333
|
-
/^(
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
624
|
+
// English
|
|
625
|
+
/^(how|tell me about|explain|describe|show me)\b/i,
|
|
626
|
+
/^(what do you think|what about|any)\b/i,
|
|
627
|
+
/\b(overview|summary|list|compare)\b/i,
|
|
628
|
+
// Chinese - exploratory
|
|
629
|
+
/(怎么样|怎样|如何)/,
|
|
630
|
+
/(介绍|说说|讲讲|聊聊|谈谈)/,
|
|
631
|
+
/(有哪些|有什么|有没有)/,
|
|
632
|
+
/(关于|对于|至于|关联)/,
|
|
633
|
+
/(总结|概括|梳理|回顾|盘点)/,
|
|
634
|
+
// Chinese - opinion/analysis
|
|
635
|
+
/(看法|想法|意见|建议|评价|感觉|觉得)/,
|
|
636
|
+
/(对比|比较|区别|差异|优缺点)/
|
|
340
637
|
]
|
|
341
638
|
};
|
|
639
|
+
var CN_STRUCTURE_BOOSTS = {
|
|
640
|
+
factual: [/^.{1,6}(是什么|叫什么|在哪)/, /^(谁|哪)/],
|
|
641
|
+
temporal: [/^(什么时候|上次|最近)/, /(时间|日期)$/],
|
|
642
|
+
causal: [/^(为什么|为啥)/, /(为什么|怎么回事)$/],
|
|
643
|
+
exploratory: [/^(怎么|如何|说说)/, /(哪些|什么样)$/]
|
|
644
|
+
};
|
|
342
645
|
function classifyIntent(query) {
|
|
343
646
|
const scores = {
|
|
344
647
|
factual: 0,
|
|
@@ -353,6 +656,18 @@ function classifyIntent(query) {
|
|
|
353
656
|
}
|
|
354
657
|
}
|
|
355
658
|
}
|
|
659
|
+
for (const [intent, patterns] of Object.entries(CN_STRUCTURE_BOOSTS)) {
|
|
660
|
+
for (const pattern of patterns) {
|
|
661
|
+
if (pattern.test(query)) {
|
|
662
|
+
scores[intent] += 0.5;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const tokens = tokenize(query);
|
|
667
|
+
const totalPatternScore = Object.values(scores).reduce((a, b) => a + b, 0);
|
|
668
|
+
if (totalPatternScore === 0 && tokens.length <= 3) {
|
|
669
|
+
scores.factual += 1;
|
|
670
|
+
}
|
|
356
671
|
let maxIntent = "factual";
|
|
357
672
|
let maxScore = 0;
|
|
358
673
|
for (const [intent, score] of Object.entries(scores)) {
|
|
@@ -362,7 +677,7 @@ function classifyIntent(query) {
|
|
|
362
677
|
}
|
|
363
678
|
}
|
|
364
679
|
const totalScore = Object.values(scores).reduce((a, b) => a + b, 0);
|
|
365
|
-
const confidence = totalScore > 0 ? maxScore / totalScore : 0.5;
|
|
680
|
+
const confidence = totalScore > 0 ? Math.min(0.95, maxScore / totalScore) : 0.5;
|
|
366
681
|
return { intent: maxIntent, confidence };
|
|
367
682
|
}
|
|
368
683
|
function getStrategy(intent) {
|
|
@@ -400,6 +715,346 @@ function rerank(results, opts) {
|
|
|
400
715
|
return scored.slice(0, opts.limit);
|
|
401
716
|
}
|
|
402
717
|
|
|
718
|
+
// src/search/bm25.ts
|
|
719
|
+
function searchBM25(db, query, opts) {
|
|
720
|
+
const limit = opts?.limit ?? 20;
|
|
721
|
+
const agentId = opts?.agent_id ?? "default";
|
|
722
|
+
const minVitality = opts?.min_vitality ?? 0;
|
|
723
|
+
const ftsQuery = buildFtsQuery(query);
|
|
724
|
+
if (!ftsQuery) return [];
|
|
725
|
+
try {
|
|
726
|
+
const rows = db.prepare(
|
|
727
|
+
`SELECT m.*, rank AS score
|
|
728
|
+
FROM memories_fts f
|
|
729
|
+
JOIN memories m ON m.id = f.id
|
|
730
|
+
WHERE memories_fts MATCH ?
|
|
731
|
+
AND m.agent_id = ?
|
|
732
|
+
AND m.vitality >= ?
|
|
733
|
+
ORDER BY rank
|
|
734
|
+
LIMIT ?`
|
|
735
|
+
).all(ftsQuery, agentId, minVitality, limit);
|
|
736
|
+
return rows.map((row) => {
|
|
737
|
+
const { score: _score, ...memoryFields } = row;
|
|
738
|
+
return {
|
|
739
|
+
memory: memoryFields,
|
|
740
|
+
score: Math.abs(row.score),
|
|
741
|
+
// FTS5 rank is negative (lower = better)
|
|
742
|
+
matchReason: "bm25"
|
|
743
|
+
};
|
|
744
|
+
});
|
|
745
|
+
} catch {
|
|
746
|
+
return searchSimple(db, query, agentId, minVitality, limit);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
function searchSimple(db, query, agentId, minVitality, limit) {
|
|
750
|
+
const rows = db.prepare(
|
|
751
|
+
`SELECT * FROM memories
|
|
752
|
+
WHERE agent_id = ? AND vitality >= ? AND content LIKE ?
|
|
753
|
+
ORDER BY priority ASC, updated_at DESC
|
|
754
|
+
LIMIT ?`
|
|
755
|
+
).all(agentId, minVitality, `%${query}%`, limit);
|
|
756
|
+
return rows.map((m, i) => ({
|
|
757
|
+
memory: m,
|
|
758
|
+
score: 1 / (i + 1),
|
|
759
|
+
// Simple rank by position
|
|
760
|
+
matchReason: "like"
|
|
761
|
+
}));
|
|
762
|
+
}
|
|
763
|
+
function buildFtsQuery(text) {
|
|
764
|
+
const tokens = tokenize(text);
|
|
765
|
+
if (tokens.length === 0) return null;
|
|
766
|
+
return tokens.map((w) => `"${w}"`).join(" OR ");
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// src/search/embeddings.ts
|
|
770
|
+
function encodeEmbedding(vector) {
|
|
771
|
+
const arr = vector instanceof Float32Array ? vector : Float32Array.from(vector);
|
|
772
|
+
return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
773
|
+
}
|
|
774
|
+
function decodeEmbedding(buf) {
|
|
775
|
+
const copy = Buffer.from(buf);
|
|
776
|
+
return new Float32Array(copy.buffer, copy.byteOffset, Math.floor(copy.byteLength / 4));
|
|
777
|
+
}
|
|
778
|
+
function upsertEmbedding(db, input) {
|
|
779
|
+
const ts = now();
|
|
780
|
+
const vec = input.vector instanceof Float32Array ? input.vector : Float32Array.from(input.vector);
|
|
781
|
+
const blob = encodeEmbedding(vec);
|
|
782
|
+
db.prepare(
|
|
783
|
+
`INSERT INTO embeddings (agent_id, memory_id, model, dim, vector, created_at, updated_at)
|
|
784
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
785
|
+
ON CONFLICT(agent_id, memory_id, model) DO UPDATE SET
|
|
786
|
+
dim = excluded.dim,
|
|
787
|
+
vector = excluded.vector,
|
|
788
|
+
updated_at = excluded.updated_at`
|
|
789
|
+
).run(input.agent_id, input.memory_id, input.model, vec.length, blob, ts, ts);
|
|
790
|
+
}
|
|
791
|
+
function listEmbeddings(db, agent_id, model) {
|
|
792
|
+
const rows = db.prepare(
|
|
793
|
+
"SELECT memory_id, vector FROM embeddings WHERE agent_id = ? AND model = ?"
|
|
794
|
+
).all(agent_id, model);
|
|
795
|
+
return rows.map((r) => ({ memory_id: r.memory_id, vector: decodeEmbedding(r.vector) }));
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// src/search/hybrid.ts
|
|
799
|
+
function cosine(a, b) {
|
|
800
|
+
const n = Math.min(a.length, b.length);
|
|
801
|
+
let dot = 0;
|
|
802
|
+
let na = 0;
|
|
803
|
+
let nb = 0;
|
|
804
|
+
for (let i = 0; i < n; i++) {
|
|
805
|
+
const x = a[i];
|
|
806
|
+
const y = b[i];
|
|
807
|
+
dot += x * y;
|
|
808
|
+
na += x * x;
|
|
809
|
+
nb += y * y;
|
|
810
|
+
}
|
|
811
|
+
if (na === 0 || nb === 0) return 0;
|
|
812
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
813
|
+
}
|
|
814
|
+
function rrfScore(rank, k) {
|
|
815
|
+
return 1 / (k + rank);
|
|
816
|
+
}
|
|
817
|
+
function fuseRrf(lists, k) {
|
|
818
|
+
const out = /* @__PURE__ */ new Map();
|
|
819
|
+
for (const list of lists) {
|
|
820
|
+
for (let i = 0; i < list.items.length; i++) {
|
|
821
|
+
const it = list.items[i];
|
|
822
|
+
const rank = i + 1;
|
|
823
|
+
const add = rrfScore(rank, k);
|
|
824
|
+
const prev = out.get(it.id);
|
|
825
|
+
if (!prev) out.set(it.id, { score: add, sources: [list.name] });
|
|
826
|
+
else {
|
|
827
|
+
prev.score += add;
|
|
828
|
+
if (!prev.sources.includes(list.name)) prev.sources.push(list.name);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return out;
|
|
833
|
+
}
|
|
834
|
+
function fetchMemories(db, ids, agentId) {
|
|
835
|
+
if (ids.length === 0) return [];
|
|
836
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
837
|
+
const sql = agentId ? `SELECT * FROM memories WHERE id IN (${placeholders}) AND agent_id = ?` : `SELECT * FROM memories WHERE id IN (${placeholders})`;
|
|
838
|
+
const rows = db.prepare(sql).all(...agentId ? [...ids, agentId] : ids);
|
|
839
|
+
return rows;
|
|
840
|
+
}
|
|
841
|
+
async function searchHybrid(db, query, opts) {
|
|
842
|
+
const agentId = opts?.agent_id ?? "default";
|
|
843
|
+
const limit = opts?.limit ?? 10;
|
|
844
|
+
const bm25Mult = opts?.bm25CandidateMultiplier ?? 3;
|
|
845
|
+
const semanticCandidates = opts?.semanticCandidates ?? 50;
|
|
846
|
+
const rrfK = opts?.rrfK ?? 60;
|
|
847
|
+
const bm25 = searchBM25(db, query, {
|
|
848
|
+
agent_id: agentId,
|
|
849
|
+
limit: limit * bm25Mult
|
|
850
|
+
});
|
|
851
|
+
const provider = opts?.embeddingProvider ?? null;
|
|
852
|
+
const model = opts?.embeddingModel ?? provider?.model;
|
|
853
|
+
if (!provider || !model) {
|
|
854
|
+
return bm25.slice(0, limit);
|
|
855
|
+
}
|
|
856
|
+
const embedFn = provider.embedQuery ?? provider.embed;
|
|
857
|
+
const qVec = Float32Array.from(await embedFn.call(provider, query));
|
|
858
|
+
const embeddings = listEmbeddings(db, agentId, model);
|
|
859
|
+
const scored = [];
|
|
860
|
+
for (const e of embeddings) {
|
|
861
|
+
scored.push({ id: e.memory_id, score: cosine(qVec, e.vector) });
|
|
862
|
+
}
|
|
863
|
+
scored.sort((a, b) => b.score - a.score);
|
|
864
|
+
const semanticTop = scored.slice(0, semanticCandidates);
|
|
865
|
+
const fused = fuseRrf(
|
|
866
|
+
[
|
|
867
|
+
{ name: "bm25", items: bm25.map((r) => ({ id: r.memory.id, score: r.score })) },
|
|
868
|
+
{ name: "semantic", items: semanticTop }
|
|
869
|
+
],
|
|
870
|
+
rrfK
|
|
871
|
+
);
|
|
872
|
+
const ids = [...fused.keys()];
|
|
873
|
+
const memories = fetchMemories(db, ids, agentId);
|
|
874
|
+
const byId = new Map(memories.map((m) => [m.id, m]));
|
|
875
|
+
const out = [];
|
|
876
|
+
for (const [id, meta] of fused) {
|
|
877
|
+
const mem = byId.get(id);
|
|
878
|
+
if (!mem) continue;
|
|
879
|
+
out.push({
|
|
880
|
+
memory: mem,
|
|
881
|
+
score: meta.score,
|
|
882
|
+
matchReason: meta.sources.sort().join("+")
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
out.sort((a, b) => b.score - a.score);
|
|
886
|
+
return out.slice(0, limit);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// src/search/providers.ts
|
|
890
|
+
var QWEN_DEFAULT_INSTRUCTION = "Given a query, retrieve the most semantically relevant document";
|
|
891
|
+
function getDefaultInstruction(model) {
|
|
892
|
+
const m = model.toLowerCase();
|
|
893
|
+
if (m.includes("qwen")) return QWEN_DEFAULT_INSTRUCTION;
|
|
894
|
+
if (m.includes("gemini")) return null;
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
function resolveInstruction(model) {
|
|
898
|
+
const override = process.env.AGENT_MEMORY_EMBEDDINGS_INSTRUCTION;
|
|
899
|
+
if (override !== void 0) {
|
|
900
|
+
const normalized = override.trim();
|
|
901
|
+
if (!normalized) return null;
|
|
902
|
+
const lowered = normalized.toLowerCase();
|
|
903
|
+
if (lowered === "none" || lowered === "off" || lowered === "false" || lowered === "null") return null;
|
|
904
|
+
return normalized;
|
|
905
|
+
}
|
|
906
|
+
return getDefaultInstruction(model);
|
|
907
|
+
}
|
|
908
|
+
function buildQueryInput(query, instructionPrefix) {
|
|
909
|
+
if (!instructionPrefix) return query;
|
|
910
|
+
return `Instruct: ${instructionPrefix}
|
|
911
|
+
Query: ${query}`;
|
|
912
|
+
}
|
|
913
|
+
function getEmbeddingProviderFromEnv() {
|
|
914
|
+
const provider = (process.env.AGENT_MEMORY_EMBEDDINGS_PROVIDER ?? "none").toLowerCase();
|
|
915
|
+
if (provider === "none" || provider === "off" || provider === "false") return null;
|
|
916
|
+
if (provider === "openai") {
|
|
917
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
918
|
+
const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-3-small";
|
|
919
|
+
const baseUrl = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1";
|
|
920
|
+
if (!apiKey) return null;
|
|
921
|
+
const instruction = resolveInstruction(model);
|
|
922
|
+
return createOpenAIProvider({ apiKey, model, baseUrl, instruction });
|
|
923
|
+
}
|
|
924
|
+
if (provider === "gemini" || provider === "google") {
|
|
925
|
+
const apiKey = process.env.GEMINI_API_KEY ?? process.env.OPENAI_API_KEY;
|
|
926
|
+
const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "gemini-embedding-001";
|
|
927
|
+
const baseUrl = process.env.GEMINI_BASE_URL ?? process.env.OPENAI_BASE_URL ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
928
|
+
if (!apiKey) return null;
|
|
929
|
+
const instruction = resolveInstruction(model);
|
|
930
|
+
return createOpenAIProvider({ id: "gemini", apiKey, model, baseUrl, instruction });
|
|
931
|
+
}
|
|
932
|
+
if (provider === "qwen" || provider === "dashscope" || provider === "tongyi") {
|
|
933
|
+
const apiKey = process.env.DASHSCOPE_API_KEY;
|
|
934
|
+
const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-v3";
|
|
935
|
+
const baseUrl = process.env.DASHSCOPE_BASE_URL ?? "https://dashscope.aliyuncs.com";
|
|
936
|
+
if (!apiKey) return null;
|
|
937
|
+
const instruction = resolveInstruction(model);
|
|
938
|
+
return createDashScopeProvider({ apiKey, model, baseUrl, instruction });
|
|
939
|
+
}
|
|
940
|
+
return null;
|
|
941
|
+
}
|
|
942
|
+
function authHeader(apiKey) {
|
|
943
|
+
return apiKey.startsWith("Bearer ") ? apiKey : `Bearer ${apiKey}`;
|
|
944
|
+
}
|
|
945
|
+
function normalizeEmbedding(e) {
|
|
946
|
+
if (!Array.isArray(e)) throw new Error("Invalid embedding: not an array");
|
|
947
|
+
if (e.length === 0) throw new Error("Invalid embedding: empty");
|
|
948
|
+
return e.map((x) => {
|
|
949
|
+
if (typeof x !== "number" || !Number.isFinite(x)) throw new Error("Invalid embedding: non-numeric value");
|
|
950
|
+
return x;
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
function createOpenAIProvider(opts) {
|
|
954
|
+
const baseUrl = opts.baseUrl ?? "https://api.openai.com/v1";
|
|
955
|
+
const instructionPrefix = opts.instruction ?? null;
|
|
956
|
+
async function requestEmbedding(input) {
|
|
957
|
+
const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/embeddings`, {
|
|
958
|
+
method: "POST",
|
|
959
|
+
headers: {
|
|
960
|
+
"content-type": "application/json",
|
|
961
|
+
authorization: authHeader(opts.apiKey)
|
|
962
|
+
},
|
|
963
|
+
body: JSON.stringify({ model: opts.model, input })
|
|
964
|
+
});
|
|
965
|
+
if (!resp.ok) {
|
|
966
|
+
const body = await resp.text().catch(() => "");
|
|
967
|
+
throw new Error(`OpenAI embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
|
|
968
|
+
}
|
|
969
|
+
const data = await resp.json();
|
|
970
|
+
return normalizeEmbedding(data.data?.[0]?.embedding);
|
|
971
|
+
}
|
|
972
|
+
return {
|
|
973
|
+
id: opts.id ?? "openai",
|
|
974
|
+
model: opts.model,
|
|
975
|
+
instructionPrefix,
|
|
976
|
+
async embed(text) {
|
|
977
|
+
return requestEmbedding(text);
|
|
978
|
+
},
|
|
979
|
+
async embedQuery(query) {
|
|
980
|
+
return requestEmbedding(buildQueryInput(query, instructionPrefix));
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
function createDashScopeProvider(opts) {
|
|
985
|
+
const baseUrl = opts.baseUrl ?? "https://dashscope.aliyuncs.com";
|
|
986
|
+
const instructionPrefix = opts.instruction ?? null;
|
|
987
|
+
async function requestEmbedding(text) {
|
|
988
|
+
const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/services/embeddings/text-embedding/text-embedding`, {
|
|
989
|
+
method: "POST",
|
|
990
|
+
headers: {
|
|
991
|
+
"content-type": "application/json",
|
|
992
|
+
authorization: authHeader(opts.apiKey)
|
|
993
|
+
},
|
|
994
|
+
body: JSON.stringify({
|
|
995
|
+
model: opts.model,
|
|
996
|
+
input: { texts: [text] }
|
|
997
|
+
})
|
|
998
|
+
});
|
|
999
|
+
if (!resp.ok) {
|
|
1000
|
+
const body = await resp.text().catch(() => "");
|
|
1001
|
+
throw new Error(`DashScope embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
|
|
1002
|
+
}
|
|
1003
|
+
const data = await resp.json();
|
|
1004
|
+
const emb = data.output?.embeddings?.[0]?.embedding ?? data.output?.embeddings?.[0]?.vector ?? data.output?.embedding ?? data.data?.[0]?.embedding;
|
|
1005
|
+
return normalizeEmbedding(emb);
|
|
1006
|
+
}
|
|
1007
|
+
return {
|
|
1008
|
+
id: "dashscope",
|
|
1009
|
+
model: opts.model,
|
|
1010
|
+
instructionPrefix,
|
|
1011
|
+
async embed(text) {
|
|
1012
|
+
return requestEmbedding(text);
|
|
1013
|
+
},
|
|
1014
|
+
async embedQuery(query) {
|
|
1015
|
+
return requestEmbedding(buildQueryInput(query, instructionPrefix));
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/search/embed.ts
|
|
1021
|
+
async function embedMemory(db, memoryId, provider, opts) {
|
|
1022
|
+
const row = db.prepare("SELECT id, agent_id, content FROM memories WHERE id = ?").get(memoryId);
|
|
1023
|
+
if (!row) return false;
|
|
1024
|
+
if (opts?.agent_id && row.agent_id !== opts.agent_id) return false;
|
|
1025
|
+
const model = opts?.model ?? provider.model;
|
|
1026
|
+
const maxChars = opts?.maxChars ?? 2e3;
|
|
1027
|
+
const text = row.content.length > maxChars ? row.content.slice(0, maxChars) : row.content;
|
|
1028
|
+
const vector = await provider.embed(text);
|
|
1029
|
+
upsertEmbedding(db, {
|
|
1030
|
+
agent_id: row.agent_id,
|
|
1031
|
+
memory_id: row.id,
|
|
1032
|
+
model,
|
|
1033
|
+
vector
|
|
1034
|
+
});
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
async function embedMissingForAgent(db, provider, opts) {
|
|
1038
|
+
const agentId = opts?.agent_id ?? "default";
|
|
1039
|
+
const model = opts?.model ?? provider.model;
|
|
1040
|
+
const limit = opts?.limit ?? 1e3;
|
|
1041
|
+
const rows = db.prepare(
|
|
1042
|
+
`SELECT m.id
|
|
1043
|
+
FROM memories m
|
|
1044
|
+
LEFT JOIN embeddings e
|
|
1045
|
+
ON e.memory_id = m.id AND e.agent_id = m.agent_id AND e.model = ?
|
|
1046
|
+
WHERE m.agent_id = ? AND e.memory_id IS NULL
|
|
1047
|
+
ORDER BY m.updated_at DESC
|
|
1048
|
+
LIMIT ?`
|
|
1049
|
+
).all(model, agentId, limit);
|
|
1050
|
+
let embedded = 0;
|
|
1051
|
+
for (const r of rows) {
|
|
1052
|
+
const ok = await embedMemory(db, r.id, provider, { agent_id: agentId, model, maxChars: opts?.maxChars });
|
|
1053
|
+
if (ok) embedded++;
|
|
1054
|
+
}
|
|
1055
|
+
return { embedded, scanned: rows.length };
|
|
1056
|
+
}
|
|
1057
|
+
|
|
403
1058
|
// src/core/path.ts
|
|
404
1059
|
var DEFAULT_DOMAINS = /* @__PURE__ */ new Set(["core", "emotion", "knowledge", "event", "system"]);
|
|
405
1060
|
function parseUri(uri) {
|
|
@@ -407,27 +1062,33 @@ function parseUri(uri) {
|
|
|
407
1062
|
if (!match) throw new Error(`Invalid URI: ${uri}. Expected format: domain://path`);
|
|
408
1063
|
return { domain: match[1], path: match[2] };
|
|
409
1064
|
}
|
|
410
|
-
function createPath(db, memoryId, uri, alias, validDomains) {
|
|
1065
|
+
function createPath(db, memoryId, uri, alias, validDomains, agent_id) {
|
|
411
1066
|
const { domain } = parseUri(uri);
|
|
412
1067
|
const domains = validDomains ?? DEFAULT_DOMAINS;
|
|
413
1068
|
if (!domains.has(domain)) {
|
|
414
1069
|
throw new Error(`Invalid domain "${domain}". Valid: ${[...domains].join(", ")}`);
|
|
415
1070
|
}
|
|
416
|
-
const
|
|
1071
|
+
const memoryAgent = db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(memoryId)?.agent_id;
|
|
1072
|
+
if (!memoryAgent) throw new Error(`Memory not found: ${memoryId}`);
|
|
1073
|
+
if (agent_id && agent_id !== memoryAgent) {
|
|
1074
|
+
throw new Error(`Agent mismatch for path: memory agent_id=${memoryAgent}, requested agent_id=${agent_id}`);
|
|
1075
|
+
}
|
|
1076
|
+
const agentId = agent_id ?? memoryAgent;
|
|
1077
|
+
const existing = db.prepare("SELECT id FROM paths WHERE agent_id = ? AND uri = ?").get(agentId, uri);
|
|
417
1078
|
if (existing) {
|
|
418
1079
|
throw new Error(`URI already exists: ${uri}`);
|
|
419
1080
|
}
|
|
420
1081
|
const id = newId();
|
|
421
1082
|
db.prepare(
|
|
422
|
-
"INSERT INTO paths (id, memory_id, uri, alias, domain, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
423
|
-
).run(id, memoryId, uri, alias ?? null, domain, now());
|
|
1083
|
+
"INSERT INTO paths (id, memory_id, agent_id, uri, alias, domain, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
1084
|
+
).run(id, memoryId, agentId, uri, alias ?? null, domain, now());
|
|
424
1085
|
return getPath(db, id);
|
|
425
1086
|
}
|
|
426
1087
|
function getPath(db, id) {
|
|
427
1088
|
return db.prepare("SELECT * FROM paths WHERE id = ?").get(id) ?? null;
|
|
428
1089
|
}
|
|
429
|
-
function getPathByUri(db, uri) {
|
|
430
|
-
return db.prepare("SELECT * FROM paths WHERE uri = ?").get(uri) ?? null;
|
|
1090
|
+
function getPathByUri(db, uri, agent_id = "default") {
|
|
1091
|
+
return db.prepare("SELECT * FROM paths WHERE agent_id = ? AND uri = ?").get(agent_id, uri) ?? null;
|
|
431
1092
|
}
|
|
432
1093
|
|
|
433
1094
|
// src/sleep/boot.ts
|
|
@@ -447,7 +1108,7 @@ function boot(db, opts) {
|
|
|
447
1108
|
}
|
|
448
1109
|
const bootPaths = [];
|
|
449
1110
|
for (const uri of corePaths) {
|
|
450
|
-
const path = getPathByUri(db, uri);
|
|
1111
|
+
const path = getPathByUri(db, uri, agentId);
|
|
451
1112
|
if (path) {
|
|
452
1113
|
bootPaths.push(uri);
|
|
453
1114
|
if (!memories.has(path.memory_id)) {
|
|
@@ -459,13 +1120,13 @@ function boot(db, opts) {
|
|
|
459
1120
|
}
|
|
460
1121
|
}
|
|
461
1122
|
}
|
|
462
|
-
const bootEntry = getPathByUri(db, "system://boot");
|
|
1123
|
+
const bootEntry = getPathByUri(db, "system://boot", agentId);
|
|
463
1124
|
if (bootEntry) {
|
|
464
1125
|
const bootMem = getMemory(db, bootEntry.memory_id);
|
|
465
1126
|
if (bootMem) {
|
|
466
1127
|
const additionalUris = bootMem.content.split("\n").map((l) => l.trim()).filter((l) => l.match(/^[a-z]+:\/\//));
|
|
467
1128
|
for (const uri of additionalUris) {
|
|
468
|
-
const path = getPathByUri(db, uri);
|
|
1129
|
+
const path = getPathByUri(db, uri, agentId);
|
|
469
1130
|
if (path && !memories.has(path.memory_id)) {
|
|
470
1131
|
const mem = getMemory(db, path.memory_id);
|
|
471
1132
|
if (mem) {
|
|
@@ -493,25 +1154,28 @@ var MIN_VITALITY = {
|
|
|
493
1154
|
3: 0
|
|
494
1155
|
// P3: event — full decay
|
|
495
1156
|
};
|
|
496
|
-
function calculateVitality(stability,
|
|
1157
|
+
function calculateVitality(stability, daysSinceLastAccess, priority) {
|
|
497
1158
|
if (priority === 0) return 1;
|
|
498
1159
|
const S = Math.max(0.01, stability);
|
|
499
|
-
const retention = Math.exp(-
|
|
1160
|
+
const retention = Math.exp(-daysSinceLastAccess / S);
|
|
500
1161
|
const minVit = MIN_VITALITY[priority] ?? 0;
|
|
501
1162
|
return Math.max(minVit, retention);
|
|
502
1163
|
}
|
|
503
|
-
function runDecay(db) {
|
|
1164
|
+
function runDecay(db, opts) {
|
|
504
1165
|
const currentTime = now();
|
|
505
1166
|
const currentMs = new Date(currentTime).getTime();
|
|
506
|
-
const
|
|
1167
|
+
const agentId = opts?.agent_id;
|
|
1168
|
+
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";
|
|
1169
|
+
const memories = db.prepare(query).all(...agentId ? [agentId] : []);
|
|
507
1170
|
let updated = 0;
|
|
508
1171
|
let decayed = 0;
|
|
509
1172
|
let belowThreshold = 0;
|
|
510
1173
|
const updateStmt = db.prepare("UPDATE memories SET vitality = ?, updated_at = ? WHERE id = ?");
|
|
511
1174
|
const transaction = db.transaction(() => {
|
|
512
1175
|
for (const mem of memories) {
|
|
513
|
-
const
|
|
514
|
-
const
|
|
1176
|
+
const referenceTime = mem.last_accessed ?? mem.created_at;
|
|
1177
|
+
const referenceMs = new Date(referenceTime).getTime();
|
|
1178
|
+
const daysSince = (currentMs - referenceMs) / (1e3 * 60 * 60 * 24);
|
|
515
1179
|
const newVitality = calculateVitality(mem.stability, daysSince, mem.priority);
|
|
516
1180
|
if (Math.abs(newVitality - mem.vitality) > 1e-3) {
|
|
517
1181
|
updateStmt.run(newVitality, currentTime, mem.id);
|
|
@@ -528,12 +1192,15 @@ function runDecay(db) {
|
|
|
528
1192
|
transaction();
|
|
529
1193
|
return { updated, decayed, belowThreshold };
|
|
530
1194
|
}
|
|
531
|
-
function getDecayedMemories(db, threshold = 0.05) {
|
|
1195
|
+
function getDecayedMemories(db, threshold = 0.05, opts) {
|
|
1196
|
+
const agentId = opts?.agent_id;
|
|
532
1197
|
return db.prepare(
|
|
533
|
-
`SELECT id, content, vitality, priority FROM memories
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
1198
|
+
agentId ? `SELECT id, content, vitality, priority FROM memories
|
|
1199
|
+
WHERE vitality < ? AND priority >= 3 AND agent_id = ?
|
|
1200
|
+
ORDER BY vitality ASC` : `SELECT id, content, vitality, priority FROM memories
|
|
1201
|
+
WHERE vitality < ? AND priority >= 3
|
|
1202
|
+
ORDER BY vitality ASC`
|
|
1203
|
+
).all(...agentId ? [threshold, agentId] : [threshold]);
|
|
537
1204
|
}
|
|
538
1205
|
|
|
539
1206
|
// src/core/snapshot.ts
|
|
@@ -552,11 +1219,12 @@ function createSnapshot(db, memoryId, action, changedBy) {
|
|
|
552
1219
|
function runTidy(db, opts) {
|
|
553
1220
|
const threshold = opts?.vitalityThreshold ?? 0.05;
|
|
554
1221
|
const maxSnapshots = opts?.maxSnapshotsPerMemory ?? 10;
|
|
1222
|
+
const agentId = opts?.agent_id;
|
|
555
1223
|
let archived = 0;
|
|
556
1224
|
let orphansCleaned = 0;
|
|
557
1225
|
let snapshotsPruned = 0;
|
|
558
1226
|
const transaction = db.transaction(() => {
|
|
559
|
-
const decayed = getDecayedMemories(db, threshold);
|
|
1227
|
+
const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
|
|
560
1228
|
for (const mem of decayed) {
|
|
561
1229
|
try {
|
|
562
1230
|
createSnapshot(db, mem.id, "delete", "tidy");
|
|
@@ -565,13 +1233,23 @@ function runTidy(db, opts) {
|
|
|
565
1233
|
deleteMemory(db, mem.id);
|
|
566
1234
|
archived++;
|
|
567
1235
|
}
|
|
568
|
-
const orphans = db.prepare(
|
|
569
|
-
`DELETE FROM paths
|
|
1236
|
+
const orphans = agentId ? db.prepare(
|
|
1237
|
+
`DELETE FROM paths
|
|
1238
|
+
WHERE agent_id = ?
|
|
1239
|
+
AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
|
|
1240
|
+
).run(agentId, agentId) : db.prepare(
|
|
1241
|
+
"DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)"
|
|
570
1242
|
).run();
|
|
571
1243
|
orphansCleaned = orphans.changes;
|
|
572
|
-
const memoriesWithSnapshots = db.prepare(
|
|
1244
|
+
const memoriesWithSnapshots = agentId ? db.prepare(
|
|
1245
|
+
`SELECT s.memory_id, COUNT(*) as cnt
|
|
1246
|
+
FROM snapshots s
|
|
1247
|
+
JOIN memories m ON m.id = s.memory_id
|
|
1248
|
+
WHERE m.agent_id = ?
|
|
1249
|
+
GROUP BY s.memory_id HAVING cnt > ?`
|
|
1250
|
+
).all(agentId, maxSnapshots) : db.prepare(
|
|
573
1251
|
`SELECT memory_id, COUNT(*) as cnt FROM snapshots
|
|
574
|
-
|
|
1252
|
+
GROUP BY memory_id HAVING cnt > ?`
|
|
575
1253
|
).all(maxSnapshots);
|
|
576
1254
|
for (const { memory_id } of memoriesWithSnapshots) {
|
|
577
1255
|
const pruned = db.prepare(
|
|
@@ -588,20 +1266,31 @@ function runTidy(db, opts) {
|
|
|
588
1266
|
}
|
|
589
1267
|
|
|
590
1268
|
// src/sleep/govern.ts
|
|
591
|
-
function runGovern(db) {
|
|
1269
|
+
function runGovern(db, opts) {
|
|
1270
|
+
const agentId = opts?.agent_id;
|
|
592
1271
|
let orphanPaths = 0;
|
|
593
1272
|
let orphanLinks = 0;
|
|
594
1273
|
let emptyMemories = 0;
|
|
595
1274
|
const transaction = db.transaction(() => {
|
|
596
|
-
const pathResult = db.prepare(
|
|
1275
|
+
const pathResult = agentId ? db.prepare(
|
|
1276
|
+
`DELETE FROM paths
|
|
1277
|
+
WHERE agent_id = ?
|
|
1278
|
+
AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
|
|
1279
|
+
).run(agentId, agentId) : db.prepare("DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)").run();
|
|
597
1280
|
orphanPaths = pathResult.changes;
|
|
598
|
-
const linkResult = db.prepare(
|
|
1281
|
+
const linkResult = agentId ? db.prepare(
|
|
1282
|
+
`DELETE FROM links WHERE
|
|
1283
|
+
agent_id = ? AND (
|
|
1284
|
+
source_id NOT IN (SELECT id FROM memories WHERE agent_id = ?) OR
|
|
1285
|
+
target_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)
|
|
1286
|
+
)`
|
|
1287
|
+
).run(agentId, agentId, agentId) : db.prepare(
|
|
599
1288
|
`DELETE FROM links WHERE
|
|
600
1289
|
source_id NOT IN (SELECT id FROM memories) OR
|
|
601
1290
|
target_id NOT IN (SELECT id FROM memories)`
|
|
602
1291
|
).run();
|
|
603
1292
|
orphanLinks = linkResult.changes;
|
|
604
|
-
const emptyResult = db.prepare("DELETE FROM memories WHERE TRIM(content) = ''").run();
|
|
1293
|
+
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();
|
|
605
1294
|
emptyMemories = emptyResult.changes;
|
|
606
1295
|
});
|
|
607
1296
|
transaction();
|
|
@@ -617,7 +1306,7 @@ function guard(db, input) {
|
|
|
617
1306
|
return { action: "skip", reason: "Exact duplicate (hash match)", existingId: exactMatch.id };
|
|
618
1307
|
}
|
|
619
1308
|
if (input.uri) {
|
|
620
|
-
const existingPath = getPathByUri(db, input.uri);
|
|
1309
|
+
const existingPath = getPathByUri(db, input.uri, agentId);
|
|
621
1310
|
if (existingPath) {
|
|
622
1311
|
return {
|
|
623
1312
|
action: "update",
|
|
@@ -626,40 +1315,85 @@ function guard(db, input) {
|
|
|
626
1315
|
};
|
|
627
1316
|
}
|
|
628
1317
|
}
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
1318
|
+
const ftsTokens = tokenize(input.content.slice(0, 200));
|
|
1319
|
+
const ftsQuery = ftsTokens.length > 0 ? ftsTokens.slice(0, 8).map((w) => `"${w}"`).join(" OR ") : null;
|
|
1320
|
+
if (ftsQuery) {
|
|
1321
|
+
try {
|
|
1322
|
+
const similar = db.prepare(
|
|
1323
|
+
`SELECT m.id, m.content, m.type, rank
|
|
1324
|
+
FROM memories_fts f
|
|
1325
|
+
JOIN memories m ON m.id = f.id
|
|
1326
|
+
WHERE memories_fts MATCH ? AND m.agent_id = ?
|
|
1327
|
+
ORDER BY rank
|
|
1328
|
+
LIMIT 3`
|
|
1329
|
+
).all(ftsQuery, agentId);
|
|
1330
|
+
if (similar.length > 0) {
|
|
1331
|
+
const topRank = Math.abs(similar[0].rank);
|
|
1332
|
+
const tokenCount = ftsTokens.length;
|
|
1333
|
+
const dynamicThreshold = tokenCount * 1.5;
|
|
1334
|
+
if (topRank > dynamicThreshold) {
|
|
1335
|
+
const existing = similar[0];
|
|
1336
|
+
if (existing.type === input.type) {
|
|
1337
|
+
const merged = `${existing.content}
|
|
641
1338
|
|
|
642
1339
|
[Updated] ${input.content}`;
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
1340
|
+
return {
|
|
1341
|
+
action: "merge",
|
|
1342
|
+
reason: `Similar content found (score=${topRank.toFixed(1)}, threshold=${dynamicThreshold.toFixed(1)}), merging`,
|
|
1343
|
+
existingId: existing.id,
|
|
1344
|
+
mergedContent: merged
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
} catch {
|
|
649
1350
|
}
|
|
650
1351
|
}
|
|
651
|
-
const
|
|
652
|
-
if (
|
|
653
|
-
|
|
654
|
-
return { action: "skip", reason: "Empty content rejected by gate" };
|
|
655
|
-
}
|
|
1352
|
+
const gateResult = fourCriterionGate(input);
|
|
1353
|
+
if (!gateResult.pass) {
|
|
1354
|
+
return { action: "skip", reason: `Gate rejected: ${gateResult.failedCriteria.join(", ")}` };
|
|
656
1355
|
}
|
|
657
1356
|
return { action: "add", reason: "Passed all guard checks" };
|
|
658
1357
|
}
|
|
659
|
-
function
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
1358
|
+
function fourCriterionGate(input) {
|
|
1359
|
+
const content = input.content.trim();
|
|
1360
|
+
const failed = [];
|
|
1361
|
+
const priority = input.priority ?? (input.type === "identity" ? 0 : input.type === "emotion" ? 1 : input.type === "knowledge" ? 2 : 3);
|
|
1362
|
+
const minLength = priority <= 1 ? 4 : 8;
|
|
1363
|
+
const specificity = content.length >= minLength ? Math.min(1, content.length / 50) : 0;
|
|
1364
|
+
if (specificity === 0) failed.push(`specificity (too short: ${content.length} < ${minLength} chars)`);
|
|
1365
|
+
const tokens = tokenize(content);
|
|
1366
|
+
const novelty = tokens.length >= 1 ? Math.min(1, tokens.length / 5) : 0;
|
|
1367
|
+
if (novelty === 0) failed.push("novelty (no meaningful tokens after filtering)");
|
|
1368
|
+
const hasCJK = /[\u4e00-\u9fff]/.test(content);
|
|
1369
|
+
const hasCapitalized = /[A-Z][a-z]+/.test(content);
|
|
1370
|
+
const hasNumbers = /\d+/.test(content);
|
|
1371
|
+
const hasURI = /\w+:\/\//.test(content);
|
|
1372
|
+
const hasEntityMarkers = /[@#]/.test(content);
|
|
1373
|
+
const hasMeaningfulLength = content.length >= 15;
|
|
1374
|
+
const topicSignals = [hasCJK, hasCapitalized, hasNumbers, hasURI, hasEntityMarkers, hasMeaningfulLength].filter(Boolean).length;
|
|
1375
|
+
const relevance = topicSignals >= 1 ? Math.min(1, topicSignals / 3) : 0;
|
|
1376
|
+
if (relevance === 0) failed.push("relevance (no identifiable topics/entities)");
|
|
1377
|
+
const allCaps = content === content.toUpperCase() && content.length > 20 && /^[A-Z\s]+$/.test(content);
|
|
1378
|
+
const hasWhitespaceOrPunctuation = /[\s,。!?,.!?;;::]/.test(content) || content.length < 30;
|
|
1379
|
+
const excessiveRepetition = /(.)\1{9,}/.test(content);
|
|
1380
|
+
let coherence = 1;
|
|
1381
|
+
if (allCaps) {
|
|
1382
|
+
coherence -= 0.5;
|
|
1383
|
+
}
|
|
1384
|
+
if (!hasWhitespaceOrPunctuation) {
|
|
1385
|
+
coherence -= 0.3;
|
|
1386
|
+
}
|
|
1387
|
+
if (excessiveRepetition) {
|
|
1388
|
+
coherence -= 0.5;
|
|
1389
|
+
}
|
|
1390
|
+
coherence = Math.max(0, coherence);
|
|
1391
|
+
if (coherence < 0.3) failed.push("coherence (garbled or malformed content)");
|
|
1392
|
+
return {
|
|
1393
|
+
pass: failed.length === 0,
|
|
1394
|
+
scores: { specificity, novelty, relevance, coherence },
|
|
1395
|
+
failedCriteria: failed
|
|
1396
|
+
};
|
|
663
1397
|
}
|
|
664
1398
|
|
|
665
1399
|
// src/sleep/sync.ts
|
|
@@ -706,13 +1440,16 @@ function syncOne(db, input) {
|
|
|
706
1440
|
}
|
|
707
1441
|
|
|
708
1442
|
// src/bin/agent-memory.ts
|
|
709
|
-
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
1443
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
710
1444
|
import { resolve, basename } from "path";
|
|
711
1445
|
var args = process.argv.slice(2);
|
|
712
1446
|
var command = args[0];
|
|
713
1447
|
function getDbPath() {
|
|
714
1448
|
return process.env.AGENT_MEMORY_DB ?? "./agent-memory.db";
|
|
715
1449
|
}
|
|
1450
|
+
function getAgentId() {
|
|
1451
|
+
return process.env.AGENT_MEMORY_AGENT_ID ?? "default";
|
|
1452
|
+
}
|
|
716
1453
|
function printHelp() {
|
|
717
1454
|
console.log(`
|
|
718
1455
|
\u{1F9E0} AgentMemory v2 \u2014 Sleep-cycle memory for AI agents
|
|
@@ -721,12 +1458,16 @@ Usage: agent-memory <command> [options]
|
|
|
721
1458
|
|
|
722
1459
|
Commands:
|
|
723
1460
|
init Create database
|
|
1461
|
+
db:migrate Run schema migrations (no-op if up-to-date)
|
|
1462
|
+
embed [--limit N] Embed missing memories (requires provider)
|
|
724
1463
|
remember <content> [--uri X] [--type T] Store a memory
|
|
725
1464
|
recall <query> [--limit N] Search memories
|
|
726
1465
|
boot Load identity memories
|
|
727
1466
|
status Show statistics
|
|
728
1467
|
reflect [decay|tidy|govern|all] Run sleep cycle
|
|
1468
|
+
reindex Rebuild FTS index with jieba tokenizer
|
|
729
1469
|
migrate <dir> Import from Markdown files
|
|
1470
|
+
export <dir> Export memories to Markdown files
|
|
730
1471
|
help Show this help
|
|
731
1472
|
|
|
732
1473
|
Environment:
|
|
@@ -739,178 +1480,254 @@ function getFlag(flag) {
|
|
|
739
1480
|
if (idx >= 0 && idx + 1 < args.length) return args[idx + 1];
|
|
740
1481
|
return void 0;
|
|
741
1482
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
case "remember": {
|
|
751
|
-
const content = args.slice(1).filter((a) => !a.startsWith("--")).join(" ");
|
|
752
|
-
if (!content) {
|
|
753
|
-
console.error("Usage: agent-memory remember <content>");
|
|
754
|
-
process.exit(1);
|
|
755
|
-
}
|
|
756
|
-
const db = openDatabase({ path: getDbPath() });
|
|
757
|
-
const uri = getFlag("--uri");
|
|
758
|
-
const type = getFlag("--type") ?? "knowledge";
|
|
759
|
-
const result = syncOne(db, { content, type, uri });
|
|
760
|
-
console.log(`${result.action}: ${result.reason}${result.memoryId ? ` (${result.memoryId.slice(0, 8)})` : ""}`);
|
|
761
|
-
db.close();
|
|
762
|
-
break;
|
|
763
|
-
}
|
|
764
|
-
case "recall": {
|
|
765
|
-
const query = args.slice(1).filter((a) => !a.startsWith("--")).join(" ");
|
|
766
|
-
if (!query) {
|
|
767
|
-
console.error("Usage: agent-memory recall <query>");
|
|
768
|
-
process.exit(1);
|
|
1483
|
+
async function main() {
|
|
1484
|
+
try {
|
|
1485
|
+
switch (command) {
|
|
1486
|
+
case "init": {
|
|
1487
|
+
const dbPath = getDbPath();
|
|
1488
|
+
openDatabase({ path: dbPath });
|
|
1489
|
+
console.log(`\u2705 Database created at ${dbPath}`);
|
|
1490
|
+
break;
|
|
769
1491
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
`);
|
|
778
|
-
for (const r of results) {
|
|
779
|
-
const p = ["\u{1F534}", "\u{1F7E0}", "\u{1F7E1}", "\u26AA"][r.memory.priority];
|
|
780
|
-
const v = (r.memory.vitality * 100).toFixed(0);
|
|
781
|
-
console.log(`${p} P${r.memory.priority} [${v}%] ${r.memory.content.slice(0, 80)}`);
|
|
1492
|
+
case "db:migrate": {
|
|
1493
|
+
const dbPath = getDbPath();
|
|
1494
|
+
const db = openDatabase({ path: dbPath });
|
|
1495
|
+
const version = db.prepare("SELECT value FROM schema_meta WHERE key = 'version'").get()?.value;
|
|
1496
|
+
console.log(`\u2705 Schema version: ${version ?? "unknown"} (${dbPath})`);
|
|
1497
|
+
db.close();
|
|
1498
|
+
break;
|
|
782
1499
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
1500
|
+
case "embed": {
|
|
1501
|
+
const provider = getEmbeddingProviderFromEnv();
|
|
1502
|
+
if (!provider) {
|
|
1503
|
+
console.error("Embedding provider not configured. Set AGENT_MEMORY_EMBEDDINGS_PROVIDER=openai|qwen and the corresponding API key.");
|
|
1504
|
+
process.exit(1);
|
|
1505
|
+
}
|
|
1506
|
+
const db = openDatabase({ path: getDbPath() });
|
|
1507
|
+
const agentId = getAgentId();
|
|
1508
|
+
const limit = parseInt(getFlag("--limit") ?? "200");
|
|
1509
|
+
const r = await embedMissingForAgent(db, provider, { agent_id: agentId, limit });
|
|
1510
|
+
console.log(`\u2705 Embedded: ${r.embedded}/${r.scanned} (agent_id=${agentId}, model=${provider.model})`);
|
|
1511
|
+
db.close();
|
|
1512
|
+
break;
|
|
793
1513
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1514
|
+
case "remember": {
|
|
1515
|
+
const content = args.slice(1).filter((a) => !a.startsWith("--")).join(" ");
|
|
1516
|
+
if (!content) {
|
|
1517
|
+
console.error("Usage: agent-memory remember <content>");
|
|
1518
|
+
process.exit(1);
|
|
1519
|
+
}
|
|
1520
|
+
const db = openDatabase({ path: getDbPath() });
|
|
1521
|
+
const uri = getFlag("--uri");
|
|
1522
|
+
const type = getFlag("--type") ?? "knowledge";
|
|
1523
|
+
const agentId = getAgentId();
|
|
1524
|
+
const provider = getEmbeddingProviderFromEnv();
|
|
1525
|
+
const result = syncOne(db, { content, type, uri, agent_id: agentId });
|
|
1526
|
+
if (provider && result.memoryId && (result.action === "added" || result.action === "updated" || result.action === "merged")) {
|
|
1527
|
+
try {
|
|
1528
|
+
await embedMemory(db, result.memoryId, provider, { agent_id: agentId });
|
|
1529
|
+
} catch {
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
console.log(`${result.action}: ${result.reason}${result.memoryId ? ` (${result.memoryId.slice(0, 8)})` : ""}`);
|
|
1533
|
+
db.close();
|
|
1534
|
+
break;
|
|
797
1535
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
console.log(` Low vitality (<10%): ${lowVit}`);
|
|
814
|
-
db.close();
|
|
815
|
-
break;
|
|
816
|
-
}
|
|
817
|
-
case "reflect": {
|
|
818
|
-
const phase = args[1] ?? "all";
|
|
819
|
-
const db = openDatabase({ path: getDbPath() });
|
|
820
|
-
console.log(`\u{1F319} Running ${phase} phase...
|
|
1536
|
+
case "recall": {
|
|
1537
|
+
const query = args.slice(1).filter((a) => !a.startsWith("--")).join(" ");
|
|
1538
|
+
if (!query) {
|
|
1539
|
+
console.error("Usage: agent-memory recall <query>");
|
|
1540
|
+
process.exit(1);
|
|
1541
|
+
}
|
|
1542
|
+
const db = openDatabase({ path: getDbPath() });
|
|
1543
|
+
const limit = parseInt(getFlag("--limit") ?? "10");
|
|
1544
|
+
const { intent } = classifyIntent(query);
|
|
1545
|
+
const strategy = getStrategy(intent);
|
|
1546
|
+
const agentId = getAgentId();
|
|
1547
|
+
const provider = getEmbeddingProviderFromEnv();
|
|
1548
|
+
const raw = await searchHybrid(db, query, { agent_id: agentId, embeddingProvider: provider, limit: limit * 2 });
|
|
1549
|
+
const results = rerank(raw, { ...strategy, limit });
|
|
1550
|
+
console.log(`\u{1F50D} Intent: ${intent} | Results: ${results.length}
|
|
821
1551
|
`);
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
}
|
|
830
|
-
if (phase === "govern" || phase === "all") {
|
|
831
|
-
const r = runGovern(db);
|
|
832
|
-
console.log(` Govern: ${r.orphanPaths} paths, ${r.orphanLinks} links, ${r.emptyMemories} empty cleaned`);
|
|
1552
|
+
for (const r of results) {
|
|
1553
|
+
const p = ["\u{1F534}", "\u{1F7E0}", "\u{1F7E1}", "\u26AA"][r.memory.priority];
|
|
1554
|
+
const v = (r.memory.vitality * 100).toFixed(0);
|
|
1555
|
+
console.log(`${p} P${r.memory.priority} [${v}%] ${r.memory.content.slice(0, 80)}`);
|
|
1556
|
+
}
|
|
1557
|
+
db.close();
|
|
1558
|
+
break;
|
|
833
1559
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1560
|
+
case "boot": {
|
|
1561
|
+
const db = openDatabase({ path: getDbPath() });
|
|
1562
|
+
const result = boot(db, { agent_id: getAgentId() });
|
|
1563
|
+
console.log(`\u{1F9E0} Boot: ${result.identityMemories.length} identity memories loaded
|
|
1564
|
+
`);
|
|
1565
|
+
for (const m of result.identityMemories) {
|
|
1566
|
+
console.log(` \u{1F534} ${m.content.slice(0, 100)}`);
|
|
1567
|
+
}
|
|
1568
|
+
if (result.bootPaths.length) {
|
|
1569
|
+
console.log(`
|
|
1570
|
+
\u{1F4CD} Boot paths: ${result.bootPaths.join(", ")}`);
|
|
1571
|
+
}
|
|
1572
|
+
db.close();
|
|
1573
|
+
break;
|
|
842
1574
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1575
|
+
case "status": {
|
|
1576
|
+
const db = openDatabase({ path: getDbPath() });
|
|
1577
|
+
const agentId = getAgentId();
|
|
1578
|
+
const stats = countMemories(db, agentId);
|
|
1579
|
+
const lowVit = db.prepare("SELECT COUNT(*) as c FROM memories WHERE vitality < 0.1 AND agent_id = ?").get(agentId).c;
|
|
1580
|
+
const paths = db.prepare("SELECT COUNT(*) as c FROM paths WHERE agent_id = ?").get(agentId).c;
|
|
1581
|
+
const links = db.prepare("SELECT COUNT(*) as c FROM links WHERE agent_id = ?").get(agentId).c;
|
|
1582
|
+
const snaps = db.prepare(
|
|
1583
|
+
`SELECT COUNT(*) as c FROM snapshots s
|
|
1584
|
+
JOIN memories m ON m.id = s.memory_id
|
|
1585
|
+
WHERE m.agent_id = ?`
|
|
1586
|
+
).get(agentId).c;
|
|
1587
|
+
console.log("\u{1F9E0} AgentMemory Status\n");
|
|
1588
|
+
console.log(` Total memories: ${stats.total}`);
|
|
1589
|
+
console.log(` By type: ${Object.entries(stats.by_type).map(([k, v]) => `${k}=${v}`).join(", ")}`);
|
|
1590
|
+
console.log(` By priority: ${Object.entries(stats.by_priority).map(([k, v]) => `${k}=${v}`).join(", ")}`);
|
|
1591
|
+
console.log(` Paths: ${paths} | Links: ${links} | Snapshots: ${snaps}`);
|
|
1592
|
+
console.log(` Low vitality (<10%): ${lowVit}`);
|
|
1593
|
+
db.close();
|
|
1594
|
+
break;
|
|
847
1595
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
syncOne(db, { content: `## ${title}
|
|
862
|
-
${body}`, type, uri, source: "migrate:MEMORY.md" });
|
|
863
|
-
imported++;
|
|
1596
|
+
case "reflect": {
|
|
1597
|
+
const phase = args[1] ?? "all";
|
|
1598
|
+
const db = openDatabase({ path: getDbPath() });
|
|
1599
|
+
const agentId = getAgentId();
|
|
1600
|
+
console.log(`\u{1F319} Running ${phase} phase...
|
|
1601
|
+
`);
|
|
1602
|
+
if (phase === "decay" || phase === "all") {
|
|
1603
|
+
const r = runDecay(db, { agent_id: agentId });
|
|
1604
|
+
console.log(` Decay: ${r.updated} updated, ${r.decayed} decayed, ${r.belowThreshold} below threshold`);
|
|
1605
|
+
}
|
|
1606
|
+
if (phase === "tidy" || phase === "all") {
|
|
1607
|
+
const r = runTidy(db, { agent_id: agentId });
|
|
1608
|
+
console.log(` Tidy: ${r.archived} archived, ${r.orphansCleaned} orphans, ${r.snapshotsPruned} snapshots pruned`);
|
|
864
1609
|
}
|
|
865
|
-
|
|
1610
|
+
if (phase === "govern" || phase === "all") {
|
|
1611
|
+
const r = runGovern(db, { agent_id: agentId });
|
|
1612
|
+
console.log(` Govern: ${r.orphanPaths} paths, ${r.orphanLinks} links, ${r.emptyMemories} empty cleaned`);
|
|
1613
|
+
}
|
|
1614
|
+
db.close();
|
|
1615
|
+
break;
|
|
866
1616
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1617
|
+
case "reindex": {
|
|
1618
|
+
const db = openDatabase({ path: getDbPath() });
|
|
1619
|
+
const memories = db.prepare("SELECT id, content FROM memories").all();
|
|
1620
|
+
db.exec("DELETE FROM memories_fts");
|
|
1621
|
+
const insert = db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)");
|
|
1622
|
+
let count = 0;
|
|
1623
|
+
const txn = db.transaction(() => {
|
|
1624
|
+
for (const mem of memories) {
|
|
1625
|
+
insert.run(mem.id, tokenizeForIndex(mem.content));
|
|
1626
|
+
count++;
|
|
1627
|
+
}
|
|
876
1628
|
});
|
|
877
|
-
|
|
1629
|
+
txn();
|
|
1630
|
+
console.log(`\u{1F504} Reindexed ${count} memories with jieba tokenizer`);
|
|
1631
|
+
db.close();
|
|
1632
|
+
break;
|
|
1633
|
+
}
|
|
1634
|
+
case "export": {
|
|
1635
|
+
const dir = args[1];
|
|
1636
|
+
if (!dir) {
|
|
1637
|
+
console.error("Usage: agent-memory export <directory>");
|
|
1638
|
+
process.exit(1);
|
|
1639
|
+
}
|
|
1640
|
+
const dirPath = resolve(dir);
|
|
1641
|
+
const db = openDatabase({ path: getDbPath() });
|
|
1642
|
+
const agentId = getAgentId();
|
|
1643
|
+
const result = exportMemories(db, dirPath, { agent_id: agentId });
|
|
1644
|
+
console.log(`\u2705 Export complete: ${result.exported} items to ${dirPath} (${result.files.length} files)`);
|
|
1645
|
+
db.close();
|
|
1646
|
+
break;
|
|
878
1647
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1648
|
+
case "migrate": {
|
|
1649
|
+
const dir = args[1];
|
|
1650
|
+
if (!dir) {
|
|
1651
|
+
console.error("Usage: agent-memory migrate <directory>");
|
|
1652
|
+
process.exit(1);
|
|
1653
|
+
}
|
|
1654
|
+
const dirPath = resolve(dir);
|
|
1655
|
+
if (!existsSync2(dirPath)) {
|
|
1656
|
+
console.error(`Directory not found: ${dirPath}`);
|
|
1657
|
+
process.exit(1);
|
|
1658
|
+
}
|
|
1659
|
+
const db = openDatabase({ path: getDbPath() });
|
|
1660
|
+
const agentId = getAgentId();
|
|
1661
|
+
let imported = 0;
|
|
1662
|
+
const memoryFile = ["MEMORY.md", "MEMORY.qmd"].map((f) => resolve(dirPath, f)).find((p) => existsSync2(p));
|
|
1663
|
+
if (memoryFile) {
|
|
1664
|
+
const content = readFileSync2(memoryFile, "utf-8");
|
|
1665
|
+
const sections = content.split(/^## /m).filter((s) => s.trim());
|
|
1666
|
+
for (const section of sections) {
|
|
1667
|
+
const lines = section.split("\n");
|
|
1668
|
+
const title = lines[0]?.trim();
|
|
1669
|
+
const body = lines.slice(1).join("\n").trim();
|
|
1670
|
+
if (!body) continue;
|
|
1671
|
+
const type = title?.toLowerCase().includes("\u5173\u4E8E") || title?.toLowerCase().includes("about") ? "identity" : "knowledge";
|
|
1672
|
+
const uri = `knowledge://memory-md/${title?.replace(/[^a-z0-9\u4e00-\u9fff]/gi, "-").toLowerCase()}`;
|
|
1673
|
+
syncOne(db, { content: `## ${title}
|
|
1674
|
+
${body}`, type, uri, source: `migrate:${basename(memoryFile)}`, agent_id: agentId });
|
|
1675
|
+
imported++;
|
|
1676
|
+
}
|
|
1677
|
+
console.log(`\u{1F4C4} ${basename(memoryFile)}: ${sections.length} sections imported`);
|
|
1678
|
+
}
|
|
1679
|
+
const mdFiles = readdirSync(dirPath).filter((f) => /^\d{4}-\d{2}-\d{2}\.(md|qmd)$/.test(f)).sort();
|
|
1680
|
+
for (const file of mdFiles) {
|
|
1681
|
+
const content = readFileSync2(resolve(dirPath, file), "utf-8");
|
|
1682
|
+
const date = file.replace(/\.(md|qmd)$/i, "");
|
|
886
1683
|
syncOne(db, {
|
|
887
1684
|
content,
|
|
888
|
-
type: "
|
|
889
|
-
uri: `
|
|
890
|
-
source: `migrate
|
|
1685
|
+
type: "event",
|
|
1686
|
+
uri: `event://journal/${date}`,
|
|
1687
|
+
source: `migrate:${file}`,
|
|
1688
|
+
agent_id: agentId
|
|
891
1689
|
});
|
|
892
1690
|
imported++;
|
|
893
1691
|
}
|
|
894
|
-
if (
|
|
895
|
-
|
|
896
|
-
|
|
1692
|
+
if (mdFiles.length) console.log(`\u{1F4DD} Journals: ${mdFiles.length} files imported`);
|
|
1693
|
+
const weeklyDir = resolve(dirPath, "weekly");
|
|
1694
|
+
if (existsSync2(weeklyDir)) {
|
|
1695
|
+
const weeklyFiles = readdirSync(weeklyDir).filter((f) => f.endsWith(".md") || f.endsWith(".qmd"));
|
|
1696
|
+
for (const file of weeklyFiles) {
|
|
1697
|
+
const content = readFileSync2(resolve(weeklyDir, file), "utf-8");
|
|
1698
|
+
const week = file.replace(/\.(md|qmd)$/i, "");
|
|
1699
|
+
syncOne(db, {
|
|
1700
|
+
content,
|
|
1701
|
+
type: "knowledge",
|
|
1702
|
+
uri: `knowledge://weekly/${week}`,
|
|
1703
|
+
source: `migrate:weekly/${file}`,
|
|
1704
|
+
agent_id: agentId
|
|
1705
|
+
});
|
|
1706
|
+
imported++;
|
|
1707
|
+
}
|
|
1708
|
+
if (weeklyFiles.length) console.log(`\u{1F4E6} Weekly: ${weeklyFiles.length} files imported`);
|
|
1709
|
+
}
|
|
1710
|
+
console.log(`
|
|
897
1711
|
\u2705 Migration complete: ${imported} items imported`);
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
} catch (err) {
|
|
913
|
-
|
|
914
|
-
|
|
1712
|
+
db.close();
|
|
1713
|
+
break;
|
|
1714
|
+
}
|
|
1715
|
+
case "help":
|
|
1716
|
+
case "--help":
|
|
1717
|
+
case "-h":
|
|
1718
|
+
case void 0:
|
|
1719
|
+
printHelp();
|
|
1720
|
+
break;
|
|
1721
|
+
default:
|
|
1722
|
+
console.error(`Unknown command: ${command}`);
|
|
1723
|
+
printHelp();
|
|
1724
|
+
process.exit(1);
|
|
1725
|
+
}
|
|
1726
|
+
} catch (err) {
|
|
1727
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1728
|
+
console.error("Error:", message);
|
|
1729
|
+
process.exit(1);
|
|
1730
|
+
}
|
|
915
1731
|
}
|
|
1732
|
+
main();
|
|
916
1733
|
//# sourceMappingURL=agent-memory.js.map
|