adaptive-memory-multi-model-router 2.13.18 → 2.13.22

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 (91) hide show
  1. package/.dockerignore +82 -0
  2. package/.env.example +303 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +83 -12
  4. package/.github/ISSUE_TEMPLATE/config.yml +12 -6
  5. package/.github/ISSUE_TEMPLATE/feature_request.md +61 -10
  6. package/.github/PULL_REQUEST_TEMPLATE.md +53 -26
  7. package/.github/dependabot.yml +9 -0
  8. package/.github/workflows/codeql.yml +38 -0
  9. package/.github/workflows/npm-publish.yml +20 -0
  10. package/.github/workflows/stale.yml +56 -0
  11. package/ARCHITECTURE.md +346 -0
  12. package/AUDIT_REPORT.md +28 -0
  13. package/CHANGELOG.md +386 -22
  14. package/CONTRIBUTORS.md +20 -0
  15. package/Dockerfile +53 -0
  16. package/Dockerfile.proxy +33 -0
  17. package/PR_STATUS_REPORT.md +148 -0
  18. package/README.md +22 -0
  19. package/RUNKIT.md +83 -0
  20. package/_schema.html +61 -15
  21. package/articles/AI_AGENT_LLM_ROUTING.md +150 -0
  22. package/articles/FROM_ZERO_TO_10K.md +107 -0
  23. package/articles/LLM_BENCHMARK_DEEP_DIVE.md +153 -0
  24. package/articles/TWEETS_10K_DOWNLOADS.md +47 -0
  25. package/articles/TWEETS_BENCHMARK_FIRST.md +46 -0
  26. package/articles/TWEETS_MCP_PLAY.md +51 -0
  27. package/articles/TWEETS_SEQUENTIAL_BROKEN.md +49 -0
  28. package/articles/TWEETS_WHY_BUILD.md +54 -0
  29. package/benchmark-results.json +26 -45
  30. package/cli/a3m +840 -0
  31. package/demo/package.json +13 -0
  32. package/demo/public/index.html +762 -0
  33. package/demo/server.js +405 -0
  34. package/dist/cli.js +4 -0
  35. package/docker-compose.yml +74 -0
  36. package/docs/.nojekyll +0 -0
  37. package/docs/BENCHMARK.md +96 -22
  38. package/docs/_config.yml +49 -0
  39. package/docs/api.html +513 -0
  40. package/docs/benchmark.html +387 -0
  41. package/docs/cli-cheatsheet.md +339 -0
  42. package/docs/comparison.md +108 -0
  43. package/docs/curl-examples.md +247 -0
  44. package/docs/index.html +390 -99
  45. package/docs/openapi.yaml +1318 -0
  46. package/docs/quick-start.html +366 -0
  47. package/docs/robots.txt +1 -1
  48. package/docs/sitemap.xml +23 -5
  49. package/docs/styles.css +682 -0
  50. package/examples/README.md +61 -0
  51. package/examples/a3m-sdk.js +124 -0
  52. package/examples/basic-route.js +54 -0
  53. package/examples/chat-loop.js +202 -0
  54. package/examples/classify-then-route.js +102 -0
  55. package/examples/cost-compare.js +120 -0
  56. package/examples/ensemble.js +160 -0
  57. package/integrations/langchain/README.md +216 -0
  58. package/integrations/langchain/a3m_langchain.ts +1360 -0
  59. package/integrations/langchain/example.ts +287 -0
  60. package/integrations/vercel-ai-sdk/README.md +49 -0
  61. package/integrations/vercel-ai-sdk/a3m_provider.ts +78 -0
  62. package/integrations/vercel-ai-sdk/example.ts +25 -0
  63. package/llms-full.txt +43 -0
  64. package/llms.txt +9 -0
  65. package/mcp-server/README.md +188 -0
  66. package/mcp-server/package.json +29 -0
  67. package/mcp-server/src/index.ts +744 -0
  68. package/mcp-server/tsconfig.json +19 -0
  69. package/package.json +3 -3
  70. package/proxy/README.md +227 -0
  71. package/proxy/package-lock.json +831 -0
  72. package/proxy/package.json +17 -0
  73. package/proxy/rate-limit.js +145 -0
  74. package/proxy/rate-limit.test.js +311 -0
  75. package/proxy/server.js +970 -0
  76. package/scripts/banner.js +29 -0
  77. package/scripts/compare-providers.sh +230 -0
  78. package/scripts/cross_post.py +443 -0
  79. package/scripts/publish_fcc.py +106 -0
  80. package/scripts/push-to-gitee.sh +52 -0
  81. package/src/tui/dashboard.ts +13 -0
  82. package/tests/__mocks__/tokenUtils.ts +22 -0
  83. package/tests/memory/episodicMemory.test.ts +227 -0
  84. package/tests/package-lock.json +1628 -0
  85. package/tests/package.json +18 -0
  86. package/tests/routing/ensembleVoting.test.ts +236 -0
  87. package/tests/routing/providerRetry.test.ts +360 -0
  88. package/tests/routing/queryTypePresets.test.ts +206 -0
  89. package/tests/tsconfig.json +21 -0
  90. package/tests/vitest.config.ts +18 -0
  91. package/.env +0 -2
