@aitytech/agentkits-memory 1.0.1 → 2.0.0

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.
Files changed (105) hide show
  1. package/README.md +54 -5
  2. package/dist/better-sqlite3-backend.d.ts +192 -0
  3. package/dist/better-sqlite3-backend.d.ts.map +1 -0
  4. package/dist/better-sqlite3-backend.js +801 -0
  5. package/dist/better-sqlite3-backend.js.map +1 -0
  6. package/dist/cli/save.js +0 -0
  7. package/dist/cli/setup.d.ts +6 -2
  8. package/dist/cli/setup.d.ts.map +1 -1
  9. package/dist/cli/setup.js +289 -42
  10. package/dist/cli/setup.js.map +1 -1
  11. package/dist/cli/viewer.js +25 -56
  12. package/dist/cli/viewer.js.map +1 -1
  13. package/dist/cli/web-viewer.d.ts +2 -1
  14. package/dist/cli/web-viewer.d.ts.map +1 -1
  15. package/dist/cli/web-viewer.js +791 -141
  16. package/dist/cli/web-viewer.js.map +1 -1
  17. package/dist/embeddings/embedding-cache.d.ts +131 -0
  18. package/dist/embeddings/embedding-cache.d.ts.map +1 -0
  19. package/dist/embeddings/embedding-cache.js +217 -0
  20. package/dist/embeddings/embedding-cache.js.map +1 -0
  21. package/dist/embeddings/index.d.ts +11 -0
  22. package/dist/embeddings/index.d.ts.map +1 -0
  23. package/dist/embeddings/index.js +11 -0
  24. package/dist/embeddings/index.js.map +1 -0
  25. package/dist/embeddings/local-embeddings.d.ts +140 -0
  26. package/dist/embeddings/local-embeddings.d.ts.map +1 -0
  27. package/dist/embeddings/local-embeddings.js +293 -0
  28. package/dist/embeddings/local-embeddings.js.map +1 -0
  29. package/dist/hooks/context.d.ts +6 -1
  30. package/dist/hooks/context.d.ts.map +1 -1
  31. package/dist/hooks/context.js +12 -2
  32. package/dist/hooks/context.js.map +1 -1
  33. package/dist/hooks/observation.d.ts +6 -1
  34. package/dist/hooks/observation.d.ts.map +1 -1
  35. package/dist/hooks/observation.js +12 -2
  36. package/dist/hooks/observation.js.map +1 -1
  37. package/dist/hooks/service.d.ts +1 -6
  38. package/dist/hooks/service.d.ts.map +1 -1
  39. package/dist/hooks/service.js +33 -85
  40. package/dist/hooks/service.js.map +1 -1
  41. package/dist/hooks/session-init.d.ts +6 -1
  42. package/dist/hooks/session-init.d.ts.map +1 -1
  43. package/dist/hooks/session-init.js +12 -2
  44. package/dist/hooks/session-init.js.map +1 -1
  45. package/dist/hooks/summarize.d.ts +6 -1
  46. package/dist/hooks/summarize.d.ts.map +1 -1
  47. package/dist/hooks/summarize.js +12 -2
  48. package/dist/hooks/summarize.js.map +1 -1
  49. package/dist/index.d.ts +10 -17
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +172 -94
  52. package/dist/index.js.map +1 -1
  53. package/dist/mcp/server.js +17 -3
  54. package/dist/mcp/server.js.map +1 -1
  55. package/dist/migration.js +3 -3
  56. package/dist/migration.js.map +1 -1
  57. package/dist/search/hybrid-search.d.ts +262 -0
  58. package/dist/search/hybrid-search.d.ts.map +1 -0
  59. package/dist/search/hybrid-search.js +688 -0
  60. package/dist/search/hybrid-search.js.map +1 -0
  61. package/dist/search/index.d.ts +13 -0
  62. package/dist/search/index.d.ts.map +1 -0
  63. package/dist/search/index.js +13 -0
  64. package/dist/search/index.js.map +1 -0
  65. package/dist/search/token-economics.d.ts +161 -0
  66. package/dist/search/token-economics.d.ts.map +1 -0
  67. package/dist/search/token-economics.js +239 -0
  68. package/dist/search/token-economics.js.map +1 -0
  69. package/dist/types.d.ts +0 -68
  70. package/dist/types.d.ts.map +1 -1
  71. package/dist/types.js.map +1 -1
  72. package/package.json +5 -3
  73. package/src/__tests__/better-sqlite3-backend.test.ts +1466 -0
  74. package/src/__tests__/cache-manager.test.ts +499 -0
  75. package/src/__tests__/embedding-integration.test.ts +481 -0
  76. package/src/__tests__/hnsw-index.test.ts +727 -0
  77. package/src/__tests__/index.test.ts +432 -0
  78. package/src/better-sqlite3-backend.ts +1000 -0
  79. package/src/cli/setup.ts +358 -47
  80. package/src/cli/viewer.ts +28 -63
  81. package/src/cli/web-viewer.ts +936 -182
  82. package/src/embeddings/__tests__/embedding-cache.test.ts +269 -0
  83. package/src/embeddings/__tests__/local-embeddings.test.ts +495 -0
  84. package/src/embeddings/embedding-cache.ts +318 -0
  85. package/src/embeddings/index.ts +20 -0
  86. package/src/embeddings/local-embeddings.ts +419 -0
  87. package/src/hooks/__tests__/handlers.test.ts +58 -17
  88. package/src/hooks/__tests__/integration.test.ts +77 -26
  89. package/src/hooks/context.ts +13 -2
  90. package/src/hooks/observation.ts +13 -2
  91. package/src/hooks/service.ts +39 -100
  92. package/src/hooks/session-init.ts +13 -2
  93. package/src/hooks/summarize.ts +13 -2
  94. package/src/index.ts +210 -116
  95. package/src/mcp/server.ts +20 -3
  96. package/src/search/__tests__/hybrid-search.test.ts +669 -0
  97. package/src/search/__tests__/token-economics.test.ts +276 -0
  98. package/src/search/hybrid-search.ts +968 -0
  99. package/src/search/index.ts +29 -0
  100. package/src/search/token-economics.ts +367 -0
  101. package/src/types.ts +0 -96
  102. package/src/__tests__/sqljs-backend.test.ts +0 -410
  103. package/src/migration.ts +0 -574
  104. package/src/sql.js.d.ts +0 -70
  105. package/src/sqljs-backend.ts +0 -789
