@hanhnd/agent-kit 1.0.37 → 1.0.39

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.
@@ -146,6 +146,7 @@ export class MemoryStore {
146
146
  return 0;
147
147
  }
148
148
  upsert(chunks, embeddings) {
149
+ const deleteChunk = this.db.prepare(`DELETE FROM memory_chunks WHERE id = ?`);
149
150
  const insertChunk = this.db.prepare(`
150
151
  INSERT OR REPLACE INTO memory_chunks
151
152
  (id, source, source_type, heading, heading_level, content, line_start, line_end, indexed_at, file_mtime_at, embedding)
@@ -157,7 +158,7 @@ export class MemoryStore {
157
158
  try {
158
159
  const vectorBinding = this.toVectorBinding(embeddings[i]);
159
160
  // Delete existing row first to get correct rowid
160
- this.db.prepare(`DELETE FROM memory_chunks WHERE id = ?`).run(chunk.id);
161
+ deleteChunk.run(chunk.id);
161
162
  insertChunk.run(chunk.id, chunk.source, chunk.sourceType, chunk.heading, chunk.headingLevel, chunk.content, chunk.lineStart, chunk.lineEnd, Date.now(), chunk.fileMtimeAt, vectorBinding);
162
163
  }
163
164
  catch (err) {
@@ -179,17 +180,11 @@ export class MemoryStore {
179
180
  throw new Error('missing embedding');
180
181
  if (embedding.length !== this.config.vectorDimension)
181
182
  throw new Error('embedding dimension mismatch');
182
- const values = Array.from(embedding, (value) => {
183
- if (!Number.isFinite(value))
183
+ for (let i = 0; i < embedding.length; i++) {
184
+ if (!Number.isFinite(embedding[i]))
184
185
  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));
186
+ }
187
+ return Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
193
188
  }
194
189
  hashesBySource(source) {
195
190
  const rows = this.db.prepare(`SELECT id FROM memory_chunks WHERE source = ?`).all(source);
@@ -229,74 +224,18 @@ export class MemoryStore {
229
224
  });
230
225
  deleteAll();
231
226
  }
232
- searchDense(embedding, limit) {
233
- try {
234
- const queryVector = this.toVectorBinding(embedding);
235
- const rows = this.db
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);
242
- return rows.map((r) => ({
243
- id: r.id,
244
- score: this.normalizeVectorDistance(r.distance),
245
- }));
246
- }
247
- catch (err) {
248
- console.warn('[memory-store] Dense search failed:', err);
249
- return [];
250
- }
251
- }
252
- searchBm25(query, limit) {
253
- if (!query.trim())
254
- return [];
227
+ buildFtsQuery(query) {
255
228
  const tokens = query
256
229
  .trim()
257
230
  .split(/\s+/)
258
231
  .map((t) => t.replace(/[^a-zA-Z0-9_]/g, '').toLowerCase())
259
232
  .filter((t) => t.length > 1);
260
- const ftsQuery = removeStopwords(tokens, eng)
233
+ return removeStopwords(tokens, eng)
261
234
  .map((t) => `${t.replace(/s$/, '')}*`)
262
235
  .join(' OR ');
263
- if (!ftsQuery)
264
- return [];
265
- try {
266
- const rows = this.db
267
- .prepare(`SELECT mc.id, bm25(memory_fts, 0.2, 2.0, 5.0) AS rank
268
- FROM memory_fts
269
- JOIN memory_chunks mc ON mc.rowid = memory_fts.rowid
270
- WHERE memory_fts MATCH ?
271
- ORDER BY rank
272
- LIMIT ?`)
273
- .all(ftsQuery, limit);
274
- if (rows.length === 0)
275
- return [];
276
- // BM25 rank is negative in SQLite FTS5 (lower = more relevant)
277
- const ranks = rows.map((r) => r.rank);
278
- const minRank = Math.min(...ranks);
279
- const maxRank = Math.max(...ranks);
280
- const range = maxRank - minRank;
281
- return rows.map((r) => ({
282
- id: r.id,
283
- score: range > 0 ? (maxRank - r.rank) / range : 1,
284
- }));
285
- }
286
- catch (err) {
287
- console.warn('[memory-store] BM25 search failed:', err);
288
- return [];
289
- }
290
236
  }
291
- getChunksByIds(ids) {
292
- if (ids.length === 0)
293
- return [];
294
- const placeholders = ids.map(() => '?').join(', ');
295
- const rows = this.db
296
- .prepare(`SELECT id, source, source_type, heading, heading_level, content, line_start, line_end, file_mtime_at
297
- FROM memory_chunks WHERE id IN (${placeholders})`)
298
- .all(...ids);
299
- return rows.map((r) => ({
237
+ mapMemoryChunkRow(r) {
238
+ return {
300
239
  id: r.id,
301
240
  source: r.source,
302
241
  sourceType: r.source_type,
@@ -306,7 +245,202 @@ export class MemoryStore {
306
245
  lineStart: r.line_start,
307
246
  lineEnd: r.line_end,
308
247
  fileMtimeAt: r.file_mtime_at,
309
- }));
248
+ };
249
+ }
250
+ searchHybrid(options) {
251
+ const { query, embedding, topK, fetchLimit, denseScoreFloor, recencyWeight, rrfK, sourceType, sinceMtimeAt, includeDebug, } = options;
252
+ const ftsQuery = this.buildFtsQuery(query);
253
+ const hasDense = embedding !== undefined;
254
+ const hasBm25 = ftsQuery.length > 0;
255
+ if (!hasDense && !hasBm25)
256
+ return [];
257
+ const hasFilters = sourceType !== undefined || sinceMtimeAt !== undefined;
258
+ try {
259
+ const params = [];
260
+ let denseCte = '';
261
+ let denseRankedCte = '';
262
+ if (hasDense) {
263
+ const queryBuf = this.toVectorBinding(embedding);
264
+ if (hasFilters) {
265
+ // Exact filtered scan — avoids losing candidates due to ANN post-filtering
266
+ const filterClauses = [];
267
+ if (sourceType) {
268
+ filterClauses.push('mc.source_type = ?');
269
+ params.push(sourceType);
270
+ }
271
+ if (sinceMtimeAt !== undefined) {
272
+ filterClauses.push('mc.file_mtime_at >= ?');
273
+ params.push(sinceMtimeAt);
274
+ }
275
+ const whereClause = filterClauses.length > 0 ? `WHERE ${filterClauses.join(' AND ')}` : '';
276
+ params.push(queryBuf); // vector_distance_cos arg
277
+ params.push(fetchLimit);
278
+ denseCte = `dense_data AS (
279
+ SELECT mc.id, mc.source, mc.source_type, mc.file_mtime_at,
280
+ mc.heading, mc.heading_level, mc.content, mc.line_start, mc.line_end,
281
+ vector_distance_cos(mc.embedding, vector32(?)) AS dense_dist
282
+ FROM memory_chunks mc
283
+ ${whereClause}
284
+ ORDER BY dense_dist
285
+ LIMIT ?
286
+ )`;
287
+ }
288
+ else {
289
+ params.push(queryBuf); // vector_distance_cos arg
290
+ params.push(queryBuf); // vector_top_k arg
291
+ params.push(fetchLimit);
292
+ denseCte = `dense_data AS (
293
+ SELECT mc.id, mc.source, mc.source_type, mc.file_mtime_at,
294
+ mc.heading, mc.heading_level, mc.content, mc.line_start, mc.line_end,
295
+ vector_distance_cos(mc.embedding, vector32(?)) AS dense_dist
296
+ FROM vector_top_k('idx_memory_chunks_embedding', vector32(?), ?) AS vtk
297
+ JOIN memory_chunks mc ON mc.rowid = vtk.id
298
+ )`;
299
+ }
300
+ denseRankedCte = `dense_ranked AS (
301
+ SELECT *, ROW_NUMBER() OVER (ORDER BY dense_dist, id) AS dense_rank
302
+ FROM dense_data
303
+ )`;
304
+ }
305
+ let bm25Cte = '';
306
+ let bm25RankedCte = '';
307
+ if (hasBm25) {
308
+ const bm25FilterClauses = [];
309
+ if (sourceType)
310
+ bm25FilterClauses.push('mc.source_type = ?');
311
+ if (sinceMtimeAt !== undefined)
312
+ bm25FilterClauses.push('mc.file_mtime_at >= ?');
313
+ const bm25WhereExtra = bm25FilterClauses.length > 0 ? ` AND ${bm25FilterClauses.join(' AND ')}` : '';
314
+ params.push(ftsQuery);
315
+ if (sourceType)
316
+ params.push(sourceType);
317
+ if (sinceMtimeAt !== undefined)
318
+ params.push(sinceMtimeAt);
319
+ params.push(fetchLimit);
320
+ bm25Cte = `bm25_data AS (
321
+ SELECT mc.id, mc.source, mc.source_type, mc.file_mtime_at,
322
+ mc.heading, mc.heading_level, mc.content, mc.line_start, mc.line_end,
323
+ bm25(memory_fts, 0.2, 2.0, 5.0) AS bm25_neg
324
+ FROM memory_fts
325
+ JOIN memory_chunks mc ON mc.rowid = memory_fts.rowid
326
+ WHERE memory_fts MATCH ?${bm25WhereExtra}
327
+ LIMIT ?
328
+ )`;
329
+ bm25RankedCte = `bm25_ranked AS (
330
+ SELECT *, ROW_NUMBER() OVER (ORDER BY bm25_neg, id) AS bm25_rank
331
+ FROM bm25_data
332
+ )`;
333
+ }
334
+ let candidatesCte = '';
335
+ if (hasDense && hasBm25) {
336
+ candidatesCte = `all_candidates AS (
337
+ SELECT d.id, d.source, d.source_type, d.file_mtime_at,
338
+ d.heading, d.heading_level, d.content, d.line_start, d.line_end,
339
+ CAST(d.dense_rank AS INTEGER) AS dense_rank, d.dense_dist,
340
+ b.bm25_rank,
341
+ CASE WHEN b.id IS NOT NULL THEN 'both' ELSE 'dense' END AS retriever
342
+ FROM dense_ranked d
343
+ LEFT JOIN bm25_ranked b ON b.id = d.id
344
+ UNION ALL
345
+ SELECT b.id, b.source, b.source_type, b.file_mtime_at,
346
+ b.heading, b.heading_level, b.content, b.line_start, b.line_end,
347
+ NULL AS dense_rank, NULL AS dense_dist,
348
+ CAST(b.bm25_rank AS INTEGER) AS bm25_rank,
349
+ 'bm25' AS retriever
350
+ FROM bm25_ranked b
351
+ LEFT JOIN dense_ranked d ON d.id = b.id
352
+ WHERE d.id IS NULL
353
+ )`;
354
+ }
355
+ else if (hasDense) {
356
+ candidatesCte = `all_candidates AS (
357
+ SELECT id, source, source_type, file_mtime_at,
358
+ heading, heading_level, content, line_start, line_end,
359
+ CAST(dense_rank AS INTEGER) AS dense_rank, dense_dist,
360
+ NULL AS bm25_rank, 'dense' AS retriever
361
+ FROM dense_ranked
362
+ )`;
363
+ }
364
+ else {
365
+ candidatesCte = `all_candidates AS (
366
+ SELECT id, source, source_type, file_mtime_at,
367
+ heading, heading_level, content, line_start, line_end,
368
+ NULL AS dense_rank, NULL AS dense_dist,
369
+ CAST(bm25_rank AS INTEGER) AS bm25_rank, 'bm25' AS retriever
370
+ FROM bm25_ranked
371
+ )`;
372
+ }
373
+ // Scoring: RRF over active channels + recency, dense floor, source dedup
374
+ params.push(rrfK, rrfK, recencyWeight, rrfK, denseScoreFloor, topK);
375
+ const sql = `
376
+ WITH
377
+ ${[denseCte, denseRankedCte, bm25Cte, bm25RankedCte, candidatesCte].filter(Boolean).join(',\n')}
378
+ ,
379
+ recency_ranked AS (
380
+ SELECT id, ROW_NUMBER() OVER (ORDER BY file_mtime_at DESC, source, id) AS recency_rank
381
+ FROM all_candidates
382
+ ),
383
+ scored AS (
384
+ SELECT c.id, c.source, c.source_type, c.file_mtime_at,
385
+ c.heading, c.heading_level, c.content, c.line_start, c.line_end,
386
+ c.dense_rank, c.dense_dist, c.bm25_rank, r.recency_rank,
387
+ c.retriever,
388
+ COALESCE(1.0 / (? + c.dense_rank), 0.0) +
389
+ COALESCE(1.0 / (? + c.bm25_rank), 0.0) +
390
+ ? * (1.0 / (? + r.recency_rank)) AS total_score
391
+ FROM all_candidates c
392
+ JOIN recency_ranked r ON r.id = c.id
393
+ WHERE c.retriever != 'dense' OR (1.0 - c.dense_dist) >= ?
394
+ ),
395
+ best_per_source AS (
396
+ SELECT *,
397
+ ROW_NUMBER() OVER (PARTITION BY source ORDER BY total_score DESC, source, id) AS source_rank
398
+ FROM scored
399
+ )
400
+ SELECT id, source, source_type, file_mtime_at, heading, heading_level, content, line_start, line_end,
401
+ dense_rank, dense_dist, bm25_rank, recency_rank, total_score, retriever
402
+ FROM best_per_source
403
+ WHERE source_rank = 1
404
+ ORDER BY total_score DESC, source, id
405
+ LIMIT ?
406
+ `;
407
+ const rows = this.db.prepare(sql).all(...params);
408
+ if (rows.length === 0)
409
+ return [];
410
+ const denseActive = hasDense;
411
+ const bm25Active = hasBm25;
412
+ const recencyActive = rows.length > 0;
413
+ const maxScore = ((denseActive ? 1 : 0) + (bm25Active ? 1 : 0) + (recencyActive ? recencyWeight : 0)) / (rrfK + 1);
414
+ return rows.map((r) => {
415
+ const chunk = this.mapMemoryChunkRow(r);
416
+ const normalizedScore = maxScore > 0 ? r.total_score / maxScore : 0;
417
+ const row = {
418
+ chunk,
419
+ score: normalizedScore,
420
+ retriever: r.retriever,
421
+ };
422
+ if (includeDebug) {
423
+ const denseScore = r.dense_dist !== null ? Math.max(0, Math.min(1, 1 - r.dense_dist)) : undefined;
424
+ const bm25Score = r.bm25_rank !== null ? 1 / (rrfK + r.bm25_rank) : undefined;
425
+ const recencyScore = recencyWeight * (1 / (rrfK + r.recency_rank));
426
+ const debug = {
427
+ denseRank: r.dense_rank ?? undefined,
428
+ denseScore,
429
+ bm25Rank: r.bm25_rank ?? undefined,
430
+ bm25Score,
431
+ recencyRank: r.recency_rank,
432
+ recencyScore,
433
+ totalScore: r.total_score,
434
+ };
435
+ row.debug = debug;
436
+ }
437
+ return row;
438
+ });
439
+ }
440
+ catch (err) {
441
+ console.warn('[memory-store] Hybrid search failed:', err);
442
+ return [];
443
+ }
310
444
  }
311
445
  get vecAvailable() {
312
446
  return true;