@hanhnd/agent-kit 1.0.34 → 1.0.36
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/dist/cli/commands/cli-digest-pending.test.js +19 -10
- package/dist/cli/commands/memory.js +44 -4
- package/dist/cli.js +348 -179
- package/dist/core/config/index.d.ts +3 -0
- package/dist/core/config/index.js +20 -0
- package/dist/entrypoints/server.js +1 -3
- package/dist/mcp/memory.d.ts +2 -1
- package/dist/mcp/memory.js +45 -18
- package/dist/server.js +294 -200
- package/dist/services/digest/constants.d.ts +3 -3
- package/dist/services/digest/constants.js +3 -3
- package/dist/services/digest/digest-background.test.js +19 -8
- package/dist/services/digest/digest-init.test.js +13 -2
- package/dist/services/digest/digest-pending.test.js +13 -2
- package/dist/services/digest/digest-processor.test.js +52 -0
- package/dist/services/digest/files.js +3 -3
- package/dist/services/digest/model-registry.js +2 -2
- package/dist/services/digest/processor.js +43 -13
- package/dist/services/digest/providers/llama-local.js +17 -28
- package/dist/services/digest/types.d.ts +3 -1
- package/dist/services/digest/types.js +1 -1
- package/dist/services/memory/chunker.d.ts +5 -2
- package/dist/services/memory/chunker.js +3 -1
- package/dist/services/memory/chunker.test.js +21 -11
- package/dist/services/memory/constants.js +1 -1
- package/dist/services/memory/embedder.js +1 -1
- package/dist/services/memory/index.d.ts +1 -1
- package/dist/services/memory/indexer.d.ts +2 -1
- package/dist/services/memory/indexer.js +24 -8
- package/dist/services/memory/indexer.test.js +38 -2
- package/dist/services/memory/memory.test.js +172 -15
- package/dist/services/memory/store.d.ts +9 -3
- package/dist/services/memory/store.js +132 -98
- package/dist/services/memory/store.test.js +239 -14
- package/dist/services/memory/types.d.ts +9 -1
- package/dist/services/memory/types.js +2 -1
- package/dist/utils/paths.d.ts +2 -0
- package/dist/utils/paths.js +2 -1
- package/dist/utils/utils.d.ts +1 -0
- package/dist/utils/utils.js +3 -3
- package/package.json +3 -5
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import Database from '
|
|
1
|
+
import Database from 'libsql';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { eng, removeStopwords } from 'stopword';
|
|
5
|
+
export const SCHEMA_VERSION = 3;
|
|
6
|
+
const LibsqlDatabase = Database;
|
|
5
7
|
export class StoreError extends Error {
|
|
6
8
|
constructor(message, cause) {
|
|
7
9
|
super(message);
|
|
@@ -13,7 +15,6 @@ export class StoreError extends Error {
|
|
|
13
15
|
export class MemoryStore {
|
|
14
16
|
config;
|
|
15
17
|
db;
|
|
16
|
-
_vecAvailable = false;
|
|
17
18
|
constructor(dbPath, config) {
|
|
18
19
|
this.config = config;
|
|
19
20
|
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
@@ -21,10 +22,9 @@ export class MemoryStore {
|
|
|
21
22
|
}
|
|
22
23
|
openDatabase(dbPath) {
|
|
23
24
|
try {
|
|
24
|
-
const db = new
|
|
25
|
+
const db = new LibsqlDatabase(dbPath);
|
|
25
26
|
db.pragma('journal_mode = WAL');
|
|
26
27
|
db.pragma('busy_timeout = 5000');
|
|
27
|
-
this.loadVecExtension(db);
|
|
28
28
|
this.createSchema(db);
|
|
29
29
|
return db;
|
|
30
30
|
}
|
|
@@ -36,97 +36,107 @@ export class MemoryStore {
|
|
|
36
36
|
catch {
|
|
37
37
|
// ignore
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
try {
|
|
40
|
+
const db = new LibsqlDatabase(dbPath);
|
|
41
|
+
db.pragma('journal_mode = WAL');
|
|
42
|
+
db.pragma('busy_timeout = 5000');
|
|
43
|
+
this.createSchema(db);
|
|
44
|
+
return db;
|
|
45
|
+
}
|
|
46
|
+
catch (retryErr) {
|
|
47
|
+
throw new StoreError('database open failed', retryErr);
|
|
48
|
+
}
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
|
-
|
|
51
|
+
createSchema(db) {
|
|
48
52
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
const current = this.readUserVersion(db);
|
|
54
|
+
if (current < SCHEMA_VERSION) {
|
|
55
|
+
db.exec(`
|
|
56
|
+
DROP TRIGGER IF EXISTS memory_chunks_ai;
|
|
57
|
+
DROP TRIGGER IF EXISTS memory_chunks_ad;
|
|
58
|
+
DROP TABLE IF EXISTS memory_fts;
|
|
59
|
+
DROP TABLE IF EXISTS memory_vec;
|
|
60
|
+
DROP INDEX IF EXISTS idx_memory_chunks_embedding;
|
|
61
|
+
DROP TABLE IF EXISTS memory_chunks;
|
|
62
|
+
`);
|
|
63
|
+
db.pragma(`user_version = ${SCHEMA_VERSION}`);
|
|
64
|
+
}
|
|
65
|
+
db.exec(`
|
|
66
|
+
CREATE TABLE IF NOT EXISTS memory_chunks (
|
|
67
|
+
rowid INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
68
|
+
id TEXT NOT NULL UNIQUE,
|
|
69
|
+
source TEXT NOT NULL,
|
|
70
|
+
source_type TEXT NOT NULL DEFAULT 'wiki',
|
|
71
|
+
heading TEXT NOT NULL DEFAULT '',
|
|
72
|
+
heading_level INTEGER NOT NULL DEFAULT 0,
|
|
73
|
+
content TEXT NOT NULL,
|
|
74
|
+
line_start INTEGER NOT NULL,
|
|
75
|
+
line_end INTEGER NOT NULL,
|
|
76
|
+
indexed_at INTEGER NOT NULL,
|
|
77
|
+
file_mtime_at INTEGER NOT NULL DEFAULT 0,
|
|
78
|
+
embedding F32_BLOB(${this.config.vectorDimension}) NOT NULL
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_memory_chunks_type_mtime
|
|
82
|
+
ON memory_chunks (source_type, file_mtime_at DESC);
|
|
83
|
+
|
|
84
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
|
|
85
|
+
source,
|
|
86
|
+
heading,
|
|
87
|
+
content,
|
|
88
|
+
content='memory_chunks',
|
|
89
|
+
content_rowid='rowid'
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
CREATE TRIGGER IF NOT EXISTS memory_chunks_ai
|
|
93
|
+
AFTER INSERT ON memory_chunks BEGIN
|
|
94
|
+
INSERT INTO memory_fts(rowid, source, heading, content)
|
|
95
|
+
VALUES (new.rowid, new.source, new.heading, new.content);
|
|
96
|
+
END;
|
|
97
|
+
|
|
98
|
+
CREATE TRIGGER IF NOT EXISTS memory_chunks_ad
|
|
99
|
+
AFTER DELETE ON memory_chunks BEGIN
|
|
100
|
+
INSERT INTO memory_fts(memory_fts, rowid, source, heading, content)
|
|
101
|
+
VALUES ('delete', old.rowid, old.source, old.heading, old.content);
|
|
102
|
+
END;
|
|
103
|
+
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_memory_chunks_embedding
|
|
105
|
+
ON memory_chunks (libsql_vector_idx(embedding));
|
|
106
|
+
`);
|
|
53
107
|
}
|
|
54
108
|
catch (err) {
|
|
55
|
-
|
|
56
|
-
this._vecAvailable = false;
|
|
109
|
+
throw new StoreError('schema migration failed', err);
|
|
57
110
|
}
|
|
58
111
|
}
|
|
59
|
-
|
|
60
|
-
db.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
indexed_at INTEGER NOT NULL
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
|
|
74
|
-
source,
|
|
75
|
-
heading,
|
|
76
|
-
content,
|
|
77
|
-
content='memory_chunks',
|
|
78
|
-
content_rowid='rowid'
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
CREATE TRIGGER IF NOT EXISTS memory_chunks_ai
|
|
82
|
-
AFTER INSERT ON memory_chunks BEGIN
|
|
83
|
-
INSERT INTO memory_fts(rowid, source, heading, content)
|
|
84
|
-
VALUES (new.rowid, new.source, new.heading, new.content);
|
|
85
|
-
END;
|
|
86
|
-
|
|
87
|
-
CREATE TRIGGER IF NOT EXISTS memory_chunks_ad
|
|
88
|
-
AFTER DELETE ON memory_chunks BEGIN
|
|
89
|
-
INSERT INTO memory_fts(memory_fts, rowid, source, heading, content)
|
|
90
|
-
VALUES ('delete', old.rowid, old.source, old.heading, old.content);
|
|
91
|
-
END;
|
|
92
|
-
`);
|
|
93
|
-
if (this._vecAvailable) {
|
|
94
|
-
try {
|
|
95
|
-
db.exec(`
|
|
96
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS memory_vec USING vec0(
|
|
97
|
-
embedding FLOAT[${this.config.vectorDimension}]
|
|
98
|
-
);
|
|
99
|
-
`);
|
|
100
|
-
}
|
|
101
|
-
catch (err) {
|
|
102
|
-
console.warn('[memory-store] Failed to create memory_vec, disabling vector search:', err);
|
|
103
|
-
this._vecAvailable = false;
|
|
104
|
-
}
|
|
112
|
+
readUserVersion(db) {
|
|
113
|
+
const value = db.pragma('user_version', { simple: true });
|
|
114
|
+
if (typeof value === 'number')
|
|
115
|
+
return value;
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
const [row] = value;
|
|
118
|
+
return typeof row?.user_version === 'number' ? row.user_version : 0;
|
|
119
|
+
}
|
|
120
|
+
if (value && typeof value === 'object' && 'user_version' in value) {
|
|
121
|
+
const row = value;
|
|
122
|
+
return typeof row.user_version === 'number' ? row.user_version : 0;
|
|
105
123
|
}
|
|
124
|
+
return 0;
|
|
106
125
|
}
|
|
107
126
|
upsert(chunks, embeddings) {
|
|
108
127
|
const insertChunk = this.db.prepare(`
|
|
109
128
|
INSERT OR REPLACE INTO memory_chunks
|
|
110
|
-
(id, source, heading, heading_level, content, line_start, line_end, indexed_at)
|
|
111
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
129
|
+
(id, source, source_type, heading, heading_level, content, line_start, line_end, indexed_at, file_mtime_at, embedding)
|
|
130
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?))
|
|
112
131
|
`);
|
|
113
|
-
let insertVec = null;
|
|
114
|
-
if (this._vecAvailable) {
|
|
115
|
-
insertVec = this.db.prepare(`INSERT OR REPLACE INTO memory_vec (rowid, embedding) VALUES (?, ?)`);
|
|
116
|
-
}
|
|
117
132
|
const upsertAll = this.db.transaction(() => {
|
|
118
133
|
for (let i = 0; i < chunks.length; i++) {
|
|
119
134
|
const chunk = chunks[i];
|
|
120
135
|
try {
|
|
136
|
+
const vectorBinding = this.toVectorBinding(embeddings[i]);
|
|
121
137
|
// Delete existing row first to get correct rowid
|
|
122
138
|
this.db.prepare(`DELETE FROM memory_chunks WHERE id = ?`).run(chunk.id);
|
|
123
|
-
|
|
124
|
-
if (insertVec && embeddings[i] && info.lastInsertRowid) {
|
|
125
|
-
const rowid = Number(info.lastInsertRowid);
|
|
126
|
-
const emb = embeddings[i];
|
|
127
|
-
const buf = Buffer.from(emb.buffer, emb.byteOffset, emb.byteLength);
|
|
128
|
-
insertVec.run(rowid, buf);
|
|
129
|
-
}
|
|
139
|
+
insertChunk.run(chunk.id, chunk.source, chunk.sourceType, chunk.heading, chunk.headingLevel, chunk.content, chunk.lineStart, chunk.lineEnd, Date.now(), chunk.fileMtimeAt, vectorBinding);
|
|
130
140
|
}
|
|
131
141
|
catch (err) {
|
|
132
142
|
console.warn(`[memory-store] Failed to upsert chunk ${chunk.id}:`, err);
|
|
@@ -140,6 +150,25 @@ export class MemoryStore {
|
|
|
140
150
|
throw new StoreError('Full batch upsert failed', err);
|
|
141
151
|
}
|
|
142
152
|
}
|
|
153
|
+
toVectorBinding(embedding) {
|
|
154
|
+
if (!embedding)
|
|
155
|
+
throw new Error('missing embedding');
|
|
156
|
+
if (!(embedding instanceof Float32Array))
|
|
157
|
+
throw new Error('missing embedding');
|
|
158
|
+
if (embedding.length !== this.config.vectorDimension)
|
|
159
|
+
throw new Error('embedding dimension mismatch');
|
|
160
|
+
const values = Array.from(embedding, (value) => {
|
|
161
|
+
if (!Number.isFinite(value))
|
|
162
|
+
throw new Error('embedding contains non-finite value');
|
|
163
|
+
return value;
|
|
164
|
+
});
|
|
165
|
+
return JSON.stringify(values);
|
|
166
|
+
}
|
|
167
|
+
normalizeVectorDistance(distance) {
|
|
168
|
+
if (distance <= 0)
|
|
169
|
+
return 1;
|
|
170
|
+
return Math.max(0, Math.min(1, 1 - distance));
|
|
171
|
+
}
|
|
143
172
|
hashesBySource(source) {
|
|
144
173
|
const rows = this.db.prepare(`SELECT id FROM memory_chunks WHERE source = ?`).all(source);
|
|
145
174
|
return new Set(rows.map((r) => r.id));
|
|
@@ -148,13 +177,23 @@ export class MemoryStore {
|
|
|
148
177
|
const rows = this.db.prepare(`SELECT DISTINCT source FROM memory_chunks`).all();
|
|
149
178
|
return rows.map((r) => r.source);
|
|
150
179
|
}
|
|
180
|
+
getRecentSources(options) {
|
|
181
|
+
const limit = Math.min(Math.max(1, options.limit || 5), 50);
|
|
182
|
+
const whereClause = options.sourceType ? ' WHERE source_type = ?' : '';
|
|
183
|
+
const params = options.sourceType ? [options.sourceType, limit] : [limit];
|
|
184
|
+
try {
|
|
185
|
+
const rows = this.db
|
|
186
|
+
.prepare(`SELECT source, MAX(file_mtime_at) AS t FROM memory_chunks${whereClause} GROUP BY source ORDER BY t DESC LIMIT ?`)
|
|
187
|
+
.all(...params);
|
|
188
|
+
return rows.map((row) => ({ source: row.source, fileMtimeAt: row.t }));
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
console.warn('[memory-store] Recent source lookup failed:', err);
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
151
195
|
deleteBySource(source) {
|
|
152
|
-
const rows = this.db.prepare(`SELECT rowid FROM memory_chunks WHERE source = ?`).all(source);
|
|
153
196
|
const deleteAll = this.db.transaction(() => {
|
|
154
|
-
if (this._vecAvailable && rows.length > 0) {
|
|
155
|
-
const placeholders = rows.map(() => '?').join(', ');
|
|
156
|
-
this.db.prepare(`DELETE FROM memory_vec WHERE rowid IN (${placeholders})`).run(...rows.map((r) => r.rowid));
|
|
157
|
-
}
|
|
158
197
|
this.db.prepare(`DELETE FROM memory_chunks WHERE source = ?`).run(source);
|
|
159
198
|
});
|
|
160
199
|
deleteAll();
|
|
@@ -163,31 +202,24 @@ export class MemoryStore {
|
|
|
163
202
|
if (ids.length === 0)
|
|
164
203
|
return;
|
|
165
204
|
const placeholders = ids.map(() => '?').join(', ');
|
|
166
|
-
const rows = this.db.prepare(`SELECT rowid FROM memory_chunks WHERE id IN (${placeholders})`).all(...ids);
|
|
167
205
|
const deleteAll = this.db.transaction(() => {
|
|
168
|
-
if (this._vecAvailable && rows.length > 0) {
|
|
169
|
-
const rPlaceholders = rows.map(() => '?').join(', ');
|
|
170
|
-
this.db.prepare(`DELETE FROM memory_vec WHERE rowid IN (${rPlaceholders})`).run(...rows.map((r) => r.rowid));
|
|
171
|
-
}
|
|
172
206
|
this.db.prepare(`DELETE FROM memory_chunks WHERE id IN (${placeholders})`).run(...ids);
|
|
173
207
|
});
|
|
174
208
|
deleteAll();
|
|
175
209
|
}
|
|
176
210
|
searchDense(embedding, limit) {
|
|
177
|
-
if (!this._vecAvailable)
|
|
178
|
-
return [];
|
|
179
211
|
try {
|
|
180
|
-
const
|
|
212
|
+
const queryVector = this.toVectorBinding(embedding);
|
|
181
213
|
const rows = this.db
|
|
182
|
-
.prepare(`SELECT mc.id,
|
|
183
|
-
FROM
|
|
184
|
-
JOIN memory_chunks mc ON mc.rowid =
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
.all(
|
|
214
|
+
.prepare(`SELECT mc.id, vector_distance_cos(mc.embedding, vector32(?)) AS distance
|
|
215
|
+
FROM vector_top_k('idx_memory_chunks_embedding', vector32(?), ?) AS vector_matches
|
|
216
|
+
JOIN memory_chunks mc ON mc.rowid = vector_matches.id
|
|
217
|
+
ORDER BY distance
|
|
218
|
+
LIMIT ?`)
|
|
219
|
+
.all(queryVector, queryVector, limit, limit);
|
|
188
220
|
return rows.map((r) => ({
|
|
189
221
|
id: r.id,
|
|
190
|
-
score:
|
|
222
|
+
score: this.normalizeVectorDistance(r.distance),
|
|
191
223
|
}));
|
|
192
224
|
}
|
|
193
225
|
catch (err) {
|
|
@@ -239,21 +271,23 @@ export class MemoryStore {
|
|
|
239
271
|
return [];
|
|
240
272
|
const placeholders = ids.map(() => '?').join(', ');
|
|
241
273
|
const rows = this.db
|
|
242
|
-
.prepare(`SELECT id, source, heading, heading_level, content, line_start, line_end
|
|
274
|
+
.prepare(`SELECT id, source, source_type, heading, heading_level, content, line_start, line_end, file_mtime_at
|
|
243
275
|
FROM memory_chunks WHERE id IN (${placeholders})`)
|
|
244
276
|
.all(...ids);
|
|
245
277
|
return rows.map((r) => ({
|
|
246
278
|
id: r.id,
|
|
247
279
|
source: r.source,
|
|
280
|
+
sourceType: r.source_type,
|
|
248
281
|
heading: r.heading,
|
|
249
282
|
headingLevel: r.heading_level,
|
|
250
283
|
content: r.content,
|
|
251
284
|
lineStart: r.line_start,
|
|
252
285
|
lineEnd: r.line_end,
|
|
286
|
+
fileMtimeAt: r.file_mtime_at,
|
|
253
287
|
}));
|
|
254
288
|
}
|
|
255
289
|
get vecAvailable() {
|
|
256
|
-
return
|
|
290
|
+
return true;
|
|
257
291
|
}
|
|
258
292
|
close() {
|
|
259
293
|
this.db.close();
|