@aitytech/agentkits-memory 1.0.1 → 2.0.1

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 +6 -4
  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,495 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import {
3
+ LocalEmbeddingsService,
4
+ createLocalEmbeddings,
5
+ createEmbeddingGenerator,
6
+ } from '../local-embeddings.js';
7
+
8
+ describe('LocalEmbeddingsService', () => {
9
+ let service: LocalEmbeddingsService;
10
+
11
+ describe('mock provider', () => {
12
+ beforeEach(() => {
13
+ service = new LocalEmbeddingsService({ provider: 'mock' });
14
+ });
15
+
16
+ afterEach(async () => {
17
+ await service.shutdown();
18
+ });
19
+
20
+ it('should create service with default config', () => {
21
+ expect(service).toBeDefined();
22
+ expect(service.getDimensions()).toBe(384);
23
+ });
24
+
25
+ it('should generate embedding for text', async () => {
26
+ const result = await service.embed('Hello world');
27
+
28
+ expect(result.embedding).toBeInstanceOf(Float32Array);
29
+ expect(result.embedding.length).toBe(384);
30
+ expect(result.cached).toBe(false);
31
+ expect(result.timeMs).toBeGreaterThanOrEqual(0);
32
+ });
33
+
34
+ it('should return normalized embeddings', async () => {
35
+ const result = await service.embed('Test content');
36
+
37
+ // Calculate L2 norm
38
+ let norm = 0;
39
+ for (let i = 0; i < result.embedding.length; i++) {
40
+ norm += result.embedding[i] * result.embedding[i];
41
+ }
42
+ norm = Math.sqrt(norm);
43
+
44
+ // Should be normalized (L2 norm ≈ 1)
45
+ expect(norm).toBeCloseTo(1, 5);
46
+ });
47
+
48
+ it('should cache embeddings', async () => {
49
+ const text = 'Cache test';
50
+
51
+ const first = await service.embed(text);
52
+ expect(first.cached).toBe(false);
53
+
54
+ const second = await service.embed(text);
55
+ expect(second.cached).toBe(true);
56
+
57
+ // Same embedding
58
+ expect(Array.from(first.embedding)).toEqual(Array.from(second.embedding));
59
+ });
60
+
61
+ it('should generate deterministic embeddings', async () => {
62
+ const service2 = new LocalEmbeddingsService({
63
+ provider: 'mock',
64
+ cacheEnabled: false,
65
+ });
66
+
67
+ const text = 'Deterministic test';
68
+ const result1 = await service.embed(text);
69
+
70
+ // Clear cache by using new service
71
+ service.clearCache();
72
+
73
+ const result2 = await service2.embed(text);
74
+
75
+ // Same text should produce same embedding
76
+ expect(Array.from(result1.embedding)).toEqual(Array.from(result2.embedding));
77
+
78
+ await service2.shutdown();
79
+ });
80
+
81
+ it('should handle batch embeddings', async () => {
82
+ const texts = ['First', 'Second', 'Third'];
83
+ const results = await service.embedBatch(texts);
84
+
85
+ expect(results.length).toBe(3);
86
+ results.forEach((result, i) => {
87
+ expect(result.embedding).toBeInstanceOf(Float32Array);
88
+ expect(result.embedding.length).toBe(384);
89
+ });
90
+ });
91
+
92
+ it('should track statistics', async () => {
93
+ await service.embed('Text 1');
94
+ await service.embed('Text 2');
95
+ await service.embed('Text 1'); // Cache hit
96
+
97
+ const stats = service.getStats();
98
+
99
+ expect(stats.totalEmbeddings).toBe(2); // Only 2 unique
100
+ expect(stats.cacheHits).toBe(1);
101
+ expect(stats.cacheMisses).toBe(2);
102
+ expect(stats.modelLoaded).toBe(true);
103
+ expect(stats.provider).toBe('mock');
104
+ });
105
+
106
+ it('should clear cache', async () => {
107
+ await service.embed('Cache clear test');
108
+ service.clearCache();
109
+
110
+ const result = await service.embed('Cache clear test');
111
+ expect(result.cached).toBe(false);
112
+ });
113
+
114
+ it('should return embedding generator function', async () => {
115
+ const generator = service.getGenerator();
116
+
117
+ const embedding = await generator('Test content');
118
+
119
+ expect(embedding).toBeInstanceOf(Float32Array);
120
+ expect(embedding.length).toBe(384);
121
+ });
122
+ });
123
+
124
+ describe('createLocalEmbeddings factory', () => {
125
+ it('should create service with default config', () => {
126
+ const service = createLocalEmbeddings();
127
+ expect(service).toBeInstanceOf(LocalEmbeddingsService);
128
+ expect(service.getDimensions()).toBe(384);
129
+ });
130
+
131
+ it('should accept custom config', () => {
132
+ const service = createLocalEmbeddings({
133
+ dimensions: 768,
134
+ provider: 'mock',
135
+ maxCacheSize: 500,
136
+ });
137
+
138
+ expect(service.getDimensions()).toBe(768);
139
+ });
140
+ });
141
+
142
+ describe('createEmbeddingGenerator factory', () => {
143
+ it('should create embedding generator function', async () => {
144
+ const generator = await createEmbeddingGenerator({ provider: 'mock' });
145
+
146
+ const embedding = await generator('Hello world');
147
+
148
+ expect(embedding).toBeInstanceOf(Float32Array);
149
+ expect(embedding.length).toBe(384);
150
+ });
151
+ });
152
+
153
+ describe('cache disabled', () => {
154
+ beforeEach(() => {
155
+ service = new LocalEmbeddingsService({
156
+ provider: 'mock',
157
+ cacheEnabled: false,
158
+ });
159
+ });
160
+
161
+ afterEach(async () => {
162
+ await service.shutdown();
163
+ });
164
+
165
+ it('should not cache when disabled', async () => {
166
+ const text = 'No cache test';
167
+
168
+ const first = await service.embed(text);
169
+ expect(first.cached).toBe(false);
170
+
171
+ const second = await service.embed(text);
172
+ expect(second.cached).toBe(false);
173
+ });
174
+
175
+ it('should still generate valid embeddings', async () => {
176
+ const result = await service.embed('Test');
177
+
178
+ expect(result.embedding).toBeInstanceOf(Float32Array);
179
+ expect(result.embedding.length).toBe(384);
180
+ });
181
+ });
182
+
183
+ describe('custom dimensions', () => {
184
+ it('should support 768 dimensions', async () => {
185
+ service = new LocalEmbeddingsService({
186
+ provider: 'mock',
187
+ dimensions: 768,
188
+ });
189
+
190
+ const result = await service.embed('Test');
191
+
192
+ expect(result.embedding.length).toBe(768);
193
+
194
+ await service.shutdown();
195
+ });
196
+
197
+ it('should support 1024 dimensions', async () => {
198
+ service = new LocalEmbeddingsService({
199
+ provider: 'mock',
200
+ dimensions: 1024,
201
+ });
202
+
203
+ const result = await service.embed('Test');
204
+
205
+ expect(result.embedding.length).toBe(1024);
206
+
207
+ await service.shutdown();
208
+ });
209
+ });
210
+ });
211
+
212
+ describe('Embedding Similarity', () => {
213
+ let service: LocalEmbeddingsService;
214
+
215
+ beforeEach(() => {
216
+ service = new LocalEmbeddingsService({ provider: 'mock' });
217
+ });
218
+
219
+ afterEach(async () => {
220
+ await service.shutdown();
221
+ });
222
+
223
+ it('should produce different embeddings for different texts', async () => {
224
+ const result1 = await service.embed('Hello world');
225
+ const result2 = await service.embed('Goodbye universe');
226
+
227
+ // At least some values should differ
228
+ let diffCount = 0;
229
+ for (let i = 0; i < result1.embedding.length; i++) {
230
+ if (result1.embedding[i] !== result2.embedding[i]) {
231
+ diffCount++;
232
+ }
233
+ }
234
+
235
+ expect(diffCount).toBeGreaterThan(0);
236
+ });
237
+
238
+ it('should produce same embedding for same text', async () => {
239
+ service.clearCache();
240
+
241
+ const result1 = await service.embed('Identical text');
242
+ service.clearCache();
243
+ const result2 = await service.embed('Identical text');
244
+
245
+ // All values should be the same
246
+ for (let i = 0; i < result1.embedding.length; i++) {
247
+ expect(result1.embedding[i]).toBe(result2.embedding[i]);
248
+ }
249
+ });
250
+ });
251
+
252
+ describe('LocalEmbeddingsService advanced', () => {
253
+ describe('cache eviction', () => {
254
+ it('should evict oldest entries when cache is full', async () => {
255
+ // Create service with tiny cache
256
+ const service = new LocalEmbeddingsService({
257
+ provider: 'mock',
258
+ maxCacheSize: 3,
259
+ cacheEnabled: true,
260
+ });
261
+
262
+ // Fill the cache
263
+ await service.embed('Text 1');
264
+ await service.embed('Text 2');
265
+ await service.embed('Text 3');
266
+
267
+ // This should evict 'Text 1'
268
+ await service.embed('Text 4');
269
+
270
+ // 'Text 1' should no longer be cached
271
+ const result1 = await service.embed('Text 1');
272
+ expect(result1.cached).toBe(false);
273
+
274
+ // 'Text 4' should be cached
275
+ const result4 = await service.embed('Text 4');
276
+ expect(result4.cached).toBe(true);
277
+
278
+ await service.shutdown();
279
+ });
280
+
281
+ it('should update existing cache entries without evicting', async () => {
282
+ const service = new LocalEmbeddingsService({
283
+ provider: 'mock',
284
+ maxCacheSize: 3,
285
+ cacheEnabled: true,
286
+ });
287
+
288
+ await service.embed('Text 1');
289
+ await service.embed('Text 2');
290
+ await service.embed('Text 3');
291
+
292
+ // Access existing entry (should update, not evict)
293
+ const result = await service.embed('Text 2');
294
+ expect(result.cached).toBe(true);
295
+
296
+ await service.shutdown();
297
+ });
298
+
299
+ it('should move accessed entries to end of LRU', async () => {
300
+ const service = new LocalEmbeddingsService({
301
+ provider: 'mock',
302
+ maxCacheSize: 3,
303
+ cacheEnabled: true,
304
+ });
305
+
306
+ await service.embed('Text 1');
307
+ await service.embed('Text 2');
308
+ await service.embed('Text 3');
309
+
310
+ // Access 'Text 1' to move it to end
311
+ await service.embed('Text 1');
312
+
313
+ // Add new entry - should evict 'Text 2' (oldest after Text 1 access)
314
+ await service.embed('Text 4');
315
+
316
+ // 'Text 1' should still be cached
317
+ const result1 = await service.embed('Text 1');
318
+ expect(result1.cached).toBe(true);
319
+
320
+ // 'Text 2' should be evicted
321
+ const result2 = await service.embed('Text 2');
322
+ expect(result2.cached).toBe(false);
323
+
324
+ await service.shutdown();
325
+ });
326
+ });
327
+
328
+ describe('initialization', () => {
329
+ it('should handle double initialization', async () => {
330
+ const service = new LocalEmbeddingsService({ provider: 'mock' });
331
+
332
+ await service.initialize();
333
+ await service.initialize(); // Should not throw
334
+
335
+ await service.shutdown();
336
+ });
337
+
338
+ it('should handle concurrent initialization', async () => {
339
+ const service = new LocalEmbeddingsService({ provider: 'mock' });
340
+
341
+ // Start multiple initializations concurrently
342
+ const [result1, result2] = await Promise.all([
343
+ service.initialize(),
344
+ service.initialize(),
345
+ ]);
346
+
347
+ // Both should complete without error
348
+ expect(result1).toBeUndefined();
349
+ expect(result2).toBeUndefined();
350
+
351
+ await service.shutdown();
352
+ });
353
+
354
+ it('should initialize when using transformers provider without transformers installed', async () => {
355
+ // This tests the fallback to mock when transformers.js is not available
356
+ const service = new LocalEmbeddingsService({ provider: 'transformers' });
357
+
358
+ // Capture console.warn
359
+ const warnings: string[] = [];
360
+ const originalWarn = console.warn;
361
+ console.warn = (...args) => warnings.push(args.join(' '));
362
+
363
+ // The service will try to load transformers and fall back to mock
364
+ await service.initialize();
365
+
366
+ // Should have generated a warning about transformers not being available
367
+ // (only if transformers.js is not installed)
368
+ // If transformers IS installed, it will load successfully
369
+
370
+ const result = await service.embed('Test');
371
+ expect(result.embedding.length).toBeGreaterThan(0);
372
+
373
+ console.warn = originalWarn;
374
+ await service.shutdown();
375
+ });
376
+ });
377
+
378
+ describe('stats tracking', () => {
379
+ it('should track average time correctly', async () => {
380
+ const service = new LocalEmbeddingsService({ provider: 'mock' });
381
+
382
+ await service.embed('Text 1');
383
+ await service.embed('Text 2');
384
+
385
+ const stats = service.getStats();
386
+ expect(stats.avgTimeMs).toBeGreaterThanOrEqual(0);
387
+ expect(stats.totalTimeMs).toBeGreaterThanOrEqual(0);
388
+
389
+ await service.shutdown();
390
+ });
391
+
392
+ it('should report zero average time when no embeddings', () => {
393
+ const service = new LocalEmbeddingsService({ provider: 'mock' });
394
+
395
+ const stats = service.getStats();
396
+ expect(stats.avgTimeMs).toBe(0);
397
+ expect(stats.totalEmbeddings).toBe(0);
398
+ });
399
+ });
400
+
401
+ describe('shutdown', () => {
402
+ it('should clear state on shutdown', async () => {
403
+ const service = new LocalEmbeddingsService({ provider: 'mock' });
404
+
405
+ await service.embed('Test');
406
+ await service.shutdown();
407
+
408
+ // After shutdown, should start fresh
409
+ const result = await service.embed('Test');
410
+ expect(result.cached).toBe(false);
411
+ });
412
+ });
413
+
414
+ describe('configuration', () => {
415
+ it('should use custom model ID', () => {
416
+ const service = new LocalEmbeddingsService({
417
+ provider: 'mock',
418
+ modelId: 'custom/model',
419
+ });
420
+
421
+ // Model ID is stored in config (accessed via getStats)
422
+ const stats = service.getStats();
423
+ expect(stats.provider).toBe('mock');
424
+ });
425
+
426
+ it('should use custom cache directory', () => {
427
+ const service = new LocalEmbeddingsService({
428
+ provider: 'mock',
429
+ cacheDir: '/custom/cache',
430
+ });
431
+
432
+ expect(service.getDimensions()).toBe(384);
433
+ });
434
+
435
+ it('should handle showProgress option', async () => {
436
+ const service = new LocalEmbeddingsService({
437
+ provider: 'mock',
438
+ showProgress: true,
439
+ });
440
+
441
+ await service.initialize();
442
+ // Mock provider doesn't actually show progress, but config is accepted
443
+
444
+ await service.shutdown();
445
+ });
446
+ });
447
+
448
+ describe('edge cases', () => {
449
+ it('should handle empty string', async () => {
450
+ const service = new LocalEmbeddingsService({ provider: 'mock' });
451
+
452
+ const result = await service.embed('');
453
+
454
+ expect(result.embedding).toBeInstanceOf(Float32Array);
455
+ expect(result.embedding.length).toBe(384);
456
+
457
+ await service.shutdown();
458
+ });
459
+
460
+ it('should handle very long text', async () => {
461
+ const service = new LocalEmbeddingsService({ provider: 'mock' });
462
+
463
+ const longText = 'a'.repeat(10000);
464
+ const result = await service.embed(longText);
465
+
466
+ expect(result.embedding).toBeInstanceOf(Float32Array);
467
+ expect(result.embedding.length).toBe(384);
468
+
469
+ await service.shutdown();
470
+ });
471
+
472
+ it('should handle unicode text', async () => {
473
+ const service = new LocalEmbeddingsService({ provider: 'mock' });
474
+
475
+ const unicodeText = '日本語テスト 中文测试 한국어 테스트 🎉';
476
+ const result = await service.embed(unicodeText);
477
+
478
+ expect(result.embedding).toBeInstanceOf(Float32Array);
479
+ expect(result.embedding.length).toBe(384);
480
+
481
+ await service.shutdown();
482
+ });
483
+
484
+ it('should handle special characters', async () => {
485
+ const service = new LocalEmbeddingsService({ provider: 'mock' });
486
+
487
+ const specialText = '!@#$%^&*()[]{}|\\;:\'",.<>?/`~';
488
+ const result = await service.embed(specialText);
489
+
490
+ expect(result.embedding).toBeInstanceOf(Float32Array);
491
+
492
+ await service.shutdown();
493
+ });
494
+ });
495
+ });