@illuma-ai/agents 1.1.28 → 1.3.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 (263) hide show
  1. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  2. package/dist/cjs/common/spawnPath.cjs +104 -0
  3. package/dist/cjs/common/spawnPath.cjs.map +1 -0
  4. package/dist/cjs/graphs/Graph.cjs +84 -33
  5. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  6. package/dist/cjs/graphs/HandoffRegistry.cjs +47 -8
  7. package/dist/cjs/graphs/HandoffRegistry.cjs.map +1 -1
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs +493 -267
  9. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  10. package/dist/cjs/graphs/phases/flushLoop.cjs +214 -0
  11. package/dist/cjs/graphs/phases/flushLoop.cjs.map +1 -0
  12. package/dist/cjs/graphs/phases/memoryFlushPhase.cjs +102 -0
  13. package/dist/cjs/graphs/phases/memoryFlushPhase.cjs.map +1 -0
  14. package/dist/cjs/llm/bedrock/index.cjs +4 -3
  15. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  16. package/dist/cjs/main.cjs +113 -0
  17. package/dist/cjs/main.cjs.map +1 -1
  18. package/dist/cjs/memory/citations.cjs +69 -0
  19. package/dist/cjs/memory/citations.cjs.map +1 -0
  20. package/dist/cjs/memory/compositeBackend.cjs +60 -0
  21. package/dist/cjs/memory/compositeBackend.cjs.map +1 -0
  22. package/dist/cjs/memory/constants.cjs +232 -0
  23. package/dist/cjs/memory/constants.cjs.map +1 -0
  24. package/dist/cjs/memory/embeddings.cjs +151 -0
  25. package/dist/cjs/memory/embeddings.cjs.map +1 -0
  26. package/dist/cjs/memory/factory.cjs +95 -0
  27. package/dist/cjs/memory/factory.cjs.map +1 -0
  28. package/dist/cjs/memory/migrate.cjs +81 -0
  29. package/dist/cjs/memory/migrate.cjs.map +1 -0
  30. package/dist/cjs/memory/mmr.cjs +138 -0
  31. package/dist/cjs/memory/mmr.cjs.map +1 -0
  32. package/dist/cjs/memory/paths.cjs +217 -0
  33. package/dist/cjs/memory/paths.cjs.map +1 -0
  34. package/dist/cjs/memory/pgvectorStore.cjs +225 -0
  35. package/dist/cjs/memory/pgvectorStore.cjs.map +1 -0
  36. package/dist/cjs/memory/recallTracking.cjs +98 -0
  37. package/dist/cjs/memory/recallTracking.cjs.map +1 -0
  38. package/dist/cjs/memory/schema.sql +51 -0
  39. package/dist/cjs/memory/temporalDecay.cjs +118 -0
  40. package/dist/cjs/memory/temporalDecay.cjs.map +1 -0
  41. package/dist/cjs/nodes/ApprovalGateNode.cjs +1 -1
  42. package/dist/cjs/nodes/ApprovalGateNode.cjs.map +1 -1
  43. package/dist/cjs/prompts/memoryFlushPrompt.cjs +49 -0
  44. package/dist/cjs/prompts/memoryFlushPrompt.cjs.map +1 -0
  45. package/dist/cjs/run.cjs +16 -3
  46. package/dist/cjs/run.cjs.map +1 -1
  47. package/dist/cjs/tools/AskUser.cjs +6 -1
  48. package/dist/cjs/tools/AskUser.cjs.map +1 -1
  49. package/dist/cjs/tools/BrowserTools.cjs +1 -1
  50. package/dist/cjs/tools/BrowserTools.cjs.map +1 -1
  51. package/dist/cjs/tools/ToolNode.cjs +127 -10
  52. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  53. package/dist/cjs/tools/approval/constants.cjs +2 -2
  54. package/dist/cjs/tools/approval/constants.cjs.map +1 -1
  55. package/dist/cjs/tools/memory/index.cjs +58 -0
  56. package/dist/cjs/tools/memory/index.cjs.map +1 -0
  57. package/dist/cjs/tools/memory/memoryAppendTool.cjs +69 -0
  58. package/dist/cjs/tools/memory/memoryAppendTool.cjs.map +1 -0
  59. package/dist/cjs/tools/memory/memoryGetTool.cjs +49 -0
  60. package/dist/cjs/tools/memory/memoryGetTool.cjs.map +1 -0
  61. package/dist/cjs/tools/memory/memorySearchTool.cjs +65 -0
  62. package/dist/cjs/tools/memory/memorySearchTool.cjs.map +1 -0
  63. package/dist/cjs/tools/memory/shared.cjs +106 -0
  64. package/dist/cjs/tools/memory/shared.cjs.map +1 -0
  65. package/dist/cjs/types/graph.cjs.map +1 -1
  66. package/dist/cjs/utils/childAgentContext.cjs +242 -0
  67. package/dist/cjs/utils/childAgentContext.cjs.map +1 -0
  68. package/dist/cjs/utils/events.cjs +36 -7
  69. package/dist/cjs/utils/events.cjs.map +1 -1
  70. package/dist/cjs/utils/finishReasons.cjs +44 -0
  71. package/dist/cjs/utils/finishReasons.cjs.map +1 -0
  72. package/dist/cjs/utils/llm.cjs.map +1 -1
  73. package/dist/cjs/utils/logging.cjs +34 -0
  74. package/dist/cjs/utils/logging.cjs.map +1 -0
  75. package/dist/cjs/utils/toolCallNormalization.cjs +250 -0
  76. package/dist/cjs/utils/toolCallNormalization.cjs.map +1 -0
  77. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  78. package/dist/esm/common/spawnPath.mjs +95 -0
  79. package/dist/esm/common/spawnPath.mjs.map +1 -0
  80. package/dist/esm/graphs/Graph.mjs +84 -33
  81. package/dist/esm/graphs/Graph.mjs.map +1 -1
  82. package/dist/esm/graphs/HandoffRegistry.mjs +47 -8
  83. package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -1
  84. package/dist/esm/graphs/MultiAgentGraph.mjs +493 -267
  85. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  86. package/dist/esm/graphs/phases/flushLoop.mjs +209 -0
  87. package/dist/esm/graphs/phases/flushLoop.mjs.map +1 -0
  88. package/dist/esm/graphs/phases/memoryFlushPhase.mjs +99 -0
  89. package/dist/esm/graphs/phases/memoryFlushPhase.mjs.map +1 -0
  90. package/dist/esm/llm/bedrock/index.mjs +4 -3
  91. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  92. package/dist/esm/main.mjs +20 -0
  93. package/dist/esm/main.mjs.map +1 -1
  94. package/dist/esm/memory/citations.mjs +64 -0
  95. package/dist/esm/memory/citations.mjs.map +1 -0
  96. package/dist/esm/memory/compositeBackend.mjs +58 -0
  97. package/dist/esm/memory/compositeBackend.mjs.map +1 -0
  98. package/dist/esm/memory/constants.mjs +198 -0
  99. package/dist/esm/memory/constants.mjs.map +1 -0
  100. package/dist/esm/memory/embeddings.mjs +148 -0
  101. package/dist/esm/memory/embeddings.mjs.map +1 -0
  102. package/dist/esm/memory/factory.mjs +93 -0
  103. package/dist/esm/memory/factory.mjs.map +1 -0
  104. package/dist/esm/memory/migrate.mjs +78 -0
  105. package/dist/esm/memory/migrate.mjs.map +1 -0
  106. package/dist/esm/memory/mmr.mjs +130 -0
  107. package/dist/esm/memory/mmr.mjs.map +1 -0
  108. package/dist/esm/memory/paths.mjs +207 -0
  109. package/dist/esm/memory/paths.mjs.map +1 -0
  110. package/dist/esm/memory/pgvectorStore.mjs +223 -0
  111. package/dist/esm/memory/pgvectorStore.mjs.map +1 -0
  112. package/dist/esm/memory/recallTracking.mjs +94 -0
  113. package/dist/esm/memory/recallTracking.mjs.map +1 -0
  114. package/dist/esm/memory/schema.sql +51 -0
  115. package/dist/esm/memory/temporalDecay.mjs +110 -0
  116. package/dist/esm/memory/temporalDecay.mjs.map +1 -0
  117. package/dist/esm/nodes/ApprovalGateNode.mjs +1 -1
  118. package/dist/esm/nodes/ApprovalGateNode.mjs.map +1 -1
  119. package/dist/esm/prompts/memoryFlushPrompt.mjs +44 -0
  120. package/dist/esm/prompts/memoryFlushPrompt.mjs.map +1 -0
  121. package/dist/esm/run.mjs +16 -3
  122. package/dist/esm/run.mjs.map +1 -1
  123. package/dist/esm/tools/AskUser.mjs +6 -1
  124. package/dist/esm/tools/AskUser.mjs.map +1 -1
  125. package/dist/esm/tools/BrowserTools.mjs +1 -1
  126. package/dist/esm/tools/BrowserTools.mjs.map +1 -1
  127. package/dist/esm/tools/ToolNode.mjs +128 -11
  128. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  129. package/dist/esm/tools/approval/constants.mjs +2 -2
  130. package/dist/esm/tools/approval/constants.mjs.map +1 -1
  131. package/dist/esm/tools/memory/index.mjs +46 -0
  132. package/dist/esm/tools/memory/index.mjs.map +1 -0
  133. package/dist/esm/tools/memory/memoryAppendTool.mjs +67 -0
  134. package/dist/esm/tools/memory/memoryAppendTool.mjs.map +1 -0
  135. package/dist/esm/tools/memory/memoryGetTool.mjs +47 -0
  136. package/dist/esm/tools/memory/memoryGetTool.mjs.map +1 -0
  137. package/dist/esm/tools/memory/memorySearchTool.mjs +63 -0
  138. package/dist/esm/tools/memory/memorySearchTool.mjs.map +1 -0
  139. package/dist/esm/tools/memory/shared.mjs +98 -0
  140. package/dist/esm/tools/memory/shared.mjs.map +1 -0
  141. package/dist/esm/types/graph.mjs.map +1 -1
  142. package/dist/esm/utils/childAgentContext.mjs +237 -0
  143. package/dist/esm/utils/childAgentContext.mjs.map +1 -0
  144. package/dist/esm/utils/events.mjs +36 -8
  145. package/dist/esm/utils/events.mjs.map +1 -1
  146. package/dist/esm/utils/finishReasons.mjs +41 -0
  147. package/dist/esm/utils/finishReasons.mjs.map +1 -0
  148. package/dist/esm/utils/llm.mjs.map +1 -1
  149. package/dist/esm/utils/logging.mjs +31 -0
  150. package/dist/esm/utils/logging.mjs.map +1 -0
  151. package/dist/esm/utils/toolCallNormalization.mjs +247 -0
  152. package/dist/esm/utils/toolCallNormalization.mjs.map +1 -0
  153. package/dist/types/common/index.d.ts +1 -0
  154. package/dist/types/common/spawnPath.d.ts +59 -0
  155. package/dist/types/graphs/HandoffRegistry.d.ts +24 -7
  156. package/dist/types/graphs/MultiAgentGraph.d.ts +43 -23
  157. package/dist/types/graphs/phases/flushLoop.d.ts +106 -0
  158. package/dist/types/graphs/phases/memoryFlushPhase.d.ts +100 -0
  159. package/dist/types/index.d.ts +7 -0
  160. package/dist/types/memory/__tests__/mockBackend.d.ts +40 -0
  161. package/dist/types/memory/citations.d.ts +39 -0
  162. package/dist/types/memory/compositeBackend.d.ts +30 -0
  163. package/dist/types/memory/constants.d.ts +121 -0
  164. package/dist/types/memory/embeddings.d.ts +15 -0
  165. package/dist/types/memory/factory.d.ts +23 -0
  166. package/dist/types/memory/index.d.ts +21 -0
  167. package/dist/types/memory/migrate.d.ts +14 -0
  168. package/dist/types/memory/mmr.d.ts +50 -0
  169. package/dist/types/memory/paths.d.ts +107 -0
  170. package/dist/types/memory/pgvectorStore.d.ts +56 -0
  171. package/dist/types/memory/recallTracking.d.ts +30 -0
  172. package/dist/types/memory/temporalDecay.d.ts +53 -0
  173. package/dist/types/memory/types.d.ts +182 -0
  174. package/dist/types/prompts/memoryFlushPrompt.d.ts +54 -0
  175. package/dist/types/run.d.ts +1 -0
  176. package/dist/types/tools/AskUser.d.ts +1 -1
  177. package/dist/types/tools/BrowserTools.d.ts +2 -2
  178. package/dist/types/tools/approval/constants.d.ts +2 -2
  179. package/dist/types/tools/memory/index.d.ts +39 -0
  180. package/dist/types/tools/memory/memoryAppendTool.d.ts +27 -0
  181. package/dist/types/tools/memory/memoryGetTool.d.ts +22 -0
  182. package/dist/types/tools/memory/memorySearchTool.d.ts +22 -0
  183. package/dist/types/tools/memory/shared.d.ts +106 -0
  184. package/dist/types/types/graph.d.ts +10 -3
  185. package/dist/types/utils/childAgentContext.d.ts +99 -0
  186. package/dist/types/utils/events.d.ts +21 -0
  187. package/dist/types/utils/finishReasons.d.ts +32 -0
  188. package/dist/types/utils/logging.d.ts +2 -0
  189. package/dist/types/utils/toolCallNormalization.d.ts +44 -0
  190. package/package.json +6 -4
  191. package/src/agents/AgentContext.ts +12 -4
  192. package/src/common/__tests__/enum.test.ts +4 -2
  193. package/src/common/__tests__/spawnPath.test.ts +110 -0
  194. package/src/common/index.ts +1 -0
  195. package/src/common/spawnPath.ts +101 -0
  196. package/src/graphs/Graph.ts +90 -47
  197. package/src/graphs/HandoffRegistry.ts +48 -17
  198. package/src/graphs/MultiAgentGraph.ts +588 -327
  199. package/src/graphs/__tests__/HandoffRegistry.test.ts +4 -1
  200. package/src/graphs/__tests__/multi-agent-delegate.test.ts +61 -16
  201. package/src/graphs/__tests__/multi-agent-edges.test.ts +4 -2
  202. package/src/graphs/__tests__/multi-agent-nested-subgraph.test.ts +221 -0
  203. package/src/graphs/__tests__/structured-output.integration.test.ts +212 -118
  204. package/src/graphs/contextManagement.e2e.test.ts +1 -1
  205. package/src/graphs/phases/__tests__/flushLoop.test.ts +264 -0
  206. package/src/graphs/phases/__tests__/memoryFlushPhase.test.ts +37 -0
  207. package/src/graphs/phases/__tests__/runMemoryFlush.test.ts +150 -0
  208. package/src/graphs/phases/flushLoop.ts +303 -0
  209. package/src/graphs/phases/memoryFlushPhase.ts +209 -0
  210. package/src/index.ts +30 -1
  211. package/src/llm/bedrock/index.ts +4 -5
  212. package/src/memory/__tests__/citations.test.ts +61 -0
  213. package/src/memory/__tests__/compositeBackend.test.ts +79 -0
  214. package/src/memory/__tests__/isolation.test.ts +206 -0
  215. package/src/memory/__tests__/mmr.test.ts +148 -0
  216. package/src/memory/__tests__/mockBackend.ts +161 -0
  217. package/src/memory/__tests__/paths.test.ts +168 -0
  218. package/src/memory/__tests__/recallTracking.test.ts +96 -0
  219. package/src/memory/__tests__/temporalDecay.test.ts +151 -0
  220. package/src/memory/citations.ts +80 -0
  221. package/src/memory/compositeBackend.ts +99 -0
  222. package/src/memory/constants.ts +229 -0
  223. package/src/memory/embeddings.ts +188 -0
  224. package/src/memory/factory.ts +111 -0
  225. package/src/memory/index.ts +46 -0
  226. package/src/memory/migrate.ts +116 -0
  227. package/src/memory/mmr.ts +161 -0
  228. package/src/memory/paths.ts +258 -0
  229. package/src/memory/pgvectorStore.ts +324 -0
  230. package/src/memory/recallTracking.ts +127 -0
  231. package/src/memory/schema.sql +51 -0
  232. package/src/memory/temporalDecay.ts +134 -0
  233. package/src/memory/types.ts +185 -0
  234. package/src/nodes/ApprovalGateNode.ts +4 -10
  235. package/src/nodes/__tests__/ApprovalGateNode.test.ts +11 -20
  236. package/src/prompts/memoryFlushPrompt.ts +78 -0
  237. package/src/run.ts +17 -6
  238. package/src/scripts/test-bedrock-handoff-autonomous.ts +56 -20
  239. package/src/specs/agent-handoffs-bedrock.integration.test.ts +8 -5
  240. package/src/specs/agent-handoffs.test.ts +8 -2
  241. package/src/tools/AskUser.ts +7 -2
  242. package/src/tools/BrowserTools.ts +3 -5
  243. package/src/tools/ToolNode.ts +150 -13
  244. package/src/tools/__tests__/ToolApproval.test.ts +22 -9
  245. package/src/tools/approval/__tests__/constants.test.ts +1 -1
  246. package/src/tools/approval/constants.ts +2 -2
  247. package/src/tools/memory/__tests__/memoryTools.test.ts +205 -0
  248. package/src/tools/memory/index.ts +96 -0
  249. package/src/tools/memory/memoryAppendTool.ts +101 -0
  250. package/src/tools/memory/memoryGetTool.ts +53 -0
  251. package/src/tools/memory/memorySearchTool.ts +80 -0
  252. package/src/tools/memory/shared.ts +169 -0
  253. package/src/tools/search/search.test.ts +6 -1
  254. package/src/types/graph.ts +10 -3
  255. package/src/utils/__tests__/childAgentContext.test.ts +217 -0
  256. package/src/utils/__tests__/finishReasons.test.ts +55 -0
  257. package/src/utils/__tests__/toolCallNormalization.test.ts +181 -0
  258. package/src/utils/childAgentContext.ts +259 -0
  259. package/src/utils/events.ts +37 -7
  260. package/src/utils/finishReasons.ts +40 -0
  261. package/src/utils/llm.ts +0 -1
  262. package/src/utils/logging.ts +45 -8
  263. package/src/utils/toolCallNormalization.ts +271 -0