@@ -0,0 +1,499 @@
1
+ /**
2
+ * Tests for CacheManager
3
+ *
4
+ * Tests LRU cache functionality, TTL expiration, memory pressure handling,
5
+ * and cache statistics.
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
9
+ import { CacheManager, TieredCacheManager } from '../cache-manager.js';
10
+
11
+ describe('CacheManager', () => {
12
+ let cache: CacheManager<string>;
13
+
14
+ beforeEach(() => {
15
+ cache = new CacheManager<string>({
16
+ maxSize: 100,
17
+ ttl: 60000, // 1 minute
18
+ });
19
+ });
20
+
21
+ afterEach(() => {
22
+ cache.shutdown();
23
+ });
24
+
25
+ describe('basic operations', () => {
26
+ it('should set and get values', () => {
27
+ cache.set('key1', 'value1');
28
+ expect(cache.get('key1')).toBe('value1');
29
+ });
30
+
31
+ it('should return null for missing keys', () => {
32
+ expect(cache.get('nonexistent')).toBeNull();
33
+ });
34
+
35
+ it('should delete values', () => {
36
+ cache.set('key1', 'value1');
37
+ expect(cache.delete('key1')).toBe(true);
38
+ expect(cache.get('key1')).toBeNull();
39
+ });
40
+
41
+ it('should return false when deleting nonexistent key', () => {
42
+ expect(cache.delete('nonexistent')).toBe(false);
43
+ });
44
+
45
+ it('should check if key exists', () => {
46
+ cache.set('key1', 'value1');
47
+ expect(cache.has('key1')).toBe(true);
48
+ expect(cache.has('nonexistent')).toBe(false);
49
+ });
50
+
51
+ it('should clear all entries', () => {
52
+ cache.set('key1', 'value1');
53
+ cache.set('key2', 'value2');
54
+ cache.clear();
55
+ expect(cache.size).toBe(0);
56
+ expect(cache.get('key1')).toBeNull();
57
+ });
58
+
59
+ it('should return all keys', () => {
60
+ cache.set('key1', 'value1');
61
+ cache.set('key2', 'value2');
62
+ const keys = cache.keys();
63
+ expect(keys).toContain('key1');
64
+ expect(keys).toContain('key2');
65
+ });
66
+
67
+ it('should update existing entries', () => {
68
+ cache.set('key1', 'value1');
69
+ cache.set('key1', 'value2');
70
+ expect(cache.get('key1')).toBe('value2');
71
+ expect(cache.size).toBe(1);
72
+ });
73
+ });
74
+
75
+ describe('LRU eviction', () => {
76
+ it('should evict least recently used when at capacity', () => {
77
+ const smallCache = new CacheManager<string>({ maxSize: 3, ttl: 60000 });
78
+
79
+ smallCache.set('key1', 'value1');
80
+ smallCache.set('key2', 'value2');
81
+ smallCache.set('key3', 'value3');
82
+
83
+ // Access key1 to make it recently used
84
+ smallCache.get('key1');
85
+
86
+ // Add new entry, should evict key2 (LRU)
87
+ smallCache.set('key4', 'value4');
88
+
89
+ expect(smallCache.get('key1')).toBe('value1');
90
+ expect(smallCache.get('key2')).toBeNull(); // Evicted
91
+ expect(smallCache.get('key3')).toBe('value3');
92
+ expect(smallCache.get('key4')).toBe('value4');
93
+
94
+ smallCache.shutdown();
95
+ });
96
+
97
+ it('should move accessed items to front of LRU', () => {
98
+ const smallCache = new CacheManager<string>({ maxSize: 3, ttl: 60000 });
99
+
100
+ smallCache.set('key1', 'value1');
101
+ smallCache.set('key2', 'value2');
102
+ smallCache.set('key3', 'value3');
103
+
104
+ // Access key1 multiple times
105
+ smallCache.get('key1');
106
+ smallCache.get('key1');
107
+
108
+ // Add two new entries
109
+ smallCache.set('key4', 'value4');
110
+ smallCache.set('key5', 'value5');
111
+
112
+ // key1 should still exist, key2 and key3 should be evicted
113
+ expect(smallCache.get('key1')).toBe('value1');
114
+ expect(smallCache.has('key2')).toBe(false);
115
+ expect(smallCache.has('key3')).toBe(false);
116
+
117
+ smallCache.shutdown();
118
+ });
119
+ });
120
+
121
+ describe('TTL expiration', () => {
122
+ it('should expire entries after TTL', async () => {
123
+ const shortTtlCache = new CacheManager<string>({
124
+ maxSize: 100,
125
+ ttl: 50, // 50ms TTL
126
+ });
127
+
128
+ shortTtlCache.set('key1', 'value1');
129
+ expect(shortTtlCache.get('key1')).toBe('value1');
130
+
131
+ // Wait for expiration
132
+ await new Promise((resolve) => setTimeout(resolve, 100));
133
+
134
+ expect(shortTtlCache.get('key1')).toBeNull();
135
+
136
+ shortTtlCache.shutdown();
137
+ });
138
+
139
+ it('should respect custom TTL per entry', async () => {
140
+ cache.set('short', 'value1', 50); // 50ms TTL
141
+ cache.set('long', 'value2', 5000); // 5s TTL
142
+
143
+ await new Promise((resolve) => setTimeout(resolve, 100));
144
+
145
+ expect(cache.get('short')).toBeNull(); // Expired
146
+ expect(cache.get('long')).toBe('value2'); // Still valid
147
+ });
148
+
149
+ it('should not return expired entries via has()', async () => {
150
+ const shortTtlCache = new CacheManager<string>({
151
+ maxSize: 100,
152
+ ttl: 50,
153
+ });
154
+
155
+ shortTtlCache.set('key1', 'value1');
156
+ expect(shortTtlCache.has('key1')).toBe(true);
157
+
158
+ await new Promise((resolve) => setTimeout(resolve, 100));
159
+
160
+ expect(shortTtlCache.has('key1')).toBe(false);
161
+
162
+ shortTtlCache.shutdown();
163
+ });
164
+ });
165
+
166
+ describe('memory pressure', () => {
167
+ it('should evict when memory limit exceeded', () => {
168
+ const memoryCache = new CacheManager<string>({
169
+ maxSize: 1000,
170
+ maxMemory: 100, // Very small memory limit
171
+ ttl: 60000,
172
+ });
173
+
174
+ // Add entries until memory is exceeded
175
+ for (let i = 0; i < 10; i++) {
176
+ memoryCache.set(`key${i}`, 'a'.repeat(20)); // Each ~40 bytes
177
+ }
178
+
179
+ // Should have evicted some entries
180
+ expect(memoryCache.size).toBeLessThan(10);
181
+
182
+ memoryCache.shutdown();
183
+ });
184
+ });
185
+
186
+ describe('statistics', () => {
187
+ it('should track hits and misses', () => {
188
+ cache.set('key1', 'value1');
189
+
190
+ cache.get('key1'); // Hit
191
+ cache.get('key1'); // Hit
192
+ cache.get('nonexistent'); // Miss
193
+
194
+ const stats = cache.getStats();
195
+ expect(stats.hits).toBe(2);
196
+ expect(stats.misses).toBe(1);
197
+ expect(stats.hitRate).toBeCloseTo(2 / 3);
198
+ });
199
+
200
+ it('should track evictions', () => {
201
+ const smallCache = new CacheManager<string>({ maxSize: 2, ttl: 60000 });
202
+
203
+ smallCache.set('key1', 'value1');
204
+ smallCache.set('key2', 'value2');
205
+ smallCache.set('key3', 'value3'); // Evicts key1
206
+
207
+ const stats = smallCache.getStats();
208
+ expect(stats.evictions).toBe(1);
209
+
210
+ smallCache.shutdown();
211
+ });
212
+
213
+ it('should track size and memory usage', () => {
214
+ cache.set('key1', 'value1');
215
+ cache.set('key2', 'value2');
216
+
217
+ const stats = cache.getStats();
218
+ expect(stats.size).toBe(2);
219
+ expect(stats.memoryUsage).toBeGreaterThan(0);
220
+ });
221
+ });
222
+
223
+ describe('getOrSet', () => {
224
+ it('should return cached value if exists', async () => {
225
+ cache.set('key1', 'cached');
226
+
227
+ const loader = vi.fn().mockResolvedValue('loaded');
228
+ const result = await cache.getOrSet('key1', loader);
229
+
230
+ expect(result).toBe('cached');
231
+ expect(loader).not.toHaveBeenCalled();
232
+ });
233
+
234
+ it('should load and cache value if not exists', async () => {
235
+ const loader = vi.fn().mockResolvedValue('loaded');
236
+ const result = await cache.getOrSet('key1', loader);
237
+
238
+ expect(result).toBe('loaded');
239
+ expect(loader).toHaveBeenCalledTimes(1);
240
+ expect(cache.get('key1')).toBe('loaded');
241
+ });
242
+ });
243
+
244
+ describe('prefetch', () => {
245
+ it('should load missing keys in batch', async () => {
246
+ cache.set('key1', 'existing');
247
+
248
+ const loader = vi.fn().mockResolvedValue(
249
+ new Map([
250
+ ['key2', 'loaded2'],
251
+ ['key3', 'loaded3'],
252
+ ])
253
+ );
254
+
255
+ await cache.prefetch(['key1', 'key2', 'key3'], loader);
256
+
257
+ // Loader should only be called for missing keys
258
+ expect(loader).toHaveBeenCalledWith(['key2', 'key3']);
259
+ expect(cache.get('key1')).toBe('existing');
260
+ expect(cache.get('key2')).toBe('loaded2');
261
+ expect(cache.get('key3')).toBe('loaded3');
262
+ });
263
+
264
+ it('should not call loader if all keys exist', async () => {
265
+ cache.set('key1', 'value1');
266
+ cache.set('key2', 'value2');
267
+
268
+ const loader = vi.fn();
269
+ await cache.prefetch(['key1', 'key2'], loader);
270
+
271
+ expect(loader).not.toHaveBeenCalled();
272
+ });
273
+ });
274
+
275
+ describe('warmUp', () => {
276
+ it('should populate cache with initial data', () => {
277
+ cache.warmUp([
278
+ { key: 'key1', data: 'value1' },
279
+ { key: 'key2', data: 'value2' },
280
+ { key: 'key3', data: 'value3', ttl: 1000 },
281
+ ]);
282
+
283
+ expect(cache.get('key1')).toBe('value1');
284
+ expect(cache.get('key2')).toBe('value2');
285
+ expect(cache.get('key3')).toBe('value3');
286
+ expect(cache.size).toBe(3);
287
+ });
288
+ });
289
+
290
+ describe('invalidatePattern', () => {
291
+ it('should invalidate keys matching string pattern', () => {
292
+ cache.set('user:1', 'value1');
293
+ cache.set('user:2', 'value2');
294
+ cache.set('post:1', 'value3');
295
+
296
+ const count = cache.invalidatePattern('user:');
297
+
298
+ expect(count).toBe(2);
299
+ expect(cache.has('user:1')).toBe(false);
300
+ expect(cache.has('user:2')).toBe(false);
301
+ expect(cache.has('post:1')).toBe(true);
302
+ });
303
+
304
+ it('should invalidate keys matching regex pattern', () => {
305
+ cache.set('cache:session:abc', 'value1');
306
+ cache.set('cache:session:def', 'value2');
307
+ cache.set('cache:data:xyz', 'value3');
308
+
309
+ const count = cache.invalidatePattern(/cache:session:/);
310
+
311
+ expect(count).toBe(2);
312
+ expect(cache.has('cache:session:abc')).toBe(false);
313
+ expect(cache.has('cache:data:xyz')).toBe(true);
314
+ });
315
+ });
316
+
317
+ describe('events', () => {
318
+ it('should emit cache:hit event', () => {
319
+ const handler = vi.fn();
320
+ cache.on('cache:hit', handler);
321
+
322
+ cache.set('key1', 'value1');
323
+ cache.get('key1');
324
+
325
+ expect(handler).toHaveBeenCalledWith({ key: 'key1' });
326
+ });
327
+
328
+ it('should emit cache:miss event', () => {
329
+ const handler = vi.fn();
330
+ cache.on('cache:miss', handler);
331
+
332
+ cache.get('nonexistent');
333
+
334
+ expect(handler).toHaveBeenCalledWith({ key: 'nonexistent' });
335
+ });
336
+
337
+ it('should emit cache:set event', () => {
338
+ const handler = vi.fn();
339
+ cache.on('cache:set', handler);
340
+
341
+ cache.set('key1', 'value1');
342
+
343
+ expect(handler).toHaveBeenCalledWith({ key: 'key1', ttl: 60000 });
344
+ });
345
+
346
+ it('should emit cache:delete event', () => {
347
+ const handler = vi.fn();
348
+ cache.on('cache:delete', handler);
349
+
350
+ cache.set('key1', 'value1');
351
+ cache.delete('key1');
352
+
353
+ expect(handler).toHaveBeenCalledWith({ key: 'key1' });
354
+ });
355
+ });
356
+
357
+ describe('shutdown', () => {
358
+ it('should clear cache and stop cleanup timer', () => {
359
+ cache.set('key1', 'value1');
360
+ cache.shutdown();
361
+
362
+ expect(cache.size).toBe(0);
363
+ });
364
+ });
365
+ });
366
+
367
+ describe('TieredCacheManager', () => {
368
+ let tieredCache: TieredCacheManager<string>;
369
+ let l2Store: Map<string, string>;
370
+
371
+ beforeEach(() => {
372
+ l2Store = new Map();
373
+
374
+ tieredCache = new TieredCacheManager<string>(
375
+ { maxSize: 10, ttl: 60000 },
376
+ {
377
+ loader: async (key) => l2Store.get(key) ?? null,
378
+ writer: async (key, value) => {
379
+ l2Store.set(key, value);
380
+ },
381
+ }
382
+ );
383
+ });
384
+
385
+ afterEach(() => {
386
+ tieredCache.shutdown();
387
+ });
388
+
389
+ describe('tiered caching', () => {
390
+ it('should store in L1 cache and write through to L2', async () => {
391
+ await tieredCache.set('key1', 'value1');
392
+ const result = await tieredCache.get('key1');
393
+ expect(result).toBe('value1');
394
+ expect(l2Store.get('key1')).toBe('value1');
395
+ });
396
+
397
+ it('should fall back to L2 when L1 misses', async () => {
398
+ // Directly set in L2 (simulating data loaded from storage)
399
+ l2Store.set('key1', 'value1');
400
+
401
+ // Should find in L2
402
+ const result = await tieredCache.get('key1');
403
+ expect(result).toBe('value1');
404
+ });
405
+
406
+ it('should promote L2 hits to L1', async () => {
407
+ // Set in L2 only
408
+ l2Store.set('key1', 'value1');
409
+
410
+ // First access loads from L2
411
+ await tieredCache.get('key1');
412
+
413
+ // Now should be in L1 (faster subsequent access)
414
+ const stats = tieredCache.getStats();
415
+ expect(stats.size).toBe(1);
416
+ });
417
+
418
+ it('should delete from L1', async () => {
419
+ await tieredCache.set('key1', 'value1');
420
+ expect(tieredCache.delete('key1')).toBe(true);
421
+
422
+ // Should miss in L1, but L2 still has it
423
+ expect(l2Store.has('key1')).toBe(true);
424
+ });
425
+
426
+ it('should clear L1 cache', async () => {
427
+ await tieredCache.set('key1', 'value1');
428
+ await tieredCache.set('key2', 'value2');
429
+
430
+ tieredCache.clear();
431
+
432
+ const stats = tieredCache.getStats();
433
+ expect(stats.size).toBe(0);
434
+ });
435
+ });
436
+
437
+ describe('without L2', () => {
438
+ it('should work with L1 only', async () => {
439
+ const l1Only = new TieredCacheManager<string>({ maxSize: 10, ttl: 60000 });
440
+
441
+ await l1Only.set('key1', 'value1');
442
+ const result = await l1Only.get('key1');
443
+ expect(result).toBe('value1');
444
+
445
+ l1Only.shutdown();
446
+ });
447
+
448
+ it('should return null for missing keys without L2', async () => {
449
+ const l1Only = new TieredCacheManager<string>({ maxSize: 10, ttl: 60000 });
450
+
451
+ const result = await l1Only.get('nonexistent');
452
+ expect(result).toBeNull();
453
+
454
+ l1Only.shutdown();
455
+ });
456
+ });
457
+
458
+ describe('statistics', () => {
459
+ it('should return L1 statistics', async () => {
460
+ await tieredCache.set('key1', 'value1');
461
+ await tieredCache.get('key1');
462
+
463
+ const stats = tieredCache.getStats();
464
+ expect(stats.hits).toBeGreaterThanOrEqual(1);
465
+ expect(stats.size).toBe(1);
466
+ });
467
+ });
468
+
469
+ describe('events', () => {
470
+ it('should emit l1:hit event', async () => {
471
+ const handler = vi.fn();
472
+ tieredCache.on('l1:hit', handler);
473
+
474
+ await tieredCache.set('key1', 'value1');
475
+ await tieredCache.get('key1');
476
+
477
+ expect(handler).toHaveBeenCalled();
478
+ });
479
+
480
+ it('should emit l2:hit event', async () => {
481
+ const handler = vi.fn();
482
+ tieredCache.on('l2:hit', handler);
483
+
484
+ l2Store.set('key1', 'value1');
485
+ await tieredCache.get('key1');
486
+
487
+ expect(handler).toHaveBeenCalledWith({ key: 'key1' });
488
+ });
489
+
490
+ it('should emit l2:write event', async () => {
491
+ const handler = vi.fn();
492
+ tieredCache.on('l2:write', handler);
493
+
494
+ await tieredCache.set('key1', 'value1');
495
+
496
+ expect(handler).toHaveBeenCalledWith({ key: 'key1' });
497
+ });
498
+ });
499
+ });