@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.
@@ -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 real store vector rows when sqlite-vec is available', async () => {
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
- if (store.vecAvailable) {
78
- const db = new Database(dbPath, { readonly: true });
79
- try {
80
- loadSqliteVec(db);
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: 384,
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;
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 'better-sqlite3';
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 require = createRequire(import.meta.url);
7
- export const SCHEMA_VERSION = 2;
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 Database(dbPath);
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
- const db = new Database(dbPath);
43
- db.pragma('journal_mode = WAL');
44
- db.pragma('busy_timeout = 5000');
45
- this.loadVecExtension(db);
46
- this.createSchema(db);
47
- return db;
48
- }
49
- }
50
- loadVecExtension(db) {
51
- try {
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 = Number(db.pragma('user_version', { simple: true }) ?? 0);
64
- if (current < SCHEMA_VERSION) {
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
- const info = insertChunk.run(chunk.id, chunk.source, chunk.sourceType, chunk.heading, chunk.headingLevel, chunk.content, chunk.lineStart, chunk.lineEnd, Date.now(), chunk.fileMtimeAt);
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 buf = Buffer.from(embedding.buffer);
234
+ const queryVector = this.toVectorBinding(embedding);
219
235
  const rows = this.db
220
- .prepare(`SELECT mc.id, mv.distance
221
- FROM memory_vec mv
222
- JOIN memory_chunks mc ON mc.rowid = mv.rowid
223
- WHERE mv.embedding MATCH ? AND k = ?
224
- ORDER BY mv.distance`)
225
- .all(buf, limit);
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: Math.max(0, 1 - r.distance),
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 this._vecAvailable;
312
+ return true;
297
313
  }
298
314
  close() {
299
315
  this.db.close();