@@ -0,0 +1,79 @@
1
+ import { CompositeMemoryBackend } from '../compositeBackend';
2
+ import { MockMemoryBackend } from './mockBackend';
3
+ import type { MemoryBackend, MemoryEntry } from '../types';
4
+
5
+ class FakeGraphBackend implements MemoryBackend {
6
+ readonly kind = 'graph' as const;
7
+ private data: MemoryEntry[] = [];
8
+ constructor(seed: Array<{ content: string; score: number }>) {
9
+ this.data = seed.map((s, i) => ({
10
+ id: `g${i}`,
11
+ path: 'graph/node.md',
12
+ content: s.content,
13
+ score: s.score,
14
+ createdAt: new Date(),
15
+ source: 'graph',
16
+ }));
17
+ }
18
+ async search() {
19
+ return this.data;
20
+ }
21
+ async get() {
22
+ return null;
23
+ }
24
+ async append() {}
25
+ async health() {
26
+ return { ok: true, backend: 'graph' as const };
27
+ }
28
+ }
29
+
30
+ describe('CompositeMemoryBackend', () => {
31
+ const scope = { agentId: 'A', userId: '1' };
32
+
33
+ it('merges and ranks results from every backend', async () => {
34
+ const vector = new MockMemoryBackend();
35
+ await vector.append(scope, {
36
+ path: 'memory/agent/playbook.md',
37
+ content: 'decision about X',
38
+ });
39
+ const graph = new FakeGraphBackend([
40
+ { content: 'graph node about X', score: 0.95 },
41
+ ]);
42
+ const composite = new CompositeMemoryBackend([vector, graph]);
43
+
44
+ const results = await composite.search(scope, 'X', { maxResults: 5 });
45
+ expect(results.length).toBeGreaterThanOrEqual(2);
46
+ expect(results[0].score).toBeGreaterThanOrEqual(
47
+ results[results.length - 1].score
48
+ );
49
+ expect(results.map((r) => r.source)).toEqual(
50
+ expect.arrayContaining(['vector', 'graph'])
51
+ );
52
+ });
53
+
54
+ it('fan-out append stops on first failure', async () => {
55
+ const ok = new MockMemoryBackend();
56
+ const broken: MemoryBackend = {
57
+ kind: 'graph',
58
+ async search() {
59
+ return [];
60
+ },
61
+ async get() {
62
+ return null;
63
+ },
64
+ async append() {
65
+ throw new Error('graph down');
66
+ },
67
+ async health() {
68
+ return { ok: false, backend: 'graph', error: 'down' };
69
+ },
70
+ };
71
+ const composite = new CompositeMemoryBackend([ok, broken]);
72
+ await expect(
73
+ composite.append(scope, {
74
+ path: 'memory/agent/pitfalls.md',
75
+ content: 'test',
76
+ })
77
+ ).rejects.toThrow('graph down');
78
+ });
79
+ });
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Tier isolation test — the most load-bearing privacy guarantee in the
3
+ * whole memory system. If any of these fail, users can see each other's
4
+ * private rows, which would be a serious correctness regression.
5
+ *
6
+ * Three guarantees covered:
7
+ * 1. Agent-tier rows (`memory/agent/*`) are shared across every user
8
+ * of the same agent.
9
+ * 2. User-tier rows (`memory/user/*`) are private to the caller —
10
+ * another user of the same agent can NEVER read them.
11
+ * 3. Writes to user-tier paths require a scoped caller; isolated
12
+ * contexts (no userId) throw.
13
+ *
14
+ * Plus the original cross-agent isolation (two different agents can
15
+ * never see each other's rows).
16
+ */
17
+ import { MockMemoryBackend } from './mockBackend';
18
+
19
+ describe('MemoryBackend tier + user isolation', () => {
20
+ const salesAlice = { agentId: 'sales', userId: 'alice' };
21
+ const salesBob = { agentId: 'sales', userId: 'bob' };
22
+ const salesNoCaller = { agentId: 'sales' }; // isolated / autonomous
23
+ const supportAlice = { agentId: 'support', userId: 'alice' };
24
+
25
+ let backend: MockMemoryBackend;
26
+
27
+ beforeEach(async () => {
28
+ backend = new MockMemoryBackend();
29
+ });
30
+
31
+ describe('agent tier (shared)', () => {
32
+ it('one agent/playbook.md row is visible to every user of the same agent', async () => {
33
+ await backend.append(salesAlice, {
34
+ path: 'memory/agent/playbook.md',
35
+ content: 'Sales discovery: lead with ROI numbers, not features.',
36
+ });
37
+
38
+ // Bob, a different user of the SAME agent, sees the row.
39
+ const fromBob = await backend.get(salesBob, {
40
+ path: 'memory/agent/playbook.md',
41
+ });
42
+ expect(fromBob?.text).toContain('ROI numbers');
43
+
44
+ // Isolated caller (no userId) also sees it — agent tier is inherent.
45
+ const fromIsolated = await backend.get(salesNoCaller, {
46
+ path: 'memory/agent/playbook.md',
47
+ });
48
+ expect(fromIsolated?.text).toContain('ROI numbers');
49
+
50
+ // Search returns exactly one row (shared slot, NULLS NOT DISTINCT).
51
+ const rows = backend.allRows();
52
+ const playbookRows = rows.filter(
53
+ (r) => r.path === 'memory/agent/playbook.md'
54
+ );
55
+ expect(playbookRows).toHaveLength(1);
56
+ expect(playbookRows[0].userId).toBeNull();
57
+ });
58
+
59
+ it('different users appending to the same agent-tier path merge into one row', async () => {
60
+ await backend.append(salesAlice, {
61
+ path: 'memory/agent/pitfalls.md',
62
+ content: 'AlicePitfall',
63
+ });
64
+ await backend.append(salesBob, {
65
+ path: 'memory/agent/pitfalls.md',
66
+ content: 'BobPitfall',
67
+ });
68
+
69
+ const rows = backend
70
+ .allRows()
71
+ .filter(
72
+ (r) => r.agentId === 'sales' && r.path === 'memory/agent/pitfalls.md'
73
+ );
74
+ expect(rows).toHaveLength(1);
75
+ expect(rows[0].userId).toBeNull();
76
+ expect(rows[0].content).toContain('AlicePitfall');
77
+ expect(rows[0].content).toContain('BobPitfall');
78
+ // Latest writer recorded as provenance even though scoping stays NULL.
79
+ expect(rows[0].lastUserId).toBe('bob');
80
+ });
81
+ });
82
+
83
+ describe('user tier (private per caller)', () => {
84
+ beforeEach(async () => {
85
+ await backend.append(salesAlice, {
86
+ path: 'memory/user/preferences.md',
87
+ content: 'Alice prefers bullet points and formal tone.',
88
+ });
89
+ await backend.append(salesBob, {
90
+ path: 'memory/user/preferences.md',
91
+ content: 'Bob prefers terse one-liners.',
92
+ });
93
+ });
94
+
95
+ it('each user sees only their own preferences row via get()', async () => {
96
+ const aliceRead = await backend.get(salesAlice, {
97
+ path: 'memory/user/preferences.md',
98
+ });
99
+ expect(aliceRead?.text).toContain('Alice prefers');
100
+ expect(aliceRead?.text).not.toContain('Bob prefers');
101
+
102
+ const bobRead = await backend.get(salesBob, {
103
+ path: 'memory/user/preferences.md',
104
+ });
105
+ expect(bobRead?.text).toContain('Bob prefers');
106
+ expect(bobRead?.text).not.toContain('Alice prefers');
107
+ });
108
+
109
+ it('each user sees only their own user-tier rows via search()', async () => {
110
+ const aliceHits = await backend.search(salesAlice, 'prefers bullet');
111
+ expect(aliceHits.length).toBeGreaterThan(0);
112
+ expect(aliceHits.every((h) => h.content.includes('Alice prefers'))).toBe(
113
+ true
114
+ );
115
+ expect(aliceHits.some((h) => h.content.includes('Bob prefers'))).toBe(
116
+ false
117
+ );
118
+
119
+ const bobHits = await backend.search(salesBob, 'prefers one-liners');
120
+ expect(bobHits.length).toBeGreaterThan(0);
121
+ expect(bobHits.every((h) => h.content.includes('Bob prefers'))).toBe(
122
+ true
123
+ );
124
+ });
125
+
126
+ it('isolated caller (no userId) cannot see any user-tier row', async () => {
127
+ const hits = await backend.search(salesNoCaller, 'prefers');
128
+ // Only agent-tier rows would surface. None exist here, so empty.
129
+ expect(
130
+ hits.filter((h) => h.path.startsWith('memory/user/'))
131
+ ).toHaveLength(0);
132
+ });
133
+
134
+ it('two rows exist in storage — one per user, same path', async () => {
135
+ const prefRows = backend
136
+ .allRows()
137
+ .filter((r) => r.path === 'memory/user/preferences.md');
138
+ expect(prefRows).toHaveLength(2);
139
+ const byUser = new Set(prefRows.map((r) => r.userId));
140
+ expect(byUser).toEqual(new Set(['alice', 'bob']));
141
+ });
142
+
143
+ it('writing to a user-tier path without userId throws', async () => {
144
+ await expect(
145
+ backend.append(salesNoCaller, {
146
+ path: 'memory/user/profile.md',
147
+ content: 'nope',
148
+ })
149
+ ).rejects.toThrow(/requires a caller userId/);
150
+ });
151
+ });
152
+
153
+ describe('path whitelist', () => {
154
+ it('rejects legacy date-keyed paths', async () => {
155
+ await expect(
156
+ backend.append(salesAlice, {
157
+ path: 'memory/2026-04-14.md',
158
+ content: 'anything',
159
+ })
160
+ ).rejects.toThrow(/not on the canonical whitelist/);
161
+ });
162
+
163
+ it('rejects paths without the memory/ prefix', async () => {
164
+ await expect(
165
+ backend.append(salesAlice, {
166
+ path: 'agent/playbook.md',
167
+ content: 'anything',
168
+ })
169
+ ).rejects.toThrow(/must start with "memory\//);
170
+ });
171
+
172
+ it('rejects invented sibling paths inside memory/agent/', async () => {
173
+ await expect(
174
+ backend.append(salesAlice, {
175
+ path: 'memory/agent/notes.md',
176
+ content: 'anything',
177
+ })
178
+ ).rejects.toThrow(/not on the canonical whitelist/);
179
+ });
180
+ });
181
+
182
+ describe('cross-agent isolation', () => {
183
+ it('two different agents cannot see each other rows — even agent-tier', async () => {
184
+ await backend.append(salesAlice, {
185
+ path: 'memory/agent/playbook.md',
186
+ content: 'Sales playbook entry',
187
+ });
188
+ await backend.append(supportAlice, {
189
+ path: 'memory/agent/playbook.md',
190
+ content: 'Support playbook entry',
191
+ });
192
+
193
+ const salesRead = await backend.get(salesAlice, {
194
+ path: 'memory/agent/playbook.md',
195
+ });
196
+ expect(salesRead?.text).toContain('Sales playbook');
197
+ expect(salesRead?.text).not.toContain('Support playbook');
198
+
199
+ const supportRead = await backend.get(supportAlice, {
200
+ path: 'memory/agent/playbook.md',
201
+ });
202
+ expect(supportRead?.text).toContain('Support playbook');
203
+ expect(supportRead?.text).not.toContain('Sales playbook');
204
+ });
205
+ });
206
+ });
@@ -0,0 +1,148 @@
1
+ import {
2
+ applyMMRToMemoryHits,
3
+ computeMMRScore,
4
+ DEFAULT_MMR_CONFIG,
5
+ jaccardSimilarity,
6
+ mmrRerank,
7
+ textSimilarity,
8
+ tokenize,
9
+ type MMRItem,
10
+ } from '../mmr';
11
+
12
+ describe('tokenize', () => {
13
+ it('returns empty set for empty string', () => {
14
+ expect(tokenize('')).toEqual(new Set());
15
+ });
16
+
17
+ it('dedupes and lowercases ASCII', () => {
18
+ expect(tokenize('Hello Hello World')).toEqual(new Set(['hello', 'world']));
19
+ });
20
+
21
+ it('produces CJK unigrams and adjacent bigrams', () => {
22
+ expect(tokenize('今天讨论')).toEqual(
23
+ new Set(['今', '天', '讨', '论', '今天', '天讨', '讨论'])
24
+ );
25
+ });
26
+
27
+ it('does not create bigrams across non-adjacent CJK chars', () => {
28
+ expect(tokenize('我a好')).toEqual(new Set(['a', '我', '好']));
29
+ });
30
+ });
31
+
32
+ describe('jaccardSimilarity', () => {
33
+ it('is 1 for identical sets', () => {
34
+ expect(jaccardSimilarity(new Set(['a', 'b']), new Set(['a', 'b']))).toBe(1);
35
+ });
36
+ it('is 0 for disjoint sets', () => {
37
+ expect(jaccardSimilarity(new Set(['a']), new Set(['b']))).toBe(0);
38
+ });
39
+ it('handles partial overlap', () => {
40
+ expect(
41
+ jaccardSimilarity(new Set(['a', 'b', 'c']), new Set(['b', 'c', 'd']))
42
+ ).toBe(0.5);
43
+ });
44
+ });
45
+
46
+ describe('textSimilarity', () => {
47
+ it('is case insensitive', () => {
48
+ expect(textSimilarity('Hello World', 'hello world')).toBe(1);
49
+ });
50
+ it('returns 0 for unrelated text', () => {
51
+ expect(textSimilarity('apple banana', 'stone iron')).toBe(0);
52
+ });
53
+ });
54
+
55
+ describe('computeMMRScore', () => {
56
+ it('matches the formula λ*r - (1-λ)*s', () => {
57
+ expect(computeMMRScore(1, 0.5, 0.7)).toBeCloseTo(0.55);
58
+ expect(computeMMRScore(0.8, 0.5, 1)).toBe(0.8);
59
+ expect(computeMMRScore(0.8, 0.5, 0)).toBe(-0.5);
60
+ });
61
+ });
62
+
63
+ describe('mmrRerank', () => {
64
+ it('is a no-op when disabled', () => {
65
+ const items: MMRItem[] = [
66
+ { id: '1', score: 0.9, content: 'apple apple' },
67
+ { id: '2', score: 0.8, content: 'apple apple' },
68
+ ];
69
+ expect(mmrRerank(items)).toEqual(items);
70
+ });
71
+
72
+ it('returns single item unchanged', () => {
73
+ const items: MMRItem[] = [{ id: '1', score: 0.9, content: 'x' }];
74
+ expect(mmrRerank(items, { enabled: true })).toEqual(items);
75
+ });
76
+
77
+ it('promotes diverse items over near-duplicates', () => {
78
+ const items: MMRItem[] = [
79
+ { id: '1', score: 1.0, content: 'machine learning neural networks' },
80
+ { id: '2', score: 0.95, content: 'machine learning deep networks' },
81
+ { id: '3', score: 0.9, content: 'database sql queries' },
82
+ ];
83
+ const reranked = mmrRerank(items, { enabled: true, lambda: 0.5 });
84
+ expect(reranked[0].id).toBe('1');
85
+ expect(reranked[1].id).toBe('3'); // diverse beats the ML near-duplicate
86
+ });
87
+
88
+ it('with λ=1, falls back to pure score order', () => {
89
+ const items: MMRItem[] = [
90
+ { id: '1', score: 1.0, content: 'a' },
91
+ { id: '2', score: 0.5, content: 'a' },
92
+ { id: '3', score: 0.9, content: 'a' },
93
+ ];
94
+ expect(
95
+ mmrRerank(items, { enabled: true, lambda: 1 }).map((i) => i.id)
96
+ ).toEqual(['1', '3', '2']);
97
+ });
98
+
99
+ it('default config is disabled with lambda 0.7', () => {
100
+ expect(DEFAULT_MMR_CONFIG).toEqual({ enabled: false, lambda: 0.7 });
101
+ });
102
+ });
103
+
104
+ describe('applyMMRToMemoryHits', () => {
105
+ it('preserves original MemoryEntry fields', () => {
106
+ const hits = [
107
+ {
108
+ id: 'a',
109
+ path: 'memory/x.md',
110
+ content: 'alpha beta',
111
+ score: 0.9,
112
+ createdAt: new Date(),
113
+ source: 'vector' as const,
114
+ },
115
+ ];
116
+ const out = applyMMRToMemoryHits(hits, { enabled: true });
117
+ expect(out[0]).toBe(hits[0]);
118
+ });
119
+
120
+ it('diversifies across path-duplicate hits', () => {
121
+ const hits = [
122
+ {
123
+ id: 'a',
124
+ path: 'memory/1.md',
125
+ content: 'ml neural',
126
+ score: 1.0,
127
+ createdAt: new Date(),
128
+ },
129
+ {
130
+ id: 'b',
131
+ path: 'memory/2.md',
132
+ content: 'ml neural',
133
+ score: 0.99,
134
+ createdAt: new Date(),
135
+ },
136
+ {
137
+ id: 'c',
138
+ path: 'memory/3.md',
139
+ content: 'sql db',
140
+ score: 0.8,
141
+ createdAt: new Date(),
142
+ },
143
+ ];
144
+ const out = applyMMRToMemoryHits(hits, { enabled: true, lambda: 0.5 });
145
+ expect(out[0].id).toBe('a');
146
+ expect(out[1].id).toBe('c');
147
+ });
148
+ });
@@ -0,0 +1,161 @@
1
+ /**
2
+ * In-memory mock implementation of {@link MemoryBackend} — for unit tests.
3
+ *
4
+ * Enforces the SAME invariants as the real pgvector store so behavior
5
+ * tests can run without Postgres:
6
+ *
7
+ * - writes validate path + scope via `assertWritablePath`
8
+ * - agent-tier paths store with `userId = null`
9
+ * - user-tier paths store with the caller's `userId`
10
+ * - reads apply the layered filter
11
+ * agent_id = $scope.agentId
12
+ * AND (user_id IS NULL OR user_id = $scope.userId)
13
+ * - UPSERT key is `(agentId, userId, path)` with NULL collisions
14
+ * (so `NULLS NOT DISTINCT` semantics are reproduced in-memory)
15
+ */
16
+ import type {
17
+ MemoryAppendInput,
18
+ MemoryBackend,
19
+ MemoryEntry,
20
+ MemoryGetOptions,
21
+ MemoryHealth,
22
+ MemoryReadResult,
23
+ MemoryScope,
24
+ MemorySearchOptions,
25
+ } from '../types';
26
+ import { assertWritablePath, getTierForPath } from '../paths';
27
+
28
+ interface Row {
29
+ id: string;
30
+ agentId: string;
31
+ /** null for agent-tier rows; caller id for user-tier rows. */
32
+ userId: string | null;
33
+ /** Latest writer — provenance, tracked even on agent-tier rows. */
34
+ lastUserId: string | null;
35
+ path: string;
36
+ content: string;
37
+ createdAt: Date;
38
+ updatedAt: Date;
39
+ }
40
+
41
+ function assertScope(scope: MemoryScope): void {
42
+ if (!scope || !scope.agentId) {
43
+ throw new Error(
44
+ 'MemoryScope { agentId } is required — agentId must be non-empty'
45
+ );
46
+ }
47
+ }
48
+
49
+ function normalizeCallerId(scope: MemoryScope): string | null {
50
+ const raw = scope.userId;
51
+ if (raw == null || raw === '') return null;
52
+ return String(raw);
53
+ }
54
+
55
+ /** Composite key matcher that collapses null/undefined to a single slot. */
56
+ function sameSlot(
57
+ row: Row,
58
+ agentId: string,
59
+ userId: string | null,
60
+ path: string
61
+ ): boolean {
62
+ return row.agentId === agentId && row.userId === userId && row.path === path;
63
+ }
64
+
65
+ export class MockMemoryBackend implements MemoryBackend {
66
+ readonly kind = 'vector' as const;
67
+ private rows: Row[] = [];
68
+ private seq = 0;
69
+
70
+ async search(
71
+ scope: MemoryScope,
72
+ query: string,
73
+ opts?: MemorySearchOptions
74
+ ): Promise<MemoryEntry[]> {
75
+ assertScope(scope);
76
+ const callerId = normalizeCallerId(scope);
77
+ const q = query.toLowerCase();
78
+ const hits = this.rows
79
+ .filter((r) => r.agentId === scope.agentId)
80
+ .filter((r) => r.userId === null || r.userId === callerId)
81
+ .map((r) => {
82
+ const content = r.content.toLowerCase();
83
+ const tokens = q.split(/\s+/).filter(Boolean);
84
+ const score =
85
+ tokens.length === 0
86
+ ? 0
87
+ : tokens.filter((t) => content.includes(t)).length / tokens.length;
88
+ return { row: r, score };
89
+ })
90
+ .filter((h) => h.score >= (opts?.minScore ?? 0))
91
+ .sort((a, b) => b.score - a.score)
92
+ .slice(0, opts?.maxResults ?? 10);
93
+ return hits.map(
94
+ ({ row, score }): MemoryEntry => ({
95
+ id: row.id,
96
+ path: row.path,
97
+ content: row.content,
98
+ score,
99
+ createdAt: row.createdAt,
100
+ source: 'vector',
101
+ tier: getTierForPath(row.path),
102
+ })
103
+ );
104
+ }
105
+
106
+ async get(
107
+ scope: MemoryScope,
108
+ opts: MemoryGetOptions
109
+ ): Promise<MemoryReadResult | null> {
110
+ assertScope(scope);
111
+ if (!opts.path) return null;
112
+ const callerId = normalizeCallerId(scope);
113
+ const tier = getTierForPath(opts.path);
114
+ const match = this.rows.find((r) => {
115
+ if (r.agentId !== scope.agentId || r.path !== opts.path) return false;
116
+ if (tier === 'agent') return r.userId === null;
117
+ return r.userId === callerId;
118
+ });
119
+ if (!match) return null;
120
+ return { path: opts.path, text: match.content };
121
+ }
122
+
123
+ async append(scope: MemoryScope, input: MemoryAppendInput): Promise<void> {
124
+ assertScope(scope);
125
+ const descriptor = assertWritablePath(input.path, scope);
126
+ if (!input.content.trim()) {
127
+ throw new Error('memory_append content must be non-empty');
128
+ }
129
+ const rowUserId = descriptor.tier === 'agent' ? null : String(scope.userId);
130
+ const provenance = scope.userId != null ? String(scope.userId) : null;
131
+
132
+ const existing = this.rows.find((r) =>
133
+ sameSlot(r, scope.agentId, rowUserId, input.path)
134
+ );
135
+ if (existing) {
136
+ existing.content = `${existing.content.replace(/\s+$/, '')}\n\n${input.content}`;
137
+ existing.lastUserId = provenance;
138
+ existing.updatedAt = new Date();
139
+ return;
140
+ }
141
+ this.rows.push({
142
+ id: String(++this.seq),
143
+ agentId: scope.agentId,
144
+ userId: rowUserId,
145
+ lastUserId: provenance,
146
+ path: input.path,
147
+ content: input.content,
148
+ createdAt: new Date(),
149
+ updatedAt: new Date(),
150
+ });
151
+ }
152
+
153
+ async health(): Promise<MemoryHealth> {
154
+ return { ok: true, backend: 'vector' };
155
+ }
156
+
157
+ /** Test-only introspection. */
158
+ allRows(): readonly Row[] {
159
+ return this.rows;
160
+ }
161
+ }