@@ -0,0 +1,227 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { EpisodicMemoryStore } from '../../tmlpd-pi-extension/src/memory/episodicMemory';
3
+
4
+ describe('EpisodicMemoryStore', () => {
5
+ let store: EpisodicMemoryStore;
6
+
7
+ const sampleEntry = {
8
+ task: {
9
+ description: 'Test task execution',
10
+ type: 'code_generation',
11
+ complexity: 3,
12
+ },
13
+ result: {
14
+ success: true,
15
+ output: 'Successfully generated code',
16
+ duration_ms: 1500,
17
+ },
18
+ agent: {
19
+ id: 'agent-1',
20
+ model: 'gpt-4o',
21
+ provider: 'openai',
22
+ },
23
+ metadata: { source: 'test' },
24
+ importance: 5,
25
+ };
26
+
27
+ beforeEach(() => {
28
+ store = new EpisodicMemoryStore(100);
29
+ });
30
+
31
+ describe('store and query', () => {
32
+ it('stores an entry and returns a non-empty id', () => {
33
+ const id = store.store(sampleEntry);
34
+ expect(id).toBeTruthy();
35
+ expect(typeof id).toBe('string');
36
+ expect(id.length).toBeGreaterThan(0);
37
+ });
38
+
39
+ it('stores multiple entries and retrieves them', () => {
40
+ store.store(sampleEntry);
41
+ store.store({ ...sampleEntry, task: { ...sampleEntry.task, description: 'Second task' } });
42
+
43
+ const results = store.query({ limit: 10 });
44
+ expect(results.length).toBe(2);
45
+ });
46
+
47
+ it('filters by task type', () => {
48
+ store.store(sampleEntry);
49
+ store.store({
50
+ ...sampleEntry,
51
+ task: { ...sampleEntry.task, type: 'data_analysis' },
52
+ });
53
+
54
+ const codeResults = store.query({ task_type: 'code_generation' });
55
+ expect(codeResults.length).toBe(1);
56
+ expect(codeResults[0].task.type).toBe('code_generation');
57
+
58
+ const analysisResults = store.query({ task_type: 'data_analysis' });
59
+ expect(analysisResults.length).toBe(1);
60
+ });
61
+
62
+ it('filters by keywords', () => {
63
+ store.store(sampleEntry);
64
+ store.store({
65
+ ...sampleEntry,
66
+ task: { ...sampleEntry.task, description: 'Database query optimization' },
67
+ });
68
+
69
+ const results = store.query({ keywords: ['database'] });
70
+ expect(results.length).toBe(1);
71
+ expect(results[0].task.description).toContain('Database');
72
+ });
73
+
74
+ it('returns newest entries first', () => {
75
+ store.store(sampleEntry);
76
+ store.store({ ...sampleEntry, task: { ...sampleEntry.task, description: 'Second' } });
77
+
78
+ const results = store.query({ limit: 10 });
79
+ expect(results[results.length - 1].task.description).toBe('Second');
80
+ });
81
+
82
+ it('respects limit parameter', () => {
83
+ for (let i = 0; i < 10; i++) {
84
+ store.store({ ...sampleEntry, task: { ...sampleEntry.task, description: `Task ${i}` } });
85
+ }
86
+
87
+ expect(store.query({ limit: 3 }).length).toBe(3);
88
+ expect(store.query({ limit: 20 }).length).toBe(10);
89
+ });
90
+ });
91
+
92
+ describe('max entries eviction', () => {
93
+ it('evicts oldest entries when at capacity', () => {
94
+ const smallStore = new EpisodicMemoryStore(3);
95
+ smallStore.store({ ...sampleEntry, task: { ...sampleEntry.task, description: 'A' } });
96
+ smallStore.store({ ...sampleEntry, task: { ...sampleEntry.task, description: 'B' } });
97
+ smallStore.store({ ...sampleEntry, task: { ...sampleEntry.task, description: 'C' } });
98
+ smallStore.store({ ...sampleEntry, task: { ...sampleEntry.task, description: 'D' } });
99
+
100
+ expect(smallStore.query({ limit: 10 }).length).toBe(3);
101
+ const descriptions = smallStore.query({ limit: 10 }).map(e => e.task.description);
102
+ expect(descriptions).not.toContain('A');
103
+ expect(descriptions).toContain('D');
104
+ });
105
+ });
106
+
107
+ describe('getSimilarTasks', () => {
108
+ it('finds tasks with overlapping words', () => {
109
+ store.store({ ...sampleEntry, task: { ...sampleEntry.task, description: 'Build REST API endpoint' } });
110
+ store.store({ ...sampleEntry, task: { ...sampleEntry.task, description: 'Deploy database cluster' } });
111
+ store.store({ ...sampleEntry, task: { ...sampleEntry.task, description: 'Fix API authentication bug' } });
112
+
113
+ const similar = store.getSimilarTasks('Build API endpoint');
114
+ expect(similar.length).toBeGreaterThanOrEqual(1);
115
+ // "API" should match entries 0 and 2
116
+ const descriptions = similar.map(e => e.task.description);
117
+ expect(descriptions.some(d => d.includes('API'))).toBe(true);
118
+ });
119
+
120
+ it('returns empty array for no matches', () => {
121
+ store.store(sampleEntry);
122
+ const similar = store.getSimilarTasks('xyznonexistent12345');
123
+ expect(similar.length).toBe(0);
124
+ });
125
+
126
+ it('respects limit parameter', () => {
127
+ store.store({ ...sampleEntry, task: { ...sampleEntry.task, description: 'Task A' } });
128
+ store.store({ ...sampleEntry, task: { ...sampleEntry.task, description: 'Task B' } });
129
+ store.store({ ...sampleEntry, task: { ...sampleEntry.task, description: 'Task C' } });
130
+
131
+ const similar = store.getSimilarTasks('Task', 2);
132
+ expect(similar.length).toBeLessThanOrEqual(2);
133
+ });
134
+ });
135
+
136
+ describe('getStats', () => {
137
+ it('returns zeroed stats for empty store', () => {
138
+ const stats = store.getStats();
139
+ expect(stats.total_entries).toBe(0);
140
+ expect(stats.indexed_keywords).toBe(0);
141
+ expect(stats.success_rate).toBe(0);
142
+ expect(stats.avg_duration_ms).toBe(0);
143
+ });
144
+
145
+ it('returns accurate stats after entries', () => {
146
+ store.store(sampleEntry);
147
+ store.store({
148
+ ...sampleEntry,
149
+ result: { ...sampleEntry.result, success: false },
150
+ });
151
+
152
+ const stats = store.getStats();
153
+ expect(stats.total_entries).toBe(2);
154
+ expect(stats.success_rate).toBe(0.5);
155
+ expect(stats.avg_duration_ms).toBe(1500);
156
+ });
157
+ });
158
+
159
+ describe('clear', () => {
160
+ it('removes all entries', () => {
161
+ store.store(sampleEntry);
162
+ store.store(sampleEntry);
163
+ expect(store.query({ limit: 10 }).length).toBe(2);
164
+
165
+ store.clear();
166
+ expect(store.query({ limit: 10 }).length).toBe(0);
167
+ expect(store.getStats().total_entries).toBe(0);
168
+ });
169
+ });
170
+
171
+ describe('persistence', () => {
172
+ let fsMock: any;
173
+
174
+ beforeEach(() => {
175
+ fsMock = {
176
+ existsSync: vi.fn().mockReturnValue(false),
177
+ readFileSync: vi.fn(),
178
+ writeFileSync: vi.fn(),
179
+ unlinkSync: vi.fn(),
180
+ };
181
+
182
+ vi.doMock('fs', () => fsMock);
183
+ });
184
+
185
+ afterEach(() => {
186
+ vi.restoreAllMocks();
187
+ });
188
+
189
+ it('loadFromDisk returns false when file does not exist', () => {
190
+ const store2 = new EpisodicMemoryStore(100, '/tmp/test-memory.json');
191
+ expect(store2['loadFromDisk']()).toBe(false);
192
+ });
193
+
194
+ it('saveToDisk returns false when no persistence path', () => {
195
+ expect(store.saveToDisk()).toBe(false);
196
+ });
197
+
198
+ it('autoPersist triggers after every 3 entries', () => {
199
+ const saveSpy = vi.spyOn(store as any, 'autoPersist');
200
+ store.store(sampleEntry);
201
+ store.store(sampleEntry);
202
+ expect(saveSpy).toHaveBeenCalledTimes(2); // called on each store
203
+ expect((store as any).entries.length % 3 === 0).toBe(false);
204
+
205
+ store.store(sampleEntry); // 3rd entry
206
+ expect((store as any).entries.length % 3 === 0).toBe(true);
207
+ });
208
+ });
209
+
210
+ describe('keyword indexing', () => {
211
+ it('indexes words longer than 3 characters', () => {
212
+ store.store(sampleEntry);
213
+ const index = (store as any).keywordIndex as Map<string, string[]>;
214
+
215
+ // 'test' (4 chars) should be indexed, 'is' (2 chars) should not
216
+ const keys = Array.from(index.keys());
217
+ expect(keys.some(k => k.length <= 3)).toBe(false);
218
+ });
219
+
220
+ it('automatically indexes on store', () => {
221
+ store.store(sampleEntry);
222
+ const index = (store as any).keywordIndex as Map<string, string[]>;
223
+ expect(index.has('test')).toBe(true);
224
+ expect(index.has('task')).toBe(true);
225
+ });
226
+ });
227
+ });