@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
@@ -405,3 +405,435 @@ describe('DEFAULT_NAMESPACES', () => {
405
405
  expect(DEFAULT_NAMESPACES.ERRORS).toBe('errors');
406
406
  });
407
407
  });
408
+
409
+ describe('ProjectMemoryService with embeddings', () => {
410
+ let service: ProjectMemoryService;
411
+ let testDir: string;
412
+ const mockEmbeddingGenerator = vi.fn().mockImplementation(async (content: string) => {
413
+ // Simple mock embedding based on content length
414
+ const embedding = new Float32Array(8);
415
+ for (let i = 0; i < 8; i++) {
416
+ embedding[i] = (content.charCodeAt(i % content.length) / 255) - 0.5;
417
+ }
418
+ return embedding;
419
+ });
420
+
421
+ beforeEach(async () => {
422
+ testDir = path.join(tmpdir(), `embedding-test-${Date.now()}`);
423
+ fs.mkdirSync(testDir, { recursive: true });
424
+
425
+ service = new ProjectMemoryService({
426
+ baseDir: testDir,
427
+ dbFilename: 'test.db',
428
+ cacheEnabled: true,
429
+ enableVectorIndex: true,
430
+ dimensions: 8,
431
+ embeddingGenerator: mockEmbeddingGenerator,
432
+ });
433
+ await service.initialize();
434
+ });
435
+
436
+ afterEach(async () => {
437
+ await service.shutdown();
438
+ fs.rmSync(testDir, { recursive: true, force: true });
439
+ vi.clearAllMocks();
440
+ });
441
+
442
+ it('should generate embeddings when storing entries', async () => {
443
+ await service.storeEntry({
444
+ key: 'embed-test',
445
+ content: 'Test content for embedding',
446
+ namespace: 'test',
447
+ });
448
+
449
+ expect(mockEmbeddingGenerator).toHaveBeenCalledWith('Test content for embedding');
450
+ });
451
+
452
+ it('should perform semantic search', async () => {
453
+ await service.storeEntry({
454
+ key: 'auth-pattern',
455
+ content: 'Use JWT for authentication',
456
+ namespace: 'patterns',
457
+ });
458
+
459
+ await service.storeEntry({
460
+ key: 'db-pattern',
461
+ content: 'Use PostgreSQL for database',
462
+ namespace: 'patterns',
463
+ });
464
+
465
+ const results = await service.semanticSearch('authentication', 5);
466
+ expect(results.length).toBeGreaterThan(0);
467
+ });
468
+
469
+ it('should throw error for semantic search without embedding generator', async () => {
470
+ const noEmbedService = new ProjectMemoryService({
471
+ baseDir: path.join(testDir, 'no-embed'),
472
+ dbFilename: 'test.db',
473
+ });
474
+ await noEmbedService.initialize();
475
+
476
+ await expect(noEmbedService.semanticSearch('test', 5)).rejects.toThrow(
477
+ 'Embedding generator not configured'
478
+ );
479
+
480
+ await noEmbedService.shutdown();
481
+ });
482
+
483
+ it('should use HNSW index for search with vector index enabled', async () => {
484
+ await service.storeEntry({
485
+ key: 'v1',
486
+ content: 'First vector content',
487
+ namespace: 'vectors',
488
+ });
489
+
490
+ await service.storeEntry({
491
+ key: 'v2',
492
+ content: 'Second vector content',
493
+ namespace: 'vectors',
494
+ });
495
+
496
+ const embedding = await mockEmbeddingGenerator('First');
497
+ const results = await service.search(embedding, { k: 2 });
498
+
499
+ expect(results.length).toBeLessThanOrEqual(2);
500
+ });
501
+
502
+ it('should update embedding when content changes', async () => {
503
+ const entry = await service.storeEntry({
504
+ key: 'update-embed',
505
+ content: 'Original content',
506
+ namespace: 'test',
507
+ });
508
+
509
+ const callsBefore = mockEmbeddingGenerator.mock.calls.length;
510
+
511
+ await service.update(entry.id, { content: 'Updated content' });
512
+
513
+ // Should have generated new embedding for updated content
514
+ expect(mockEmbeddingGenerator.mock.calls.length).toBeGreaterThan(callsBefore);
515
+ });
516
+
517
+ it('should handle embedding generator failure gracefully', async () => {
518
+ const failingGenerator = vi.fn().mockRejectedValue(new Error('Embedding failed'));
519
+ const failService = new ProjectMemoryService({
520
+ baseDir: path.join(testDir, 'fail-embed'),
521
+ dbFilename: 'test.db',
522
+ embeddingGenerator: failingGenerator,
523
+ verbose: true,
524
+ });
525
+ await failService.initialize();
526
+
527
+ // Should not throw, just log warning
528
+ const entry = await failService.storeEntry({
529
+ key: 'fail-test',
530
+ content: 'Content that will fail embedding',
531
+ namespace: 'test',
532
+ });
533
+
534
+ expect(entry.id).toBeDefined();
535
+ await failService.shutdown();
536
+ });
537
+
538
+ it('should remove from vector index when deleting', async () => {
539
+ const entry = await service.storeEntry({
540
+ key: 'delete-vector',
541
+ content: 'Content to delete',
542
+ namespace: 'test',
543
+ });
544
+
545
+ // Verify entry exists in vector search
546
+ const embedding = await mockEmbeddingGenerator('Content to delete');
547
+ const beforeDelete = await service.search(embedding, { k: 5 });
548
+ const foundBefore = beforeDelete.some(r => r.entry.id === entry.id);
549
+ expect(foundBefore).toBe(true);
550
+
551
+ await service.delete(entry.id);
552
+
553
+ // Entry should no longer be found
554
+ const afterDelete = await service.search(embedding, { k: 5 });
555
+ const foundAfter = afterDelete.some(r => r.entry.id === entry.id);
556
+ expect(foundAfter).toBe(false);
557
+ });
558
+
559
+ it('should apply threshold in search results', async () => {
560
+ await service.storeEntry({
561
+ key: 'similar',
562
+ content: 'Very similar content',
563
+ namespace: 'test',
564
+ });
565
+
566
+ await service.storeEntry({
567
+ key: 'different',
568
+ content: 'Completely different words here',
569
+ namespace: 'test',
570
+ });
571
+
572
+ const embedding = await mockEmbeddingGenerator('Very similar');
573
+ const results = await service.search(embedding, { k: 10, threshold: 0.9 });
574
+
575
+ // High threshold should filter out dissimilar results
576
+ expect(results.length).toBeLessThanOrEqual(2);
577
+ });
578
+
579
+ it('should include HNSW stats in getStats', async () => {
580
+ await service.storeEntry({
581
+ key: 'stats-test',
582
+ content: 'Content for stats',
583
+ namespace: 'test',
584
+ });
585
+
586
+ const stats = await service.getStats();
587
+ expect(stats.hnswStats).toBeDefined();
588
+ expect(stats.hnswStats!.vectorCount).toBe(1);
589
+ });
590
+
591
+ it('should include cache stats in getStats', async () => {
592
+ await service.storeEntry({
593
+ key: 'cache-test',
594
+ content: 'Content for cache',
595
+ namespace: 'test',
596
+ });
597
+
598
+ // Trigger a cache hit
599
+ await service.get((await service.query({ type: 'hybrid', limit: 1 }))[0].id);
600
+
601
+ const stats = await service.getStats();
602
+ expect(stats.cacheStats).toBeDefined();
603
+ });
604
+ });
605
+
606
+ describe('ProjectMemoryService with cache', () => {
607
+ let service: ProjectMemoryService;
608
+ let testDir: string;
609
+
610
+ beforeEach(async () => {
611
+ testDir = path.join(tmpdir(), `cache-test-${Date.now()}`);
612
+ fs.mkdirSync(testDir, { recursive: true });
613
+
614
+ service = new ProjectMemoryService({
615
+ baseDir: testDir,
616
+ dbFilename: 'test.db',
617
+ cacheEnabled: true,
618
+ cacheSize: 100,
619
+ cacheTtl: 60000,
620
+ });
621
+ await service.initialize();
622
+ });
623
+
624
+ afterEach(async () => {
625
+ await service.shutdown();
626
+ fs.rmSync(testDir, { recursive: true, force: true });
627
+ });
628
+
629
+ it('should cache entries on get', async () => {
630
+ const entry = await service.storeEntry({
631
+ key: 'cache-hit',
632
+ content: 'Cached content',
633
+ namespace: 'test',
634
+ });
635
+
636
+ // First get populates cache
637
+ const first = await service.get(entry.id);
638
+ expect(first).not.toBeNull();
639
+
640
+ // Second get should hit cache
641
+ const second = await service.get(entry.id);
642
+ expect(second).not.toBeNull();
643
+ expect(second!.id).toBe(first!.id);
644
+ });
645
+
646
+ it('should cache entries on getByKey', async () => {
647
+ await service.storeEntry({
648
+ key: 'cache-key',
649
+ content: 'Cached by key',
650
+ namespace: 'ns',
651
+ });
652
+
653
+ // First get populates cache
654
+ const first = await service.getByKey('ns', 'cache-key');
655
+ expect(first).not.toBeNull();
656
+
657
+ // Second get should hit cache
658
+ const second = await service.getByKey('ns', 'cache-key');
659
+ expect(second).not.toBeNull();
660
+ });
661
+
662
+ it('should update cache on update', async () => {
663
+ const entry = await service.storeEntry({
664
+ key: 'update-cache',
665
+ content: 'Original',
666
+ namespace: 'test',
667
+ });
668
+
669
+ await service.update(entry.id, { content: 'Updated' });
670
+
671
+ const retrieved = await service.get(entry.id);
672
+ expect(retrieved!.content).toBe('Updated');
673
+ });
674
+
675
+ it('should invalidate cache on delete', async () => {
676
+ const entry = await service.storeEntry({
677
+ key: 'delete-cache',
678
+ content: 'To delete',
679
+ namespace: 'test',
680
+ });
681
+
682
+ // Populate cache
683
+ await service.get(entry.id);
684
+
685
+ // Delete
686
+ await service.delete(entry.id);
687
+
688
+ // Should not find in cache or backend
689
+ const retrieved = await service.get(entry.id);
690
+ expect(retrieved).toBeNull();
691
+ });
692
+
693
+ it('should invalidate cache pattern on clearNamespace', async () => {
694
+ await service.storeEntry({ key: 'c1', content: 'C1', namespace: 'clear-ns' });
695
+ await service.storeEntry({ key: 'c2', content: 'C2', namespace: 'clear-ns' });
696
+ await service.storeEntry({ key: 'k1', content: 'K1', namespace: 'keep-ns' });
697
+
698
+ await service.clearNamespace('clear-ns');
699
+
700
+ expect(await service.count('clear-ns')).toBe(0);
701
+ expect(await service.count('keep-ns')).toBe(1);
702
+ });
703
+ });
704
+
705
+ describe('ProjectMemoryService error handling', () => {
706
+ it('should throw error when not initialized', async () => {
707
+ const service = new ProjectMemoryService({
708
+ baseDir: path.join(tmpdir(), 'not-init-test'),
709
+ });
710
+
711
+ await expect(service.get('some-id')).rejects.toThrow('not initialized');
712
+ await expect(service.query({ type: 'hybrid', limit: 10 })).rejects.toThrow('not initialized');
713
+ });
714
+
715
+ it('should handle shutdown with active session', async () => {
716
+ const testDir = path.join(tmpdir(), `session-shutdown-${Date.now()}`);
717
+ fs.mkdirSync(testDir, { recursive: true });
718
+
719
+ const service = new ProjectMemoryService({ baseDir: testDir });
720
+ await service.initialize();
721
+
722
+ await service.startSession();
723
+
724
+ // Shutdown should end the session automatically
725
+ await service.shutdown();
726
+
727
+ // Verify session was ended
728
+ expect(service.getCurrentSession()).toBeNull();
729
+
730
+ fs.rmSync(testDir, { recursive: true, force: true });
731
+ });
732
+
733
+ it('should handle double initialization', async () => {
734
+ const testDir = path.join(tmpdir(), `double-init-${Date.now()}`);
735
+ fs.mkdirSync(testDir, { recursive: true });
736
+
737
+ const service = new ProjectMemoryService({ baseDir: testDir });
738
+ await service.initialize();
739
+ await service.initialize(); // Should not throw
740
+
741
+ await service.shutdown();
742
+ fs.rmSync(testDir, { recursive: true, force: true });
743
+ });
744
+
745
+ it('should handle double shutdown', async () => {
746
+ const testDir = path.join(tmpdir(), `double-shutdown-${Date.now()}`);
747
+ fs.mkdirSync(testDir, { recursive: true });
748
+
749
+ const service = new ProjectMemoryService({ baseDir: testDir });
750
+ await service.initialize();
751
+ await service.shutdown();
752
+ await service.shutdown(); // Should not throw
753
+
754
+ fs.rmSync(testDir, { recursive: true, force: true });
755
+ });
756
+
757
+ it('should return null when ending non-existent session', async () => {
758
+ const testDir = path.join(tmpdir(), `no-session-${Date.now()}`);
759
+ fs.mkdirSync(testDir, { recursive: true });
760
+
761
+ const service = new ProjectMemoryService({ baseDir: testDir });
762
+ await service.initialize();
763
+
764
+ const result = await service.endSession('summary');
765
+ expect(result).toBeNull();
766
+
767
+ await service.shutdown();
768
+ fs.rmSync(testDir, { recursive: true, force: true });
769
+ });
770
+
771
+ it('should handle delete of non-existent entry', async () => {
772
+ const testDir = path.join(tmpdir(), `delete-none-${Date.now()}`);
773
+ fs.mkdirSync(testDir, { recursive: true });
774
+
775
+ const service = new ProjectMemoryService({ baseDir: testDir });
776
+ await service.initialize();
777
+
778
+ const result = await service.delete('non-existent-id');
779
+ expect(result).toBe(false);
780
+
781
+ await service.shutdown();
782
+ fs.rmSync(testDir, { recursive: true, force: true });
783
+ });
784
+
785
+ it('should handle getRecentSessions with invalid JSON', async () => {
786
+ const testDir = path.join(tmpdir(), `invalid-session-${Date.now()}`);
787
+ fs.mkdirSync(testDir, { recursive: true });
788
+
789
+ const service = new ProjectMemoryService({ baseDir: testDir });
790
+ await service.initialize();
791
+
792
+ // Store invalid session data
793
+ await service.storeEntry({
794
+ key: 'session:invalid',
795
+ content: 'not valid json {{{',
796
+ namespace: DEFAULT_NAMESPACES.SESSION,
797
+ tags: ['session'],
798
+ });
799
+
800
+ // Should filter out invalid entries
801
+ const sessions = await service.getRecentSessions();
802
+ expect(sessions.every(s => s.id !== undefined)).toBe(true);
803
+
804
+ await service.shutdown();
805
+ fs.rmSync(testDir, { recursive: true, force: true });
806
+ });
807
+ });
808
+
809
+ describe('createEmbeddingMemory factory', () => {
810
+ let testDir: string;
811
+
812
+ beforeEach(() => {
813
+ testDir = path.join(tmpdir(), `embed-factory-${Date.now()}`);
814
+ });
815
+
816
+ afterEach(() => {
817
+ fs.rmSync(testDir, { recursive: true, force: true });
818
+ });
819
+
820
+ it('should create memory service with embedding support', async () => {
821
+ const { createEmbeddingMemory } = await import('../index.js');
822
+
823
+ const mockGenerator = vi.fn().mockResolvedValue(new Float32Array(128));
824
+
825
+ const service = createEmbeddingMemory(testDir, mockGenerator, 128);
826
+ await service.initialize();
827
+
828
+ // Store an entry to trigger embedding generation
829
+ await service.storeEntry({
830
+ key: 'embed-test',
831
+ content: 'Test content',
832
+ namespace: 'test',
833
+ });
834
+
835
+ expect(mockGenerator).toHaveBeenCalled();
836
+
837
+ await service.shutdown();
838
+ });
839
+ });