@hanhnd/agent-kit 1.0.35 → 1.0.37
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.js +93 -73
- package/dist/core/config/index.d.ts +1 -1
- package/dist/core/config/index.js +15 -1
- package/dist/server.js +93 -73
- package/dist/services/digest/digest-processor.test.js +5 -19
- package/dist/services/memory/constants.d.ts +2 -1
- package/dist/services/memory/constants.js +6 -1
- package/dist/services/memory/memory.test.js +10 -0
- package/dist/services/memory/store.d.ts +5 -3
- package/dist/services/memory/store.js +90 -74
- package/dist/services/memory/store.test.js +163 -15
- package/package.json +3 -5
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import * as assert from 'node:assert/strict';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
-
import Database from 'better-sqlite3';
|
|
5
|
-
import { load as loadSqliteVec } from 'sqlite-vec';
|
|
6
4
|
import { afterEach, describe, test } from 'node:test';
|
|
7
5
|
import { createTempDirTracker } from '../../utils/temp-dir.test.js';
|
|
8
6
|
import { DEFAULT_DIGEST_MODEL_ID } from './constants.js';
|
|
@@ -53,7 +51,7 @@ describe('digestConversationFile', () => {
|
|
|
53
51
|
assert.equal(result.contentHash, input.contentHash);
|
|
54
52
|
assert.match(path.basename(result.markdown), /^[a-f0-9]{16}-conv\.md$/);
|
|
55
53
|
});
|
|
56
|
-
test('indexes existing provisional digest with
|
|
54
|
+
test('indexes existing provisional digest with searchable store vector data', async () => {
|
|
57
55
|
const workspace = tempDirs.makeTempDir('digest-processor-index-');
|
|
58
56
|
const inputPath = path.join(workspace, '.agent-kit', 'wiki', 'archive', 'conversations', 'conv.md');
|
|
59
57
|
fs.mkdirSync(path.dirname(inputPath), { recursive: true });
|
|
@@ -74,22 +72,10 @@ describe('digestConversationFile', () => {
|
|
|
74
72
|
});
|
|
75
73
|
assert.equal(result.indexed, true);
|
|
76
74
|
assert.equal(result.markdown, markdownPath);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const row = db
|
|
82
|
-
.prepare(`SELECT COUNT(*) AS count
|
|
83
|
-
FROM memory_vec mv
|
|
84
|
-
JOIN memory_chunks mc ON mc.rowid = mv.rowid
|
|
85
|
-
WHERE mc.source = ?`)
|
|
86
|
-
.get(path.relative(config.wikiDir, markdownPath));
|
|
87
|
-
assert.ok(row.count > 0, `Expected vector rows for ${markdownPath}`);
|
|
88
|
-
}
|
|
89
|
-
finally {
|
|
90
|
-
db.close();
|
|
91
|
-
}
|
|
92
|
-
}
|
|
75
|
+
const denseResults = store.searchDense(new Float32Array(384).fill(0.05), 5);
|
|
76
|
+
const indexedSource = path.relative(config.wikiDir, markdownPath);
|
|
77
|
+
const indexedChunks = store.getChunksByIds(denseResults.map((denseResult) => denseResult.id));
|
|
78
|
+
assert.ok(indexedChunks.some((chunk) => chunk.source === indexedSource), `Expected dense search to return a chunk for ${indexedSource}`);
|
|
93
79
|
}
|
|
94
80
|
finally {
|
|
95
81
|
store.close();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type MemoryConfig } from './types.js';
|
|
1
|
+
import { type MemoryConfig, EmbeddingModelName } from './types.js';
|
|
2
|
+
export declare const EMBEDDING_MODEL_DIMENSIONS: Record<EmbeddingModelName, number>;
|
|
2
3
|
export declare const DEFAULT_MEMORY_CONFIG: MemoryConfig;
|
|
3
4
|
export declare const LOCK_RETRY_MS = 50;
|
|
4
5
|
export declare const LOCK_TIMEOUT_MS = 500;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { EmbeddingModelName } from './types.js';
|
|
2
|
+
export const EMBEDDING_MODEL_DIMENSIONS = {
|
|
3
|
+
[EmbeddingModelName.TINY]: 384,
|
|
4
|
+
[EmbeddingModelName.BASE]: 384,
|
|
5
|
+
[EmbeddingModelName.LARGE]: 768,
|
|
6
|
+
};
|
|
2
7
|
export const DEFAULT_MEMORY_CONFIG = {
|
|
3
8
|
enabled: false,
|
|
4
9
|
wikiDir: '',
|
|
@@ -6,7 +11,7 @@ export const DEFAULT_MEMORY_CONFIG = {
|
|
|
6
11
|
chunkSize: 1500,
|
|
7
12
|
overlapLines: 2,
|
|
8
13
|
embeddingModel: EmbeddingModelName.BASE,
|
|
9
|
-
vectorDimension:
|
|
14
|
+
vectorDimension: EMBEDDING_MODEL_DIMENSIONS[EmbeddingModelName.BASE],
|
|
10
15
|
};
|
|
11
16
|
export const LOCK_RETRY_MS = 50;
|
|
12
17
|
export const LOCK_TIMEOUT_MS = 500;
|
|
@@ -402,4 +402,14 @@ describe('resolveMemoryConfig', () => {
|
|
|
402
402
|
const config = resolveMemoryConfig({ memory: { wikiDir: '/custom/wiki' } }, '/repo');
|
|
403
403
|
assert.equal(config.wikiDir, '/custom/wiki');
|
|
404
404
|
});
|
|
405
|
+
test('derives vector dimension from embedding model', () => {
|
|
406
|
+
const config = resolveMemoryConfig({ memory: { embeddingModel: EmbeddingModelName.LARGE } }, '/repo');
|
|
407
|
+
assert.equal(config.embeddingModel, EmbeddingModelName.LARGE);
|
|
408
|
+
assert.equal(config.vectorDimension, 768);
|
|
409
|
+
});
|
|
410
|
+
test('preserves explicit vector dimension override', () => {
|
|
411
|
+
const config = resolveMemoryConfig({ memory: { embeddingModel: EmbeddingModelName.LARGE, vectorDimension: 1024 } }, '/repo');
|
|
412
|
+
assert.equal(config.embeddingModel, EmbeddingModelName.LARGE);
|
|
413
|
+
assert.equal(config.vectorDimension, 1024);
|
|
414
|
+
});
|
|
405
415
|
});
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import type { MemoryChunk, MemoryConfig, RecentSource, SourceType } from './types.js';
|
|
2
|
-
export declare const SCHEMA_VERSION =
|
|
2
|
+
export declare const SCHEMA_VERSION = 4;
|
|
3
3
|
export declare class StoreError extends Error {
|
|
4
4
|
constructor(message: string, cause?: unknown);
|
|
5
5
|
}
|
|
6
6
|
export declare class MemoryStore {
|
|
7
7
|
private readonly config;
|
|
8
8
|
private db;
|
|
9
|
-
private _vecAvailable;
|
|
10
9
|
constructor(dbPath: string, config: MemoryConfig);
|
|
11
10
|
private openDatabase;
|
|
12
|
-
private loadVecExtension;
|
|
13
11
|
private createSchema;
|
|
12
|
+
private readEmbeddingColumnDimension;
|
|
13
|
+
private readUserVersion;
|
|
14
14
|
upsert(chunks: MemoryChunk[], embeddings: Float32Array[]): void;
|
|
15
|
+
private toVectorBinding;
|
|
16
|
+
private normalizeVectorDistance;
|
|
15
17
|
hashesBySource(source: string): Set<string>;
|
|
16
18
|
indexedSources(): string[];
|
|
17
19
|
getRecentSources(options: {
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import Database from '
|
|
1
|
+
import Database from 'libsql';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
|
-
import { createRequire } from 'module';
|
|
4
3
|
import * as path from 'path';
|
|
5
4
|
import { eng, removeStopwords } from 'stopword';
|
|
6
|
-
const
|
|
7
|
-
|
|
5
|
+
export const SCHEMA_VERSION = 4;
|
|
6
|
+
const LibsqlDatabase = Database;
|
|
8
7
|
export class StoreError extends Error {
|
|
9
8
|
constructor(message, cause) {
|
|
10
9
|
super(message);
|
|
@@ -16,7 +15,6 @@ export class StoreError extends Error {
|
|
|
16
15
|
export class MemoryStore {
|
|
17
16
|
config;
|
|
18
17
|
db;
|
|
19
|
-
_vecAvailable = false;
|
|
20
18
|
constructor(dbPath, config) {
|
|
21
19
|
this.config = config;
|
|
22
20
|
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
@@ -24,10 +22,9 @@ export class MemoryStore {
|
|
|
24
22
|
}
|
|
25
23
|
openDatabase(dbPath) {
|
|
26
24
|
try {
|
|
27
|
-
const db = new
|
|
25
|
+
const db = new LibsqlDatabase(dbPath);
|
|
28
26
|
db.pragma('journal_mode = WAL');
|
|
29
27
|
db.pragma('busy_timeout = 5000');
|
|
30
|
-
this.loadVecExtension(db);
|
|
31
28
|
this.createSchema(db);
|
|
32
29
|
return db;
|
|
33
30
|
}
|
|
@@ -39,34 +36,30 @@ export class MemoryStore {
|
|
|
39
36
|
catch {
|
|
40
37
|
// ignore
|
|
41
38
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const sqliteVec = require('sqlite-vec');
|
|
53
|
-
sqliteVec.load(db);
|
|
54
|
-
this._vecAvailable = true;
|
|
55
|
-
}
|
|
56
|
-
catch (err) {
|
|
57
|
-
console.warn('[memory-store] sqlite-vec unavailable, degrading to FTS5-only:', err);
|
|
58
|
-
this._vecAvailable = false;
|
|
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
|
+
}
|
|
59
49
|
}
|
|
60
50
|
}
|
|
61
51
|
createSchema(db) {
|
|
62
52
|
try {
|
|
63
|
-
const current =
|
|
64
|
-
|
|
53
|
+
const current = this.readUserVersion(db);
|
|
54
|
+
const currentVectorDimension = current >= SCHEMA_VERSION ? this.readEmbeddingColumnDimension(db) : undefined;
|
|
55
|
+
if (current < SCHEMA_VERSION ||
|
|
56
|
+
(currentVectorDimension !== undefined && currentVectorDimension !== this.config.vectorDimension)) {
|
|
65
57
|
db.exec(`
|
|
66
58
|
DROP TRIGGER IF EXISTS memory_chunks_ai;
|
|
67
59
|
DROP TRIGGER IF EXISTS memory_chunks_ad;
|
|
68
60
|
DROP TABLE IF EXISTS memory_fts;
|
|
69
61
|
DROP TABLE IF EXISTS memory_vec;
|
|
62
|
+
DROP INDEX IF EXISTS idx_memory_chunks_embedding;
|
|
70
63
|
DROP TABLE IF EXISTS memory_chunks;
|
|
71
64
|
`);
|
|
72
65
|
db.pragma(`user_version = ${SCHEMA_VERSION}`);
|
|
@@ -83,7 +76,8 @@ export class MemoryStore {
|
|
|
83
76
|
line_start INTEGER NOT NULL,
|
|
84
77
|
line_end INTEGER NOT NULL,
|
|
85
78
|
indexed_at INTEGER NOT NULL,
|
|
86
|
-
file_mtime_at INTEGER NOT NULL DEFAULT 0
|
|
79
|
+
file_mtime_at INTEGER NOT NULL DEFAULT 0,
|
|
80
|
+
embedding F32_BLOB(${this.config.vectorDimension}) NOT NULL
|
|
87
81
|
);
|
|
88
82
|
|
|
89
83
|
CREATE INDEX IF NOT EXISTS idx_memory_chunks_type_mtime
|
|
@@ -108,48 +102,63 @@ export class MemoryStore {
|
|
|
108
102
|
INSERT INTO memory_fts(memory_fts, rowid, source, heading, content)
|
|
109
103
|
VALUES ('delete', old.rowid, old.source, old.heading, old.content);
|
|
110
104
|
END;
|
|
105
|
+
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_memory_chunks_embedding
|
|
107
|
+
ON memory_chunks (libsql_vector_idx(embedding));
|
|
111
108
|
`);
|
|
112
|
-
if (this._vecAvailable) {
|
|
113
|
-
try {
|
|
114
|
-
db.exec(`
|
|
115
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS memory_vec USING vec0(
|
|
116
|
-
embedding FLOAT[${this.config.vectorDimension}]
|
|
117
|
-
);
|
|
118
|
-
`);
|
|
119
|
-
}
|
|
120
|
-
catch (err) {
|
|
121
|
-
console.warn('[memory-store] Failed to create memory_vec, disabling vector search:', err);
|
|
122
|
-
this._vecAvailable = false;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
109
|
}
|
|
126
110
|
catch (err) {
|
|
127
111
|
throw new StoreError('schema migration failed', err);
|
|
128
112
|
}
|
|
129
113
|
}
|
|
114
|
+
readEmbeddingColumnDimension(db) {
|
|
115
|
+
try {
|
|
116
|
+
const rows = db.pragma('table_info(memory_chunks)');
|
|
117
|
+
if (!Array.isArray(rows))
|
|
118
|
+
return undefined;
|
|
119
|
+
const embeddingColumn = rows.find((row) => {
|
|
120
|
+
if (!row || typeof row !== 'object')
|
|
121
|
+
return false;
|
|
122
|
+
const candidate = row;
|
|
123
|
+
return candidate.name === 'embedding' && typeof candidate.type === 'string';
|
|
124
|
+
});
|
|
125
|
+
const match = /^F32_BLOB\((\d+)\)$/i.exec(embeddingColumn?.type.trim() ?? '');
|
|
126
|
+
if (!match)
|
|
127
|
+
return undefined;
|
|
128
|
+
return Number.parseInt(match[1], 10);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
readUserVersion(db) {
|
|
135
|
+
const value = db.pragma('user_version', { simple: true });
|
|
136
|
+
if (typeof value === 'number')
|
|
137
|
+
return value;
|
|
138
|
+
if (Array.isArray(value)) {
|
|
139
|
+
const [row] = value;
|
|
140
|
+
return typeof row?.user_version === 'number' ? row.user_version : 0;
|
|
141
|
+
}
|
|
142
|
+
if (value && typeof value === 'object' && 'user_version' in value) {
|
|
143
|
+
const row = value;
|
|
144
|
+
return typeof row.user_version === 'number' ? row.user_version : 0;
|
|
145
|
+
}
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
130
148
|
upsert(chunks, embeddings) {
|
|
131
149
|
const insertChunk = this.db.prepare(`
|
|
132
150
|
INSERT OR REPLACE INTO memory_chunks
|
|
133
|
-
(id, source, source_type, heading, heading_level, content, line_start, line_end, indexed_at, file_mtime_at)
|
|
134
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
151
|
+
(id, source, source_type, heading, heading_level, content, line_start, line_end, indexed_at, file_mtime_at, embedding)
|
|
152
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?))
|
|
135
153
|
`);
|
|
136
|
-
let insertVec = null;
|
|
137
|
-
if (this._vecAvailable) {
|
|
138
|
-
insertVec = this.db.prepare(`INSERT OR REPLACE INTO memory_vec (rowid, embedding) VALUES (?, ?)`);
|
|
139
|
-
}
|
|
140
154
|
const upsertAll = this.db.transaction(() => {
|
|
141
155
|
for (let i = 0; i < chunks.length; i++) {
|
|
142
156
|
const chunk = chunks[i];
|
|
143
157
|
try {
|
|
158
|
+
const vectorBinding = this.toVectorBinding(embeddings[i]);
|
|
144
159
|
// Delete existing row first to get correct rowid
|
|
145
160
|
this.db.prepare(`DELETE FROM memory_chunks WHERE id = ?`).run(chunk.id);
|
|
146
|
-
|
|
147
|
-
if (insertVec && embeddings[i] && info.lastInsertRowid) {
|
|
148
|
-
const rowid = typeof info.lastInsertRowid === 'bigint' ? info.lastInsertRowid : BigInt(info.lastInsertRowid);
|
|
149
|
-
const emb = embeddings[i];
|
|
150
|
-
const buf = Buffer.from(emb.buffer, emb.byteOffset, emb.byteLength);
|
|
151
|
-
insertVec.run(rowid, buf);
|
|
152
|
-
}
|
|
161
|
+
insertChunk.run(chunk.id, chunk.source, chunk.sourceType, chunk.heading, chunk.headingLevel, chunk.content, chunk.lineStart, chunk.lineEnd, Date.now(), chunk.fileMtimeAt, vectorBinding);
|
|
153
162
|
}
|
|
154
163
|
catch (err) {
|
|
155
164
|
console.warn(`[memory-store] Failed to upsert chunk ${chunk.id}:`, err);
|
|
@@ -163,6 +172,25 @@ export class MemoryStore {
|
|
|
163
172
|
throw new StoreError('Full batch upsert failed', err);
|
|
164
173
|
}
|
|
165
174
|
}
|
|
175
|
+
toVectorBinding(embedding) {
|
|
176
|
+
if (!embedding)
|
|
177
|
+
throw new Error('missing embedding');
|
|
178
|
+
if (!(embedding instanceof Float32Array))
|
|
179
|
+
throw new Error('missing embedding');
|
|
180
|
+
if (embedding.length !== this.config.vectorDimension)
|
|
181
|
+
throw new Error('embedding dimension mismatch');
|
|
182
|
+
const values = Array.from(embedding, (value) => {
|
|
183
|
+
if (!Number.isFinite(value))
|
|
184
|
+
throw new Error('embedding contains non-finite value');
|
|
185
|
+
return value;
|
|
186
|
+
});
|
|
187
|
+
return JSON.stringify(values);
|
|
188
|
+
}
|
|
189
|
+
normalizeVectorDistance(distance) {
|
|
190
|
+
if (distance <= 0)
|
|
191
|
+
return 1;
|
|
192
|
+
return Math.max(0, Math.min(1, 1 - distance));
|
|
193
|
+
}
|
|
166
194
|
hashesBySource(source) {
|
|
167
195
|
const rows = this.db.prepare(`SELECT id FROM memory_chunks WHERE source = ?`).all(source);
|
|
168
196
|
return new Set(rows.map((r) => r.id));
|
|
@@ -187,12 +215,7 @@ export class MemoryStore {
|
|
|
187
215
|
}
|
|
188
216
|
}
|
|
189
217
|
deleteBySource(source) {
|
|
190
|
-
const rows = this.db.prepare(`SELECT rowid FROM memory_chunks WHERE source = ?`).all(source);
|
|
191
218
|
const deleteAll = this.db.transaction(() => {
|
|
192
|
-
if (this._vecAvailable && rows.length > 0) {
|
|
193
|
-
const placeholders = rows.map(() => '?').join(', ');
|
|
194
|
-
this.db.prepare(`DELETE FROM memory_vec WHERE rowid IN (${placeholders})`).run(...rows.map((r) => r.rowid));
|
|
195
|
-
}
|
|
196
219
|
this.db.prepare(`DELETE FROM memory_chunks WHERE source = ?`).run(source);
|
|
197
220
|
});
|
|
198
221
|
deleteAll();
|
|
@@ -201,31 +224,24 @@ export class MemoryStore {
|
|
|
201
224
|
if (ids.length === 0)
|
|
202
225
|
return;
|
|
203
226
|
const placeholders = ids.map(() => '?').join(', ');
|
|
204
|
-
const rows = this.db.prepare(`SELECT rowid FROM memory_chunks WHERE id IN (${placeholders})`).all(...ids);
|
|
205
227
|
const deleteAll = this.db.transaction(() => {
|
|
206
|
-
if (this._vecAvailable && rows.length > 0) {
|
|
207
|
-
const rPlaceholders = rows.map(() => '?').join(', ');
|
|
208
|
-
this.db.prepare(`DELETE FROM memory_vec WHERE rowid IN (${rPlaceholders})`).run(...rows.map((r) => r.rowid));
|
|
209
|
-
}
|
|
210
228
|
this.db.prepare(`DELETE FROM memory_chunks WHERE id IN (${placeholders})`).run(...ids);
|
|
211
229
|
});
|
|
212
230
|
deleteAll();
|
|
213
231
|
}
|
|
214
232
|
searchDense(embedding, limit) {
|
|
215
|
-
if (!this._vecAvailable)
|
|
216
|
-
return [];
|
|
217
233
|
try {
|
|
218
|
-
const
|
|
234
|
+
const queryVector = this.toVectorBinding(embedding);
|
|
219
235
|
const rows = this.db
|
|
220
|
-
.prepare(`SELECT mc.id,
|
|
221
|
-
FROM
|
|
222
|
-
JOIN memory_chunks mc ON mc.rowid =
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
.all(
|
|
236
|
+
.prepare(`SELECT mc.id, vector_distance_cos(mc.embedding, vector32(?)) AS distance
|
|
237
|
+
FROM vector_top_k('idx_memory_chunks_embedding', vector32(?), ?) AS vector_matches
|
|
238
|
+
JOIN memory_chunks mc ON mc.rowid = vector_matches.id
|
|
239
|
+
ORDER BY distance
|
|
240
|
+
LIMIT ?`)
|
|
241
|
+
.all(queryVector, queryVector, limit, limit);
|
|
226
242
|
return rows.map((r) => ({
|
|
227
243
|
id: r.id,
|
|
228
|
-
score:
|
|
244
|
+
score: this.normalizeVectorDistance(r.distance),
|
|
229
245
|
}));
|
|
230
246
|
}
|
|
231
247
|
catch (err) {
|
|
@@ -293,7 +309,7 @@ export class MemoryStore {
|
|
|
293
309
|
}));
|
|
294
310
|
}
|
|
295
311
|
get vecAvailable() {
|
|
296
|
-
return
|
|
312
|
+
return true;
|
|
297
313
|
}
|
|
298
314
|
close() {
|
|
299
315
|
this.db.close();
|