@illuma-ai/agents 1.1.25 → 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 (272) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +20 -3
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/spawnPath.cjs +104 -0
  4. package/dist/cjs/common/spawnPath.cjs.map +1 -0
  5. package/dist/cjs/graphs/Graph.cjs +87 -31
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/HandoffRegistry.cjs +143 -0
  8. package/dist/cjs/graphs/HandoffRegistry.cjs.map +1 -0
  9. package/dist/cjs/graphs/MultiAgentGraph.cjs +587 -184
  10. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  11. package/dist/cjs/graphs/phases/flushLoop.cjs +214 -0
  12. package/dist/cjs/graphs/phases/flushLoop.cjs.map +1 -0
  13. package/dist/cjs/graphs/phases/memoryFlushPhase.cjs +102 -0
  14. package/dist/cjs/graphs/phases/memoryFlushPhase.cjs.map +1 -0
  15. package/dist/cjs/llm/bedrock/index.cjs +4 -3
  16. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  17. package/dist/cjs/main.cjs +115 -0
  18. package/dist/cjs/main.cjs.map +1 -1
  19. package/dist/cjs/memory/citations.cjs +69 -0
  20. package/dist/cjs/memory/citations.cjs.map +1 -0
  21. package/dist/cjs/memory/compositeBackend.cjs +60 -0
  22. package/dist/cjs/memory/compositeBackend.cjs.map +1 -0
  23. package/dist/cjs/memory/constants.cjs +232 -0
  24. package/dist/cjs/memory/constants.cjs.map +1 -0
  25. package/dist/cjs/memory/embeddings.cjs +151 -0
  26. package/dist/cjs/memory/embeddings.cjs.map +1 -0
  27. package/dist/cjs/memory/factory.cjs +95 -0
  28. package/dist/cjs/memory/factory.cjs.map +1 -0
  29. package/dist/cjs/memory/migrate.cjs +81 -0
  30. package/dist/cjs/memory/migrate.cjs.map +1 -0
  31. package/dist/cjs/memory/mmr.cjs +138 -0
  32. package/dist/cjs/memory/mmr.cjs.map +1 -0
  33. package/dist/cjs/memory/paths.cjs +217 -0
  34. package/dist/cjs/memory/paths.cjs.map +1 -0
  35. package/dist/cjs/memory/pgvectorStore.cjs +225 -0
  36. package/dist/cjs/memory/pgvectorStore.cjs.map +1 -0
  37. package/dist/cjs/memory/recallTracking.cjs +98 -0
  38. package/dist/cjs/memory/recallTracking.cjs.map +1 -0
  39. package/dist/cjs/memory/schema.sql +51 -0
  40. package/dist/cjs/memory/temporalDecay.cjs +118 -0
  41. package/dist/cjs/memory/temporalDecay.cjs.map +1 -0
  42. package/dist/cjs/nodes/ApprovalGateNode.cjs +1 -1
  43. package/dist/cjs/nodes/ApprovalGateNode.cjs.map +1 -1
  44. package/dist/cjs/prompts/memoryFlushPrompt.cjs +49 -0
  45. package/dist/cjs/prompts/memoryFlushPrompt.cjs.map +1 -0
  46. package/dist/cjs/run.cjs +16 -3
  47. package/dist/cjs/run.cjs.map +1 -1
  48. package/dist/cjs/stream.cjs +4 -4
  49. package/dist/cjs/stream.cjs.map +1 -1
  50. package/dist/cjs/tools/AskUser.cjs +6 -1
  51. package/dist/cjs/tools/AskUser.cjs.map +1 -1
  52. package/dist/cjs/tools/BrowserTools.cjs +1 -1
  53. package/dist/cjs/tools/BrowserTools.cjs.map +1 -1
  54. package/dist/cjs/tools/ToolNode.cjs +127 -10
  55. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  56. package/dist/cjs/tools/approval/constants.cjs +2 -2
  57. package/dist/cjs/tools/approval/constants.cjs.map +1 -1
  58. package/dist/cjs/tools/memory/index.cjs +58 -0
  59. package/dist/cjs/tools/memory/index.cjs.map +1 -0
  60. package/dist/cjs/tools/memory/memoryAppendTool.cjs +69 -0
  61. package/dist/cjs/tools/memory/memoryAppendTool.cjs.map +1 -0
  62. package/dist/cjs/tools/memory/memoryGetTool.cjs +49 -0
  63. package/dist/cjs/tools/memory/memoryGetTool.cjs.map +1 -0
  64. package/dist/cjs/tools/memory/memorySearchTool.cjs +65 -0
  65. package/dist/cjs/tools/memory/memorySearchTool.cjs.map +1 -0
  66. package/dist/cjs/tools/memory/shared.cjs +106 -0
  67. package/dist/cjs/tools/memory/shared.cjs.map +1 -0
  68. package/dist/cjs/types/graph.cjs.map +1 -1
  69. package/dist/cjs/utils/childAgentContext.cjs +242 -0
  70. package/dist/cjs/utils/childAgentContext.cjs.map +1 -0
  71. package/dist/cjs/utils/events.cjs +36 -4
  72. package/dist/cjs/utils/events.cjs.map +1 -1
  73. package/dist/cjs/utils/finishReasons.cjs +44 -0
  74. package/dist/cjs/utils/finishReasons.cjs.map +1 -0
  75. package/dist/cjs/utils/llm.cjs.map +1 -1
  76. package/dist/cjs/utils/logging.cjs +34 -0
  77. package/dist/cjs/utils/logging.cjs.map +1 -0
  78. package/dist/cjs/utils/toolCallNormalization.cjs +250 -0
  79. package/dist/cjs/utils/toolCallNormalization.cjs.map +1 -0
  80. package/dist/esm/agents/AgentContext.mjs +20 -3
  81. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  82. package/dist/esm/common/spawnPath.mjs +95 -0
  83. package/dist/esm/common/spawnPath.mjs.map +1 -0
  84. package/dist/esm/graphs/Graph.mjs +87 -31
  85. package/dist/esm/graphs/Graph.mjs.map +1 -1
  86. package/dist/esm/graphs/HandoffRegistry.mjs +141 -0
  87. package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -0
  88. package/dist/esm/graphs/MultiAgentGraph.mjs +587 -184
  89. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  90. package/dist/esm/graphs/phases/flushLoop.mjs +209 -0
  91. package/dist/esm/graphs/phases/flushLoop.mjs.map +1 -0
  92. package/dist/esm/graphs/phases/memoryFlushPhase.mjs +99 -0
  93. package/dist/esm/graphs/phases/memoryFlushPhase.mjs.map +1 -0
  94. package/dist/esm/llm/bedrock/index.mjs +4 -3
  95. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  96. package/dist/esm/main.mjs +21 -0
  97. package/dist/esm/main.mjs.map +1 -1
  98. package/dist/esm/memory/citations.mjs +64 -0
  99. package/dist/esm/memory/citations.mjs.map +1 -0
  100. package/dist/esm/memory/compositeBackend.mjs +58 -0
  101. package/dist/esm/memory/compositeBackend.mjs.map +1 -0
  102. package/dist/esm/memory/constants.mjs +198 -0
  103. package/dist/esm/memory/constants.mjs.map +1 -0
  104. package/dist/esm/memory/embeddings.mjs +148 -0
  105. package/dist/esm/memory/embeddings.mjs.map +1 -0
  106. package/dist/esm/memory/factory.mjs +93 -0
  107. package/dist/esm/memory/factory.mjs.map +1 -0
  108. package/dist/esm/memory/migrate.mjs +78 -0
  109. package/dist/esm/memory/migrate.mjs.map +1 -0
  110. package/dist/esm/memory/mmr.mjs +130 -0
  111. package/dist/esm/memory/mmr.mjs.map +1 -0
  112. package/dist/esm/memory/paths.mjs +207 -0
  113. package/dist/esm/memory/paths.mjs.map +1 -0
  114. package/dist/esm/memory/pgvectorStore.mjs +223 -0
  115. package/dist/esm/memory/pgvectorStore.mjs.map +1 -0
  116. package/dist/esm/memory/recallTracking.mjs +94 -0
  117. package/dist/esm/memory/recallTracking.mjs.map +1 -0
  118. package/dist/esm/memory/schema.sql +51 -0
  119. package/dist/esm/memory/temporalDecay.mjs +110 -0
  120. package/dist/esm/memory/temporalDecay.mjs.map +1 -0
  121. package/dist/esm/nodes/ApprovalGateNode.mjs +1 -1
  122. package/dist/esm/nodes/ApprovalGateNode.mjs.map +1 -1
  123. package/dist/esm/prompts/memoryFlushPrompt.mjs +44 -0
  124. package/dist/esm/prompts/memoryFlushPrompt.mjs.map +1 -0
  125. package/dist/esm/run.mjs +16 -3
  126. package/dist/esm/run.mjs.map +1 -1
  127. package/dist/esm/stream.mjs +4 -4
  128. package/dist/esm/stream.mjs.map +1 -1
  129. package/dist/esm/tools/AskUser.mjs +6 -1
  130. package/dist/esm/tools/AskUser.mjs.map +1 -1
  131. package/dist/esm/tools/BrowserTools.mjs +1 -1
  132. package/dist/esm/tools/BrowserTools.mjs.map +1 -1
  133. package/dist/esm/tools/ToolNode.mjs +128 -11
  134. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  135. package/dist/esm/tools/approval/constants.mjs +2 -2
  136. package/dist/esm/tools/approval/constants.mjs.map +1 -1
  137. package/dist/esm/tools/memory/index.mjs +46 -0
  138. package/dist/esm/tools/memory/index.mjs.map +1 -0
  139. package/dist/esm/tools/memory/memoryAppendTool.mjs +67 -0
  140. package/dist/esm/tools/memory/memoryAppendTool.mjs.map +1 -0
  141. package/dist/esm/tools/memory/memoryGetTool.mjs +47 -0
  142. package/dist/esm/tools/memory/memoryGetTool.mjs.map +1 -0
  143. package/dist/esm/tools/memory/memorySearchTool.mjs +63 -0
  144. package/dist/esm/tools/memory/memorySearchTool.mjs.map +1 -0
  145. package/dist/esm/tools/memory/shared.mjs +98 -0
  146. package/dist/esm/tools/memory/shared.mjs.map +1 -0
  147. package/dist/esm/types/graph.mjs.map +1 -1
  148. package/dist/esm/utils/childAgentContext.mjs +237 -0
  149. package/dist/esm/utils/childAgentContext.mjs.map +1 -0
  150. package/dist/esm/utils/events.mjs +36 -5
  151. package/dist/esm/utils/events.mjs.map +1 -1
  152. package/dist/esm/utils/finishReasons.mjs +41 -0
  153. package/dist/esm/utils/finishReasons.mjs.map +1 -0
  154. package/dist/esm/utils/llm.mjs.map +1 -1
  155. package/dist/esm/utils/logging.mjs +31 -0
  156. package/dist/esm/utils/logging.mjs.map +1 -0
  157. package/dist/esm/utils/toolCallNormalization.mjs +247 -0
  158. package/dist/esm/utils/toolCallNormalization.mjs.map +1 -0
  159. package/dist/types/common/index.d.ts +1 -0
  160. package/dist/types/common/spawnPath.d.ts +59 -0
  161. package/dist/types/graphs/HandoffRegistry.d.ts +97 -0
  162. package/dist/types/graphs/MultiAgentGraph.d.ts +58 -18
  163. package/dist/types/graphs/index.d.ts +1 -0
  164. package/dist/types/graphs/phases/flushLoop.d.ts +106 -0
  165. package/dist/types/graphs/phases/memoryFlushPhase.d.ts +100 -0
  166. package/dist/types/index.d.ts +7 -0
  167. package/dist/types/memory/__tests__/mockBackend.d.ts +40 -0
  168. package/dist/types/memory/citations.d.ts +39 -0
  169. package/dist/types/memory/compositeBackend.d.ts +30 -0
  170. package/dist/types/memory/constants.d.ts +121 -0
  171. package/dist/types/memory/embeddings.d.ts +15 -0
  172. package/dist/types/memory/factory.d.ts +23 -0
  173. package/dist/types/memory/index.d.ts +21 -0
  174. package/dist/types/memory/migrate.d.ts +14 -0
  175. package/dist/types/memory/mmr.d.ts +50 -0
  176. package/dist/types/memory/paths.d.ts +107 -0
  177. package/dist/types/memory/pgvectorStore.d.ts +56 -0
  178. package/dist/types/memory/recallTracking.d.ts +30 -0
  179. package/dist/types/memory/temporalDecay.d.ts +53 -0
  180. package/dist/types/memory/types.d.ts +182 -0
  181. package/dist/types/prompts/memoryFlushPrompt.d.ts +54 -0
  182. package/dist/types/run.d.ts +1 -0
  183. package/dist/types/tools/AskUser.d.ts +1 -1
  184. package/dist/types/tools/BrowserTools.d.ts +2 -2
  185. package/dist/types/tools/approval/constants.d.ts +2 -2
  186. package/dist/types/tools/memory/index.d.ts +39 -0
  187. package/dist/types/tools/memory/memoryAppendTool.d.ts +27 -0
  188. package/dist/types/tools/memory/memoryGetTool.d.ts +22 -0
  189. package/dist/types/tools/memory/memorySearchTool.d.ts +22 -0
  190. package/dist/types/tools/memory/shared.d.ts +106 -0
  191. package/dist/types/types/graph.d.ts +16 -3
  192. package/dist/types/utils/childAgentContext.d.ts +99 -0
  193. package/dist/types/utils/events.d.ts +21 -0
  194. package/dist/types/utils/finishReasons.d.ts +32 -0
  195. package/dist/types/utils/logging.d.ts +2 -0
  196. package/dist/types/utils/toolCallNormalization.d.ts +44 -0
  197. package/package.json +6 -4
  198. package/src/agents/AgentContext.ts +26 -3
  199. package/src/common/__tests__/enum.test.ts +4 -2
  200. package/src/common/__tests__/spawnPath.test.ts +110 -0
  201. package/src/common/index.ts +1 -0
  202. package/src/common/spawnPath.ts +101 -0
  203. package/src/graphs/Graph.ts +94 -43
  204. package/src/graphs/HandoffRegistry.ts +199 -0
  205. package/src/graphs/MultiAgentGraph.ts +694 -226
  206. package/src/graphs/__tests__/HandoffRegistry.test.ts +410 -0
  207. package/src/graphs/__tests__/multi-agent-delegate.test.ts +61 -16
  208. package/src/graphs/__tests__/multi-agent-edges.test.ts +4 -2
  209. package/src/graphs/__tests__/multi-agent-nested-subgraph.test.ts +221 -0
  210. package/src/graphs/__tests__/structured-output.integration.test.ts +212 -118
  211. package/src/graphs/contextManagement.e2e.test.ts +1 -1
  212. package/src/graphs/index.ts +1 -0
  213. package/src/graphs/phases/__tests__/flushLoop.test.ts +264 -0
  214. package/src/graphs/phases/__tests__/memoryFlushPhase.test.ts +37 -0
  215. package/src/graphs/phases/__tests__/runMemoryFlush.test.ts +150 -0
  216. package/src/graphs/phases/flushLoop.ts +303 -0
  217. package/src/graphs/phases/memoryFlushPhase.ts +209 -0
  218. package/src/index.ts +30 -1
  219. package/src/llm/bedrock/index.ts +4 -5
  220. package/src/memory/__tests__/citations.test.ts +61 -0
  221. package/src/memory/__tests__/compositeBackend.test.ts +79 -0
  222. package/src/memory/__tests__/isolation.test.ts +206 -0
  223. package/src/memory/__tests__/mmr.test.ts +148 -0
  224. package/src/memory/__tests__/mockBackend.ts +161 -0
  225. package/src/memory/__tests__/paths.test.ts +168 -0
  226. package/src/memory/__tests__/recallTracking.test.ts +96 -0
  227. package/src/memory/__tests__/temporalDecay.test.ts +151 -0
  228. package/src/memory/citations.ts +80 -0
  229. package/src/memory/compositeBackend.ts +99 -0
  230. package/src/memory/constants.ts +229 -0
  231. package/src/memory/embeddings.ts +188 -0
  232. package/src/memory/factory.ts +111 -0
  233. package/src/memory/index.ts +46 -0
  234. package/src/memory/migrate.ts +116 -0
  235. package/src/memory/mmr.ts +161 -0
  236. package/src/memory/paths.ts +258 -0
  237. package/src/memory/pgvectorStore.ts +324 -0
  238. package/src/memory/recallTracking.ts +127 -0
  239. package/src/memory/schema.sql +51 -0
  240. package/src/memory/temporalDecay.ts +134 -0
  241. package/src/memory/types.ts +185 -0
  242. package/src/nodes/ApprovalGateNode.ts +4 -10
  243. package/src/nodes/__tests__/ApprovalGateNode.test.ts +11 -20
  244. package/src/prompts/memoryFlushPrompt.ts +78 -0
  245. package/src/run.ts +17 -6
  246. package/src/scripts/test-bedrock-handoff-autonomous.ts +56 -20
  247. package/src/specs/agent-handoffs-bedrock.integration.test.ts +8 -5
  248. package/src/specs/agent-handoffs.test.ts +8 -2
  249. package/src/stream.ts +4 -6
  250. package/src/tools/AskUser.ts +7 -2
  251. package/src/tools/BrowserTools.ts +3 -5
  252. package/src/tools/ToolNode.ts +150 -13
  253. package/src/tools/__tests__/ToolApproval.test.ts +22 -9
  254. package/src/tools/approval/__tests__/constants.test.ts +4 -4
  255. package/src/tools/approval/constants.ts +2 -2
  256. package/src/tools/memory/__tests__/memoryTools.test.ts +205 -0
  257. package/src/tools/memory/index.ts +96 -0
  258. package/src/tools/memory/memoryAppendTool.ts +101 -0
  259. package/src/tools/memory/memoryGetTool.ts +53 -0
  260. package/src/tools/memory/memorySearchTool.ts +80 -0
  261. package/src/tools/memory/shared.ts +169 -0
  262. package/src/tools/search/search.test.ts +6 -1
  263. package/src/types/graph.ts +16 -3
  264. package/src/utils/__tests__/childAgentContext.test.ts +217 -0
  265. package/src/utils/__tests__/finishReasons.test.ts +55 -0
  266. package/src/utils/__tests__/toolCallNormalization.test.ts +181 -0
  267. package/src/utils/childAgentContext.ts +259 -0
  268. package/src/utils/events.ts +37 -4
  269. package/src/utils/finishReasons.ts +40 -0
  270. package/src/utils/llm.ts +0 -1
  271. package/src/utils/logging.ts +45 -8
  272. package/src/utils/toolCallNormalization.ts +271 -0
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Unit tests for the 8-path whitelist and tier utilities.
3
+ *
4
+ * These tests pin the vocabulary in place — if someone renames a
5
+ * canonical path without updating routes/UI/prompts, the build breaks
6
+ * here first instead of at runtime with cryptic whitelist rejections.
7
+ */
8
+ import {
9
+ MEMORY_AGENT_PATHS,
10
+ MEMORY_USER_PATHS,
11
+ MEMORY_ALL_PATHS,
12
+ MEMORY_WRITABLE_PATHS,
13
+ assertWritablePath,
14
+ getTierForPath,
15
+ getPathDescriptor,
16
+ getWritablePathsForScope,
17
+ renderPathsRubric,
18
+ } from '../paths';
19
+
20
+ describe('memory/paths', () => {
21
+ describe('whitelist shape', () => {
22
+ it('has exactly 4 agent-tier + 4 user-tier paths = 8 total', () => {
23
+ expect(MEMORY_AGENT_PATHS).toHaveLength(4);
24
+ expect(MEMORY_USER_PATHS).toHaveLength(4);
25
+ expect(MEMORY_ALL_PATHS).toHaveLength(8);
26
+ expect(MEMORY_WRITABLE_PATHS.size).toBe(8);
27
+ });
28
+
29
+ it('every agent-tier path starts with memory/agent/', () => {
30
+ for (const p of MEMORY_AGENT_PATHS) {
31
+ expect(p.path).toMatch(/^memory\/agent\//);
32
+ expect(p.tier).toBe('agent');
33
+ }
34
+ });
35
+
36
+ it('every user-tier path starts with memory/user/', () => {
37
+ for (const p of MEMORY_USER_PATHS) {
38
+ expect(p.path).toMatch(/^memory\/user\//);
39
+ expect(p.tier).toBe('user');
40
+ }
41
+ });
42
+
43
+ it('every descriptor has a non-empty label and description', () => {
44
+ for (const p of MEMORY_ALL_PATHS) {
45
+ expect(p.label.length).toBeGreaterThan(0);
46
+ expect(p.description.length).toBeGreaterThan(20);
47
+ }
48
+ });
49
+
50
+ it('paths are unique', () => {
51
+ const paths = MEMORY_ALL_PATHS.map((p) => p.path);
52
+ expect(new Set(paths).size).toBe(paths.length);
53
+ });
54
+ });
55
+
56
+ describe('getTierForPath', () => {
57
+ it('returns agent for memory/agent/* paths', () => {
58
+ expect(getTierForPath('memory/agent/playbook.md')).toBe('agent');
59
+ expect(getTierForPath('memory/agent/pitfalls.md')).toBe('agent');
60
+ });
61
+
62
+ it('returns user for memory/user/* paths', () => {
63
+ expect(getTierForPath('memory/user/profile.md')).toBe('user');
64
+ expect(getTierForPath('memory/user/preferences.md')).toBe('user');
65
+ });
66
+
67
+ it('falls back to prefix inspection for unknown paths', () => {
68
+ expect(getTierForPath('memory/user/legacy.md')).toBe('user');
69
+ expect(getTierForPath('memory/2026-04-14.md')).toBe('agent');
70
+ });
71
+ });
72
+
73
+ describe('getPathDescriptor', () => {
74
+ it('returns the descriptor for a canonical path', () => {
75
+ const d = getPathDescriptor('memory/agent/playbook.md');
76
+ expect(d).toBeDefined();
77
+ expect(d!.tier).toBe('agent');
78
+ expect(d!.label).toBe('Playbook');
79
+ });
80
+
81
+ it('returns undefined for non-whitelisted paths', () => {
82
+ expect(getPathDescriptor('memory/random.md')).toBeUndefined();
83
+ });
84
+ });
85
+
86
+ describe('getWritablePathsForScope', () => {
87
+ it('returns all 8 when scope has a userId', () => {
88
+ const paths = getWritablePathsForScope({ userId: 'alice' });
89
+ expect(paths).toHaveLength(8);
90
+ });
91
+
92
+ it('returns only 4 agent-tier paths when userId is missing', () => {
93
+ const paths = getWritablePathsForScope({ userId: null });
94
+ expect(paths).toHaveLength(4);
95
+ expect(paths.every((p) => p.tier === 'agent')).toBe(true);
96
+ });
97
+
98
+ it('treats empty-string userId as isolated', () => {
99
+ const paths = getWritablePathsForScope({ userId: '' });
100
+ expect(paths).toHaveLength(4);
101
+ });
102
+ });
103
+
104
+ describe('assertWritablePath', () => {
105
+ const scoped = { userId: 'alice' };
106
+ const isolated = { userId: null };
107
+
108
+ it('accepts a valid agent-tier write from any scope', () => {
109
+ expect(() =>
110
+ assertWritablePath('memory/agent/playbook.md', scoped)
111
+ ).not.toThrow();
112
+ expect(() =>
113
+ assertWritablePath('memory/agent/playbook.md', isolated)
114
+ ).not.toThrow();
115
+ });
116
+
117
+ it('accepts a valid user-tier write from a scoped caller', () => {
118
+ expect(() =>
119
+ assertWritablePath('memory/user/preferences.md', scoped)
120
+ ).not.toThrow();
121
+ });
122
+
123
+ it('rejects a user-tier write from an isolated caller', () => {
124
+ expect(() =>
125
+ assertWritablePath('memory/user/preferences.md', isolated)
126
+ ).toThrow(/requires a caller userId/);
127
+ });
128
+
129
+ it('rejects paths without the memory/ prefix', () => {
130
+ expect(() => assertWritablePath('foo.md', scoped)).toThrow(
131
+ /must start with "memory\//
132
+ );
133
+ });
134
+
135
+ it('rejects non-whitelisted paths inside memory/', () => {
136
+ expect(() =>
137
+ assertWritablePath('memory/random-notes.md', scoped)
138
+ ).toThrow(/not on the canonical whitelist/);
139
+ });
140
+
141
+ it('rejects legacy date-keyed paths', () => {
142
+ expect(() => assertWritablePath('memory/2026-04-14.md', scoped)).toThrow(
143
+ /not on the canonical whitelist/
144
+ );
145
+ });
146
+ });
147
+
148
+ describe('renderPathsRubric', () => {
149
+ it('renders every writable path for a scoped caller', () => {
150
+ const text = renderPathsRubric({ userId: 'alice' });
151
+ for (const p of MEMORY_ALL_PATHS) {
152
+ expect(text).toContain(p.path);
153
+ }
154
+ expect(text).toContain('[Playbook, shared]');
155
+ expect(text).toContain('[Profile, private]');
156
+ });
157
+
158
+ it('omits user-tier paths for an isolated caller', () => {
159
+ const text = renderPathsRubric({ userId: null });
160
+ for (const p of MEMORY_AGENT_PATHS) {
161
+ expect(text).toContain(p.path);
162
+ }
163
+ for (const p of MEMORY_USER_PATHS) {
164
+ expect(text).not.toContain(p.path);
165
+ }
166
+ });
167
+ });
168
+ });
@@ -0,0 +1,96 @@
1
+ import {
2
+ PgvectorRecallTracker,
3
+ NullRecallTracker,
4
+ RECALL_TABLE,
5
+ } from '../recallTracking';
6
+ import type { Pool } from 'pg';
7
+
8
+ interface FakeQuery {
9
+ text: string;
10
+ values?: unknown[];
11
+ }
12
+
13
+ function makeFakePool(): Pool & { calls: FakeQuery[] } {
14
+ const calls: FakeQuery[] = [];
15
+ const p = {
16
+ calls,
17
+ async query(text: string, values?: unknown[]) {
18
+ calls.push({ text, values });
19
+ return { rows: [] };
20
+ },
21
+ };
22
+ return p as unknown as Pool & { calls: FakeQuery[] };
23
+ }
24
+
25
+ describe('PgvectorRecallTracker.migrate', () => {
26
+ it('creates table + indexes idempotently', async () => {
27
+ const pool = makeFakePool();
28
+ const t = new PgvectorRecallTracker(pool);
29
+ await t.migrate();
30
+ const joined = pool.calls.map((c) => c.text).join(' ');
31
+ expect(joined).toContain(`CREATE TABLE IF NOT EXISTS ${RECALL_TABLE}`);
32
+ expect(joined).toContain(`${RECALL_TABLE}_agent_day_idx`);
33
+ expect(joined).toContain(`${RECALL_TABLE}_memory_idx`);
34
+ expect(joined).toContain(`${RECALL_TABLE}_dedupe_idx`);
35
+ });
36
+ });
37
+
38
+ describe('PgvectorRecallTracker.record', () => {
39
+ it('no-ops on empty hits / missing agent / empty query', async () => {
40
+ const pool = makeFakePool();
41
+ const t = new PgvectorRecallTracker(pool);
42
+ await t.record({ agentId: 'a', query: 'q', hits: [] });
43
+ await t.record({
44
+ agentId: '',
45
+ query: 'q',
46
+ hits: [{ id: '1', path: 'p', score: 0.5 }],
47
+ });
48
+ await t.record({
49
+ agentId: 'a',
50
+ query: ' ',
51
+ hits: [{ id: '1', path: 'p', score: 0.5 }],
52
+ });
53
+ expect(pool.calls).toHaveLength(0);
54
+ });
55
+
56
+ it('emits a single upsert per record call with one row per hit', async () => {
57
+ const pool = makeFakePool();
58
+ const t = new PgvectorRecallTracker(pool);
59
+ await t.record({
60
+ agentId: 'poc-sales',
61
+ query: 'pricing tiers',
62
+ hits: [
63
+ { id: '1', path: 'memory/2026-04-13.md', score: 0.9 },
64
+ { id: '2', path: 'memory/2026-04-12.md', score: 0.8 },
65
+ ],
66
+ });
67
+ expect(pool.calls).toHaveLength(1);
68
+ const sql = pool.calls[0].text;
69
+ expect(sql).toContain('INSERT INTO');
70
+ expect(sql).toContain('ON CONFLICT');
71
+ // Two value groups → 16 bound parameters (7 per hit + clause).
72
+ expect(pool.calls[0].values).toHaveLength(14);
73
+ });
74
+
75
+ it('dedupe per (agent, memory, query_hash, day_bucket) via ON CONFLICT DO UPDATE', async () => {
76
+ const pool = makeFakePool();
77
+ const t = new PgvectorRecallTracker(pool);
78
+ await t.record({
79
+ agentId: 'a',
80
+ query: 'q',
81
+ hits: [{ id: '1', path: 'p', score: 0.5 }],
82
+ });
83
+ expect(pool.calls[0].text).toMatch(
84
+ /ON CONFLICT \(agent_id, memory_id, query_hash, day_bucket\)/
85
+ );
86
+ expect(pool.calls[0].text).toMatch(/GREATEST/);
87
+ });
88
+ });
89
+
90
+ describe('NullRecallTracker', () => {
91
+ it('no-ops without errors', async () => {
92
+ const t = new NullRecallTracker();
93
+ await expect(t.migrate()).resolves.toBeUndefined();
94
+ await expect(t.record()).resolves.toBeUndefined();
95
+ });
96
+ });
@@ -0,0 +1,151 @@
1
+ import {
2
+ applyTemporalDecayToHits,
3
+ applyTemporalDecayToScore,
4
+ calculateTemporalDecayMultiplier,
5
+ DEFAULT_TEMPORAL_DECAY_CONFIG,
6
+ isEvergreenMemoryPath,
7
+ parseMemoryDateFromPath,
8
+ } from '../temporalDecay';
9
+
10
+ const DAY_MS = 24 * 60 * 60 * 1000;
11
+
12
+ describe('calculateTemporalDecayMultiplier', () => {
13
+ it('is 1 at age=0', () => {
14
+ expect(
15
+ calculateTemporalDecayMultiplier({ ageInDays: 0, halfLifeDays: 30 })
16
+ ).toBe(1);
17
+ });
18
+
19
+ it('is exactly 0.5 at half-life', () => {
20
+ expect(
21
+ calculateTemporalDecayMultiplier({ ageInDays: 30, halfLifeDays: 30 })
22
+ ).toBeCloseTo(0.5);
23
+ });
24
+
25
+ it('returns 1 for non-positive half life (no decay)', () => {
26
+ expect(
27
+ calculateTemporalDecayMultiplier({ ageInDays: 100, halfLifeDays: 0 })
28
+ ).toBe(1);
29
+ expect(
30
+ calculateTemporalDecayMultiplier({ ageInDays: 100, halfLifeDays: -1 })
31
+ ).toBe(1);
32
+ });
33
+
34
+ it('clamps negative age to 0', () => {
35
+ expect(
36
+ calculateTemporalDecayMultiplier({ ageInDays: -5, halfLifeDays: 30 })
37
+ ).toBe(1);
38
+ });
39
+ });
40
+
41
+ describe('applyTemporalDecayToScore', () => {
42
+ it('halves score at half-life', () => {
43
+ expect(
44
+ applyTemporalDecayToScore({ score: 0.8, ageInDays: 30, halfLifeDays: 30 })
45
+ ).toBeCloseTo(0.4);
46
+ });
47
+ });
48
+
49
+ describe('parseMemoryDateFromPath', () => {
50
+ it('parses dated memory files', () => {
51
+ const d = parseMemoryDateFromPath('memory/2026-04-13.md');
52
+ expect(d).not.toBeNull();
53
+ expect(d!.getUTCFullYear()).toBe(2026);
54
+ expect(d!.getUTCMonth()).toBe(3);
55
+ expect(d!.getUTCDate()).toBe(13);
56
+ });
57
+
58
+ it('rejects invalid dates', () => {
59
+ expect(parseMemoryDateFromPath('memory/2026-02-30.md')).toBeNull();
60
+ expect(parseMemoryDateFromPath('memory/notes.md')).toBeNull();
61
+ expect(parseMemoryDateFromPath('MEMORY.md')).toBeNull();
62
+ });
63
+
64
+ it('handles windows-style separators', () => {
65
+ expect(parseMemoryDateFromPath('memory\\2026-04-13.md')).not.toBeNull();
66
+ });
67
+ });
68
+
69
+ describe('isEvergreenMemoryPath', () => {
70
+ it('treats MEMORY.md as evergreen', () => {
71
+ expect(isEvergreenMemoryPath('MEMORY.md')).toBe(true);
72
+ expect(isEvergreenMemoryPath('memory.md')).toBe(true);
73
+ });
74
+
75
+ it('treats dated files as non-evergreen', () => {
76
+ expect(isEvergreenMemoryPath('memory/2026-04-13.md')).toBe(false);
77
+ });
78
+
79
+ it('treats topic files as evergreen', () => {
80
+ expect(isEvergreenMemoryPath('memory/topics.md')).toBe(true);
81
+ expect(isEvergreenMemoryPath('memory/architecture.md')).toBe(true);
82
+ });
83
+
84
+ it('treats non-memory paths as non-evergreen', () => {
85
+ expect(isEvergreenMemoryPath('notes.md')).toBe(false);
86
+ });
87
+ });
88
+
89
+ describe('applyTemporalDecayToHits', () => {
90
+ const nowMs = Date.UTC(2026, 3, 13); // 2026-04-13
91
+
92
+ it('is a no-op when disabled', () => {
93
+ const hits = [
94
+ { path: 'memory/2026-03-14.md', score: 0.9, createdAt: new Date(nowMs) },
95
+ ];
96
+ expect(applyTemporalDecayToHits(hits, { enabled: false }, nowMs)).toEqual(
97
+ hits
98
+ );
99
+ });
100
+
101
+ it('decays dated files by date in path', () => {
102
+ const hits = [
103
+ { path: 'memory/2026-03-14.md', score: 1.0, createdAt: new Date(nowMs) }, // 30 days old
104
+ ];
105
+ const out = applyTemporalDecayToHits(
106
+ hits,
107
+ { enabled: true, halfLifeDays: 30 },
108
+ nowMs
109
+ );
110
+ expect(out[0].score).toBeCloseTo(0.5, 2);
111
+ });
112
+
113
+ it('does NOT decay evergreen files', () => {
114
+ const hits = [
115
+ {
116
+ path: 'memory/architecture.md',
117
+ score: 0.9,
118
+ createdAt: new Date(nowMs - 100 * DAY_MS),
119
+ },
120
+ ];
121
+ const out = applyTemporalDecayToHits(
122
+ hits,
123
+ { enabled: true, halfLifeDays: 30 },
124
+ nowMs
125
+ );
126
+ expect(out[0].score).toBe(0.9);
127
+ });
128
+
129
+ it('falls back to createdAt for non-dated non-evergreen paths', () => {
130
+ const hits = [
131
+ {
132
+ path: 'notes.md',
133
+ score: 1.0,
134
+ createdAt: new Date(nowMs - 30 * DAY_MS),
135
+ },
136
+ ];
137
+ const out = applyTemporalDecayToHits(
138
+ hits,
139
+ { enabled: true, halfLifeDays: 30 },
140
+ nowMs
141
+ );
142
+ expect(out[0].score).toBeCloseTo(0.5, 2);
143
+ });
144
+
145
+ it('default config is disabled with 30 day half-life', () => {
146
+ expect(DEFAULT_TEMPORAL_DECAY_CONFIG).toEqual({
147
+ enabled: false,
148
+ halfLifeDays: 30,
149
+ });
150
+ });
151
+ });
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Citation decoration — Phase 2.
3
+ *
4
+ * Ported from upstream `extensions/memory-core/src/tools.citations.ts`.
5
+ * Decorates memory_search hits with `[path#L{start}-L{end}]` markers so
6
+ * the model can attribute claims back to specific memory files when it
7
+ * uses them in its answer.
8
+ *
9
+ * Since our backend stores files as single rows (not line-chunked), we
10
+ * compute line ranges from the returned content block on the fly:
11
+ * - `startLine` = 1 (line 1 of the file)
12
+ * - `endLine` = total number of lines in the block
13
+ * This matches upstream's output format exactly while keeping the pg
14
+ * schema chunk-free.
15
+ */
16
+
17
+ export type MemoryCitationsMode = 'on' | 'off' | 'auto';
18
+
19
+ export const DEFAULT_CITATIONS_MODE: MemoryCitationsMode = 'auto';
20
+
21
+ export function resolveMemoryCitationsMode(
22
+ raw: string | undefined | null
23
+ ): MemoryCitationsMode {
24
+ if (raw === 'on' || raw === 'off' || raw === 'auto') return raw;
25
+ return DEFAULT_CITATIONS_MODE;
26
+ }
27
+
28
+ export interface CitationCandidate {
29
+ path: string;
30
+ content: string;
31
+ startLine?: number;
32
+ endLine?: number;
33
+ citation?: string;
34
+ }
35
+
36
+ function countLines(text: string): number {
37
+ if (!text) return 1;
38
+ return Math.max(1, text.split('\n').length);
39
+ }
40
+
41
+ function formatCitation(
42
+ path: string,
43
+ startLine: number,
44
+ endLine: number
45
+ ): string {
46
+ if (startLine === endLine) return `${path}#L${startLine}`;
47
+ return `${path}#L${startLine}-L${endLine}`;
48
+ }
49
+
50
+ /**
51
+ * Decorate each hit with a citation marker. Mirrors upstream's behavior:
52
+ * appends `\n\nSource: <citation>` to the content and sets `citation`.
53
+ * When `include=false`, clears any existing citation field.
54
+ */
55
+ export function decorateCitations<T extends CitationCandidate>(
56
+ hits: T[],
57
+ include: boolean
58
+ ): T[] {
59
+ if (!include) return hits.map((h) => ({ ...h, citation: undefined }));
60
+ return hits.map((h) => {
61
+ const start = Math.max(1, Math.floor(h.startLine ?? 1));
62
+ const end = Math.max(start, Math.floor(h.endLine ?? countLines(h.content)));
63
+ const citation = formatCitation(h.path, start, end);
64
+ const content = `${(h.content ?? '').trimEnd()}\n\nSource: ${citation}`;
65
+ return { ...h, citation, content, startLine: start, endLine: end };
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Whether citations should be emitted for this call.
71
+ *
72
+ * Upstream keys `auto` off the session type (direct/group/channel). In
73
+ * Phase 1 we only have direct chat, so `auto` => `on`. Callers that
74
+ * later distinguish session types can pass `mode` explicitly.
75
+ */
76
+ export function shouldIncludeCitations(mode: MemoryCitationsMode): boolean {
77
+ if (mode === 'on') return true;
78
+ if (mode === 'off') return false;
79
+ return true;
80
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Composite memory backend — the architectural seam for a future graph layer.
3
+ *
4
+ * Today, memory is pgvector only. Tomorrow, a Graphiti or Neo4j-agent-memory
5
+ * adapter can implement the same {@link MemoryBackend} interface and be
6
+ * composed with the vector store here — without touching the tool
7
+ * definitions, prompts, or host wiring.
8
+ *
9
+ * Fan-out strategy (simple on purpose for Phase 1):
10
+ * - `search`: query every backend in parallel, merge, dedupe by id, sort by
11
+ * score, cap at maxResults. Graph hits and vector hits are interleaved by
12
+ * score — the LLM sees one ranked list.
13
+ * - `get`: the vector store owns file paths; graph stores don't. First
14
+ * backend that returns a non-null result wins. In practice this will almost
15
+ * always be the vector store, since append paths live there.
16
+ * - `append`: fan out to every backend. Vector store persists the note,
17
+ * graph backend runs its own extraction pipeline. An append failure in any
18
+ * single backend is surfaced — we fail loud rather than silently dropping
19
+ * writes.
20
+ */
21
+ import type {
22
+ MemoryAppendInput,
23
+ MemoryBackend,
24
+ MemoryEntry,
25
+ MemoryGetOptions,
26
+ MemoryHealth,
27
+ MemoryReadResult,
28
+ MemoryScope,
29
+ MemorySearchOptions,
30
+ } from './types';
31
+
32
+ export class CompositeMemoryBackend implements MemoryBackend {
33
+ readonly kind = 'composite' as const;
34
+ private readonly backends: MemoryBackend[];
35
+
36
+ constructor(backends: MemoryBackend[]) {
37
+ if (backends.length === 0) {
38
+ throw new Error('CompositeMemoryBackend requires at least one backend');
39
+ }
40
+ this.backends = backends;
41
+ }
42
+
43
+ async search(
44
+ scope: MemoryScope,
45
+ query: string,
46
+ opts?: MemorySearchOptions
47
+ ): Promise<MemoryEntry[]> {
48
+ const perBackend = await Promise.all(
49
+ this.backends.map((backend) => backend.search(scope, query, opts))
50
+ );
51
+ const merged = new Map<string, MemoryEntry>();
52
+ for (const entries of perBackend) {
53
+ for (const entry of entries) {
54
+ const key = `${entry.source ?? 'unknown'}:${entry.id}`;
55
+ const existing = merged.get(key);
56
+ if (!existing || entry.score > existing.score) {
57
+ merged.set(key, entry);
58
+ }
59
+ }
60
+ }
61
+ const limit = Math.max(1, opts?.maxResults ?? 10);
62
+ return Array.from(merged.values())
63
+ .sort((a, b) => b.score - a.score)
64
+ .slice(0, limit);
65
+ }
66
+
67
+ async get(
68
+ scope: MemoryScope,
69
+ opts: MemoryGetOptions
70
+ ): Promise<MemoryReadResult | null> {
71
+ for (const backend of this.backends) {
72
+ const result = await backend.get(scope, opts);
73
+ if (result) return result;
74
+ }
75
+ return null;
76
+ }
77
+
78
+ async append(scope: MemoryScope, input: MemoryAppendInput): Promise<void> {
79
+ // Serial fan-out — a failure in an earlier backend means we stop and
80
+ // bubble. We do NOT want a partial write where the vector row landed but
81
+ // the graph backend quietly dropped the note.
82
+ for (const backend of this.backends) {
83
+ await backend.append(scope, input);
84
+ }
85
+ }
86
+
87
+ async health(): Promise<MemoryHealth> {
88
+ const healths = await Promise.all(this.backends.map((b) => b.health()));
89
+ const failed = healths.find((h) => !h.ok);
90
+ if (failed) {
91
+ return {
92
+ ok: false,
93
+ backend: 'composite',
94
+ error: `${failed.backend}: ${failed.error ?? 'unhealthy'}`,
95
+ };
96
+ }
97
+ return { ok: true, backend: 'composite' };
98
+ }
99
+ }