@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.
- package/dist/cli.js +241 -112
- package/dist/mcp/memory.js +1 -1
- package/dist/server.js +244 -115
- package/dist/services/digest/digest-processor.test.js +1 -3
- package/dist/services/memory/constants.d.ts +3 -0
- package/dist/services/memory/constants.js +6 -0
- package/dist/services/memory/indexer.js +37 -54
- package/dist/services/memory/indexer.test.js +229 -231
- package/dist/services/memory/store.d.ts +4 -11
- package/dist/services/memory/store.js +206 -72
- package/dist/services/memory/store.test.js +359 -108
- package/dist/services/memory/types.d.ts +28 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
183
|
-
if (!Number.isFinite(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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;
|