@illuma-ai/agents 1.1.28 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) 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 +89 -45
  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 +117 -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/errors.cjs +113 -0
  69. package/dist/cjs/utils/errors.cjs.map +1 -0
  70. package/dist/cjs/utils/events.cjs +36 -7
  71. package/dist/cjs/utils/events.cjs.map +1 -1
  72. package/dist/cjs/utils/finishReasons.cjs +44 -0
  73. package/dist/cjs/utils/finishReasons.cjs.map +1 -0
  74. package/dist/cjs/utils/llm.cjs.map +1 -1
  75. package/dist/cjs/utils/logging.cjs +34 -0
  76. package/dist/cjs/utils/logging.cjs.map +1 -0
  77. package/dist/cjs/utils/toolCallNormalization.cjs +250 -0
  78. package/dist/cjs/utils/toolCallNormalization.cjs.map +1 -0
  79. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  80. package/dist/esm/common/spawnPath.mjs +95 -0
  81. package/dist/esm/common/spawnPath.mjs.map +1 -0
  82. package/dist/esm/graphs/Graph.mjs +89 -45
  83. package/dist/esm/graphs/Graph.mjs.map +1 -1
  84. package/dist/esm/graphs/HandoffRegistry.mjs +47 -8
  85. package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -1
  86. package/dist/esm/graphs/MultiAgentGraph.mjs +493 -267
  87. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  88. package/dist/esm/graphs/phases/flushLoop.mjs +209 -0
  89. package/dist/esm/graphs/phases/flushLoop.mjs.map +1 -0
  90. package/dist/esm/graphs/phases/memoryFlushPhase.mjs +99 -0
  91. package/dist/esm/graphs/phases/memoryFlushPhase.mjs.map +1 -0
  92. package/dist/esm/llm/bedrock/index.mjs +4 -3
  93. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  94. package/dist/esm/main.mjs +21 -0
  95. package/dist/esm/main.mjs.map +1 -1
  96. package/dist/esm/memory/citations.mjs +64 -0
  97. package/dist/esm/memory/citations.mjs.map +1 -0
  98. package/dist/esm/memory/compositeBackend.mjs +58 -0
  99. package/dist/esm/memory/compositeBackend.mjs.map +1 -0
  100. package/dist/esm/memory/constants.mjs +198 -0
  101. package/dist/esm/memory/constants.mjs.map +1 -0
  102. package/dist/esm/memory/embeddings.mjs +148 -0
  103. package/dist/esm/memory/embeddings.mjs.map +1 -0
  104. package/dist/esm/memory/factory.mjs +93 -0
  105. package/dist/esm/memory/factory.mjs.map +1 -0
  106. package/dist/esm/memory/migrate.mjs +78 -0
  107. package/dist/esm/memory/migrate.mjs.map +1 -0
  108. package/dist/esm/memory/mmr.mjs +130 -0
  109. package/dist/esm/memory/mmr.mjs.map +1 -0
  110. package/dist/esm/memory/paths.mjs +207 -0
  111. package/dist/esm/memory/paths.mjs.map +1 -0
  112. package/dist/esm/memory/pgvectorStore.mjs +223 -0
  113. package/dist/esm/memory/pgvectorStore.mjs.map +1 -0
  114. package/dist/esm/memory/recallTracking.mjs +94 -0
  115. package/dist/esm/memory/recallTracking.mjs.map +1 -0
  116. package/dist/esm/memory/schema.sql +51 -0
  117. package/dist/esm/memory/temporalDecay.mjs +110 -0
  118. package/dist/esm/memory/temporalDecay.mjs.map +1 -0
  119. package/dist/esm/nodes/ApprovalGateNode.mjs +1 -1
  120. package/dist/esm/nodes/ApprovalGateNode.mjs.map +1 -1
  121. package/dist/esm/prompts/memoryFlushPrompt.mjs +44 -0
  122. package/dist/esm/prompts/memoryFlushPrompt.mjs.map +1 -0
  123. package/dist/esm/run.mjs +16 -3
  124. package/dist/esm/run.mjs.map +1 -1
  125. package/dist/esm/tools/AskUser.mjs +6 -1
  126. package/dist/esm/tools/AskUser.mjs.map +1 -1
  127. package/dist/esm/tools/BrowserTools.mjs +1 -1
  128. package/dist/esm/tools/BrowserTools.mjs.map +1 -1
  129. package/dist/esm/tools/ToolNode.mjs +128 -11
  130. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  131. package/dist/esm/tools/approval/constants.mjs +2 -2
  132. package/dist/esm/tools/approval/constants.mjs.map +1 -1
  133. package/dist/esm/tools/memory/index.mjs +46 -0
  134. package/dist/esm/tools/memory/index.mjs.map +1 -0
  135. package/dist/esm/tools/memory/memoryAppendTool.mjs +67 -0
  136. package/dist/esm/tools/memory/memoryAppendTool.mjs.map +1 -0
  137. package/dist/esm/tools/memory/memoryGetTool.mjs +47 -0
  138. package/dist/esm/tools/memory/memoryGetTool.mjs.map +1 -0
  139. package/dist/esm/tools/memory/memorySearchTool.mjs +63 -0
  140. package/dist/esm/tools/memory/memorySearchTool.mjs.map +1 -0
  141. package/dist/esm/tools/memory/shared.mjs +98 -0
  142. package/dist/esm/tools/memory/shared.mjs.map +1 -0
  143. package/dist/esm/types/graph.mjs.map +1 -1
  144. package/dist/esm/utils/childAgentContext.mjs +237 -0
  145. package/dist/esm/utils/childAgentContext.mjs.map +1 -0
  146. package/dist/esm/utils/errors.mjs +109 -0
  147. package/dist/esm/utils/errors.mjs.map +1 -0
  148. package/dist/esm/utils/events.mjs +36 -8
  149. package/dist/esm/utils/events.mjs.map +1 -1
  150. package/dist/esm/utils/finishReasons.mjs +41 -0
  151. package/dist/esm/utils/finishReasons.mjs.map +1 -0
  152. package/dist/esm/utils/llm.mjs.map +1 -1
  153. package/dist/esm/utils/logging.mjs +31 -0
  154. package/dist/esm/utils/logging.mjs.map +1 -0
  155. package/dist/esm/utils/toolCallNormalization.mjs +247 -0
  156. package/dist/esm/utils/toolCallNormalization.mjs.map +1 -0
  157. package/dist/types/common/index.d.ts +1 -0
  158. package/dist/types/common/spawnPath.d.ts +59 -0
  159. package/dist/types/graphs/HandoffRegistry.d.ts +24 -7
  160. package/dist/types/graphs/MultiAgentGraph.d.ts +43 -23
  161. package/dist/types/graphs/phases/flushLoop.d.ts +106 -0
  162. package/dist/types/graphs/phases/memoryFlushPhase.d.ts +100 -0
  163. package/dist/types/index.d.ts +7 -0
  164. package/dist/types/memory/__tests__/mockBackend.d.ts +40 -0
  165. package/dist/types/memory/citations.d.ts +39 -0
  166. package/dist/types/memory/compositeBackend.d.ts +30 -0
  167. package/dist/types/memory/constants.d.ts +121 -0
  168. package/dist/types/memory/embeddings.d.ts +15 -0
  169. package/dist/types/memory/factory.d.ts +23 -0
  170. package/dist/types/memory/index.d.ts +21 -0
  171. package/dist/types/memory/migrate.d.ts +14 -0
  172. package/dist/types/memory/mmr.d.ts +50 -0
  173. package/dist/types/memory/paths.d.ts +107 -0
  174. package/dist/types/memory/pgvectorStore.d.ts +56 -0
  175. package/dist/types/memory/recallTracking.d.ts +30 -0
  176. package/dist/types/memory/temporalDecay.d.ts +53 -0
  177. package/dist/types/memory/types.d.ts +182 -0
  178. package/dist/types/prompts/memoryFlushPrompt.d.ts +54 -0
  179. package/dist/types/run.d.ts +1 -0
  180. package/dist/types/tools/AskUser.d.ts +1 -1
  181. package/dist/types/tools/BrowserTools.d.ts +2 -2
  182. package/dist/types/tools/approval/constants.d.ts +2 -2
  183. package/dist/types/tools/memory/index.d.ts +39 -0
  184. package/dist/types/tools/memory/memoryAppendTool.d.ts +27 -0
  185. package/dist/types/tools/memory/memoryGetTool.d.ts +22 -0
  186. package/dist/types/tools/memory/memorySearchTool.d.ts +22 -0
  187. package/dist/types/tools/memory/shared.d.ts +106 -0
  188. package/dist/types/types/graph.d.ts +10 -3
  189. package/dist/types/utils/childAgentContext.d.ts +99 -0
  190. package/dist/types/utils/errors.d.ts +37 -0
  191. package/dist/types/utils/events.d.ts +21 -0
  192. package/dist/types/utils/finishReasons.d.ts +32 -0
  193. package/dist/types/utils/index.d.ts +1 -0
  194. package/dist/types/utils/logging.d.ts +2 -0
  195. package/dist/types/utils/toolCallNormalization.d.ts +44 -0
  196. package/package.json +6 -4
  197. package/src/agents/AgentContext.ts +12 -4
  198. package/src/common/__tests__/enum.test.ts +4 -2
  199. package/src/common/__tests__/spawnPath.test.ts +110 -0
  200. package/src/common/index.ts +1 -0
  201. package/src/common/spawnPath.ts +101 -0
  202. package/src/graphs/Graph.ts +95 -61
  203. package/src/graphs/HandoffRegistry.ts +48 -17
  204. package/src/graphs/MultiAgentGraph.ts +588 -327
  205. package/src/graphs/__tests__/HandoffRegistry.test.ts +4 -1
  206. package/src/graphs/__tests__/multi-agent-delegate.test.ts +61 -16
  207. package/src/graphs/__tests__/multi-agent-edges.test.ts +4 -2
  208. package/src/graphs/__tests__/multi-agent-nested-subgraph.test.ts +221 -0
  209. package/src/graphs/__tests__/structured-output.integration.test.ts +212 -118
  210. package/src/graphs/contextManagement.e2e.test.ts +1 -1
  211. package/src/graphs/phases/__tests__/flushLoop.test.ts +264 -0
  212. package/src/graphs/phases/__tests__/memoryFlushPhase.test.ts +37 -0
  213. package/src/graphs/phases/__tests__/runMemoryFlush.test.ts +150 -0
  214. package/src/graphs/phases/flushLoop.ts +303 -0
  215. package/src/graphs/phases/memoryFlushPhase.ts +209 -0
  216. package/src/index.ts +30 -1
  217. package/src/llm/bedrock/index.ts +4 -5
  218. package/src/memory/__tests__/citations.test.ts +61 -0
  219. package/src/memory/__tests__/compositeBackend.test.ts +79 -0
  220. package/src/memory/__tests__/isolation.test.ts +206 -0
  221. package/src/memory/__tests__/mmr.test.ts +148 -0
  222. package/src/memory/__tests__/mockBackend.ts +161 -0
  223. package/src/memory/__tests__/paths.test.ts +168 -0
  224. package/src/memory/__tests__/recallTracking.test.ts +96 -0
  225. package/src/memory/__tests__/temporalDecay.test.ts +151 -0
  226. package/src/memory/citations.ts +80 -0
  227. package/src/memory/compositeBackend.ts +99 -0
  228. package/src/memory/constants.ts +229 -0
  229. package/src/memory/embeddings.ts +188 -0
  230. package/src/memory/factory.ts +111 -0
  231. package/src/memory/index.ts +46 -0
  232. package/src/memory/migrate.ts +116 -0
  233. package/src/memory/mmr.ts +161 -0
  234. package/src/memory/paths.ts +258 -0
  235. package/src/memory/pgvectorStore.ts +324 -0
  236. package/src/memory/recallTracking.ts +127 -0
  237. package/src/memory/schema.sql +51 -0
  238. package/src/memory/temporalDecay.ts +134 -0
  239. package/src/memory/types.ts +185 -0
  240. package/src/nodes/ApprovalGateNode.ts +4 -10
  241. package/src/nodes/__tests__/ApprovalGateNode.test.ts +11 -20
  242. package/src/prompts/memoryFlushPrompt.ts +78 -0
  243. package/src/run.ts +17 -6
  244. package/src/scripts/test-bedrock-handoff-autonomous.ts +56 -20
  245. package/src/specs/agent-handoffs-bedrock.integration.test.ts +8 -5
  246. package/src/specs/agent-handoffs.test.ts +8 -2
  247. package/src/tools/AskUser.ts +7 -2
  248. package/src/tools/BrowserTools.ts +3 -5
  249. package/src/tools/ToolNode.ts +150 -13
  250. package/src/tools/__tests__/ToolApproval.test.ts +22 -9
  251. package/src/tools/approval/__tests__/constants.test.ts +1 -1
  252. package/src/tools/approval/constants.ts +2 -2
  253. package/src/tools/memory/__tests__/memoryTools.test.ts +205 -0
  254. package/src/tools/memory/index.ts +96 -0
  255. package/src/tools/memory/memoryAppendTool.ts +101 -0
  256. package/src/tools/memory/memoryGetTool.ts +53 -0
  257. package/src/tools/memory/memorySearchTool.ts +80 -0
  258. package/src/tools/memory/shared.ts +169 -0
  259. package/src/tools/search/search.test.ts +6 -1
  260. package/src/types/graph.ts +10 -3
  261. package/src/utils/__tests__/childAgentContext.test.ts +217 -0
  262. package/src/utils/__tests__/errors.test.ts +136 -0
  263. package/src/utils/__tests__/finishReasons.test.ts +55 -0
  264. package/src/utils/__tests__/toolCallNormalization.test.ts +181 -0
  265. package/src/utils/childAgentContext.ts +259 -0
  266. package/src/utils/errors.ts +115 -0
  267. package/src/utils/events.ts +37 -7
  268. package/src/utils/finishReasons.ts +40 -0
  269. package/src/utils/index.ts +1 -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,324 @@
1
+ /**
2
+ * Postgres + pgvector implementation of {@link MemoryBackend}.
3
+ *
4
+ * ## Scoping model (two-tier, layered)
5
+ *
6
+ * Every read query applies the layered filter
7
+ *
8
+ * WHERE agent_id = $1 AND (user_id IS NULL OR user_id = $2)
9
+ *
10
+ * so the caller sees:
11
+ * - agent-tier rows (`user_id IS NULL`) — shared operational knowledge,
12
+ * visible to every user of the agent
13
+ * - their own user-tier rows (`user_id = <caller>`) — private per-user
14
+ * personalization
15
+ *
16
+ * Another user's user-tier rows are invisible — the privacy boundary is
17
+ * enforced in SQL, not just in the UI or route layer.
18
+ *
19
+ * ## Writes
20
+ *
21
+ * `append()` routes to a row based on the path's tier, resolved via
22
+ * {@link assertWritablePath}:
23
+ *
24
+ * - `memory/agent/*` → stored with `user_id = NULL` regardless of
25
+ * what scope the caller passed. Agent-tier content is inherently
26
+ * shared; scoping it per-user would defeat the point.
27
+ * - `memory/user/*` → stored with `user_id = scope.userId`. A missing
28
+ * `scope.userId` throws — user-tier paths cannot be written from
29
+ * isolated/autonomous contexts.
30
+ *
31
+ * UPSERT key is `(agent_id, user_id, path)` with `NULLS NOT DISTINCT`,
32
+ * so each user gets their own `user/preferences.md` row and there is
33
+ * exactly one `agent/playbook.md` row shared across the whole user base.
34
+ * Content accumulates via `\n\n` concatenation on conflict, with the
35
+ * embedding regenerated over the merged content so search stays
36
+ * consistent.
37
+ */
38
+ import type { Pool } from 'pg';
39
+ import {
40
+ DEFAULT_MAX_SEARCH_RESULTS,
41
+ DEFAULT_MEMORY_TABLE,
42
+ DEFAULT_MIN_SCORE,
43
+ HYBRID_TEXT_WEIGHT,
44
+ HYBRID_VECTOR_WEIGHT,
45
+ } from './constants';
46
+ import { getMemoryEmbedder, type EmbeddingProvider } from './embeddings';
47
+ import { applyMMRToMemoryHits } from './mmr';
48
+ import { applyTemporalDecayToHits } from './temporalDecay';
49
+ import { decorateCitations, shouldIncludeCitations } from './citations';
50
+ import { assertWritablePath, getTierForPath } from './paths';
51
+ import type {
52
+ MemoryAppendInput,
53
+ MemoryBackend,
54
+ MemoryEntry,
55
+ MemoryGetOptions,
56
+ MemoryHealth,
57
+ MemoryReadResult,
58
+ MemoryScope,
59
+ MemorySearchOptions,
60
+ } from './types';
61
+
62
+ export interface PgvectorStoreOptions {
63
+ pool: Pool;
64
+ table?: string;
65
+ embedder?: EmbeddingProvider;
66
+ }
67
+
68
+ function assertScope(scope: MemoryScope): void {
69
+ if (!scope || !scope.agentId) {
70
+ throw new Error(
71
+ 'MemoryScope { agentId } is required — agentId must be non-empty'
72
+ );
73
+ }
74
+ }
75
+
76
+ /** pgvector literal format: "[0.1,0.2,...]". */
77
+ function toVectorLiteral(vec: number[]): string {
78
+ return `[${vec.join(',')}]`;
79
+ }
80
+
81
+ /**
82
+ * Normalize caller userId for the layered read filter.
83
+ *
84
+ * The SQL filter is `(user_id IS NULL OR user_id = $2)`, so an empty
85
+ * string from the caller must not match rows whose user_id was set.
86
+ * We coerce empty/null/undefined to `null`, and pg treats `$2 = null`
87
+ * as `false` — which is exactly what we want for isolated callers:
88
+ * they see only agent-tier rows and nothing in the user tier.
89
+ */
90
+ function normalizeCallerId(scope: MemoryScope): string | null {
91
+ const raw = scope.userId;
92
+ if (raw == null || raw === '') return null;
93
+ return String(raw);
94
+ }
95
+
96
+ export class PgvectorMemoryStore implements MemoryBackend {
97
+ readonly kind = 'vector' as const;
98
+ private pool: Pool;
99
+ private table: string;
100
+ private embedder: EmbeddingProvider;
101
+
102
+ constructor(opts: PgvectorStoreOptions) {
103
+ this.pool = opts.pool;
104
+ this.table = opts.table ?? DEFAULT_MEMORY_TABLE;
105
+ this.embedder = opts.embedder ?? getMemoryEmbedder();
106
+ }
107
+
108
+ async search(
109
+ scope: MemoryScope,
110
+ query: string,
111
+ opts: MemorySearchOptions = {}
112
+ ): Promise<MemoryEntry[]> {
113
+ assertScope(scope);
114
+ const trimmed = query.trim();
115
+ if (!trimmed) return [];
116
+
117
+ const maxResults = Math.max(
118
+ 1,
119
+ opts.maxResults ?? DEFAULT_MAX_SEARCH_RESULTS
120
+ );
121
+ const minScore = opts.minScore ?? DEFAULT_MIN_SCORE;
122
+
123
+ const vector = await this.embedder.embed(trimmed);
124
+ const vectorLiteral = toVectorLiteral(vector);
125
+ const callerId = normalizeCallerId(scope);
126
+
127
+ // [memory-layered-search] debug: layered scope filter
128
+ // agent-tier rows (user_id IS NULL) + this caller's user-tier rows.
129
+ // Another user's rows are invisible at the SQL layer.
130
+ const sql = `
131
+ WITH scored AS (
132
+ SELECT
133
+ id,
134
+ path,
135
+ content,
136
+ created_at,
137
+ user_id,
138
+ (1 - (embedding <=> $1::vector)) AS vector_score,
139
+ ts_rank(tsv, plainto_tsquery('english', $2)) AS text_score
140
+ FROM ${this.table}
141
+ WHERE agent_id = $3
142
+ AND (user_id IS NULL OR user_id = $4)
143
+ )
144
+ SELECT
145
+ id,
146
+ path,
147
+ content,
148
+ created_at,
149
+ user_id,
150
+ (${HYBRID_VECTOR_WEIGHT} * vector_score + ${HYBRID_TEXT_WEIGHT} * text_score) AS score
151
+ FROM scored
152
+ WHERE (${HYBRID_VECTOR_WEIGHT} * vector_score + ${HYBRID_TEXT_WEIGHT} * text_score) >= $5
153
+ ORDER BY score DESC
154
+ LIMIT $6
155
+ `;
156
+
157
+ const { rows } = await this.pool.query(sql, [
158
+ vectorLiteral,
159
+ trimmed,
160
+ scope.agentId,
161
+ callerId,
162
+ minScore,
163
+ maxResults,
164
+ ]);
165
+
166
+ let hits: MemoryEntry[] = rows.map(
167
+ (row: {
168
+ id: string | number;
169
+ path: string;
170
+ content: string;
171
+ created_at: Date;
172
+ user_id: string | null;
173
+ score: string | number;
174
+ }): MemoryEntry => ({
175
+ id: String(row.id),
176
+ path: row.path,
177
+ content: row.content,
178
+ createdAt: new Date(row.created_at),
179
+ score: Number(row.score),
180
+ source: 'vector',
181
+ tier: getTierForPath(row.path),
182
+ })
183
+ );
184
+
185
+ // Phase 2: temporal decay (before MMR so diversity sees post-decay ranks)
186
+ if (opts.temporalDecay?.enabled) {
187
+ hits = applyTemporalDecayToHits(hits, opts.temporalDecay);
188
+ hits.sort((a, b) => b.score - a.score);
189
+ }
190
+
191
+ // Phase 2: MMR reranking
192
+ if (opts.mmr?.enabled) {
193
+ hits = applyMMRToMemoryHits(hits, opts.mmr);
194
+ }
195
+
196
+ // Phase 2: citations (decorate last — mutates content with Source: trailer)
197
+ const citationsMode = opts.citations ?? 'auto';
198
+ if (shouldIncludeCitations(citationsMode)) {
199
+ hits = decorateCitations(hits, true);
200
+ }
201
+
202
+ return hits;
203
+ }
204
+
205
+ async get(
206
+ scope: MemoryScope,
207
+ opts: MemoryGetOptions
208
+ ): Promise<MemoryReadResult | null> {
209
+ assertScope(scope);
210
+ if (!opts.path) return null;
211
+
212
+ const callerId = normalizeCallerId(scope);
213
+ const tier = getTierForPath(opts.path);
214
+
215
+ // Agent-tier rows always live under user_id=NULL. User-tier rows
216
+ // always carry the caller's id. Querying with a precise predicate
217
+ // is faster than leaving it open AND guarantees a user cannot
218
+ // read another user's row even if they know the path by heart.
219
+ const userClause = tier === 'agent' ? 'user_id IS NULL' : 'user_id = $3';
220
+
221
+ const params: unknown[] = [scope.agentId, opts.path];
222
+ if (tier === 'user') params.push(callerId);
223
+
224
+ const sql = `
225
+ SELECT content
226
+ FROM ${this.table}
227
+ WHERE agent_id = $1 AND path = $2 AND ${userClause}
228
+ ORDER BY updated_at DESC
229
+ LIMIT 1
230
+ `;
231
+ const { rows } = await this.pool.query(sql, params);
232
+ if (rows.length === 0) return null;
233
+
234
+ const text = String(rows[0].content ?? '');
235
+ if (opts.from == null && opts.lines == null) {
236
+ return { path: opts.path, text };
237
+ }
238
+
239
+ const allLines = text.split('\n');
240
+ const fromIdx = Math.max(0, (opts.from ?? 1) - 1);
241
+ const count = Math.max(0, opts.lines ?? allLines.length - fromIdx);
242
+ const slice = allLines.slice(fromIdx, fromIdx + count).join('\n');
243
+ return { path: opts.path, text: slice };
244
+ }
245
+
246
+ async append(scope: MemoryScope, input: MemoryAppendInput): Promise<void> {
247
+ assertScope(scope);
248
+ // Whitelist + tier + scope-compatibility check in one call. Throws
249
+ // with an actionable message for each failure mode.
250
+ const descriptor = assertWritablePath(input.path, scope);
251
+ const content = input.content.trim();
252
+ if (!content) {
253
+ throw new Error('memory_append content must be non-empty');
254
+ }
255
+
256
+ // Tier determines the row's user_id:
257
+ // agent tier → NULL (shared across all users)
258
+ // user tier → the caller's id (assertWritablePath guarantees non-empty)
259
+ const rowUserId = descriptor.tier === 'agent' ? null : String(scope.userId);
260
+ const provenance = scope.userId != null ? String(scope.userId) : null;
261
+
262
+ // Read the existing row (if any) for THIS tier so we can embed the
263
+ // merged content. Agent-tier merges regardless of caller; user-tier
264
+ // merges only within the caller's own row.
265
+ const lookupSql = `
266
+ SELECT content FROM ${this.table}
267
+ WHERE agent_id = $1 AND path = $2 AND ${rowUserId === null ? 'user_id IS NULL' : 'user_id = $3'}
268
+ LIMIT 1
269
+ `;
270
+ const lookupParams: unknown[] =
271
+ rowUserId === null
272
+ ? [scope.agentId, input.path]
273
+ : [scope.agentId, input.path, rowUserId];
274
+ const existing = await this.pool.query(lookupSql, lookupParams);
275
+ const priorContent: string =
276
+ existing.rows.length > 0 ? String(existing.rows[0].content ?? '') : '';
277
+ const mergedContent = priorContent
278
+ ? `${priorContent.replace(/\s+$/, '')}\n\n${content}`
279
+ : content;
280
+
281
+ const vector = await this.embedder.embed(mergedContent);
282
+ const vectorLiteral = toVectorLiteral(vector);
283
+
284
+ // UPSERT on (agent_id, user_id, path) with NULLS NOT DISTINCT so
285
+ // two NULL user_ids collide on the same agent+path (exactly one
286
+ // agent-tier row) while per-user rows on the same path coexist.
287
+ //
288
+ // Provenance note: `last_user_id` always records WHO wrote the
289
+ // latest append, even for agent-tier rows where the row's own
290
+ // `user_id` stays NULL. That gives the admin UI an audit trail
291
+ // ("agent-tier row last updated by Alice") without changing the
292
+ // scoping semantics.
293
+ const upsertSql = `
294
+ INSERT INTO ${this.table} (agent_id, user_id, path, content, embedding, last_user_id, updated_at)
295
+ VALUES ($1, $2, $3, $4, $5::vector, $6, NOW())
296
+ ON CONFLICT (agent_id, user_id, path) DO UPDATE
297
+ SET content = EXCLUDED.content,
298
+ embedding = EXCLUDED.embedding,
299
+ last_user_id = EXCLUDED.last_user_id,
300
+ updated_at = NOW()
301
+ `;
302
+ await this.pool.query(upsertSql, [
303
+ scope.agentId,
304
+ rowUserId,
305
+ input.path,
306
+ mergedContent,
307
+ vectorLiteral,
308
+ provenance,
309
+ ]);
310
+ }
311
+
312
+ async health(): Promise<MemoryHealth> {
313
+ try {
314
+ await this.pool.query('SELECT 1');
315
+ return { ok: true, backend: 'vector' };
316
+ } catch (err) {
317
+ return {
318
+ ok: false,
319
+ backend: 'vector',
320
+ error: err instanceof Error ? err.message : String(err),
321
+ };
322
+ }
323
+ }
324
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Recall tracking — Phase 2.
3
+ *
4
+ * Lightweight adaptation of upstream
5
+ * `extensions/memory-core/src/short-term-promotion.ts::recordShortTermRecalls`.
6
+ * Upstream stores recalls in a JSON file under `memory/.dreams/`; we store
7
+ * them in a Postgres table `agent_memory_recalls`. Schema captures what the
8
+ * future Phase 3 dreaming/promotion algorithm will need:
9
+ * - which memory row was surfaced (`memory_id`)
10
+ * - the query that surfaced it (raw + SHA-256 hash for dedupe)
11
+ * - hybrid score at the time of recall
12
+ * - the day bucket (for per-day dedupe / frequency counting)
13
+ * - the recorded timestamp
14
+ *
15
+ * Best-effort: failures never block memory_search. The caller fires
16
+ * {@link RecallTracker.record} without awaiting the result and ignores errors.
17
+ */
18
+ import { createHash } from 'crypto';
19
+ import type { Pool } from 'pg';
20
+
21
+ export interface RecallTracker {
22
+ /** Record that the given memory ids were surfaced to the model for a query. */
23
+ record(params: RecallRecordParams): Promise<void>;
24
+ /** Backend-specific schema migration. Idempotent. */
25
+ migrate(): Promise<void>;
26
+ }
27
+
28
+ export interface RecallRecordParams {
29
+ agentId: string;
30
+ query: string;
31
+ hits: Array<{ id: string; path: string; score: number }>;
32
+ nowMs?: number;
33
+ }
34
+
35
+ export const RECALL_TABLE = 'agent_memory_recalls';
36
+
37
+ function hashQuery(query: string): string {
38
+ return createHash('sha256')
39
+ .update(query.trim().toLowerCase())
40
+ .digest('hex')
41
+ .slice(0, 32);
42
+ }
43
+
44
+ function dayBucket(nowMs: number): string {
45
+ const d = new Date(nowMs);
46
+ const y = d.getUTCFullYear();
47
+ const m = String(d.getUTCMonth() + 1).padStart(2, '0');
48
+ const day = String(d.getUTCDate()).padStart(2, '0');
49
+ return `${y}-${m}-${day}`;
50
+ }
51
+
52
+ export class PgvectorRecallTracker implements RecallTracker {
53
+ constructor(
54
+ private readonly pool: Pool,
55
+ private readonly table: string = RECALL_TABLE
56
+ ) {}
57
+
58
+ async migrate(): Promise<void> {
59
+ // [recall-tracking] debug: create table + indexes if missing
60
+ await this.pool.query(`
61
+ CREATE TABLE IF NOT EXISTS ${this.table} (
62
+ id BIGSERIAL PRIMARY KEY,
63
+ agent_id TEXT NOT NULL,
64
+ memory_id TEXT NOT NULL,
65
+ memory_path TEXT NOT NULL,
66
+ query TEXT NOT NULL,
67
+ query_hash TEXT NOT NULL,
68
+ score DOUBLE PRECISION NOT NULL,
69
+ day_bucket TEXT NOT NULL,
70
+ recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
71
+ )
72
+ `);
73
+ await this.pool.query(
74
+ `CREATE INDEX IF NOT EXISTS ${this.table}_agent_day_idx ON ${this.table} (agent_id, day_bucket)`
75
+ );
76
+ await this.pool.query(
77
+ `CREATE INDEX IF NOT EXISTS ${this.table}_memory_idx ON ${this.table} (agent_id, memory_id)`
78
+ );
79
+ await this.pool.query(
80
+ `CREATE UNIQUE INDEX IF NOT EXISTS ${this.table}_dedupe_idx
81
+ ON ${this.table} (agent_id, memory_id, query_hash, day_bucket)`
82
+ );
83
+ }
84
+
85
+ async record(params: RecallRecordParams): Promise<void> {
86
+ if (!params.agentId || !params.query.trim() || params.hits.length === 0)
87
+ return;
88
+ const nowMs = params.nowMs ?? Date.now();
89
+ const qhash = hashQuery(params.query);
90
+ const bucket = dayBucket(nowMs);
91
+
92
+ // [recall-tracking] debug: upsert one row per (agent, memory, query, day)
93
+ // Upstream dedupes per-day per-query so repeated searches don't inflate counts.
94
+ const values: string[] = [];
95
+ const args: unknown[] = [];
96
+ let i = 1;
97
+ for (const hit of params.hits) {
98
+ values.push(
99
+ `($${i++}, $${i++}, $${i++}, $${i++}, $${i++}, $${i++}, $${i++}, NOW())`
100
+ );
101
+ args.push(
102
+ params.agentId,
103
+ hit.id,
104
+ hit.path,
105
+ params.query,
106
+ qhash,
107
+ hit.score,
108
+ bucket
109
+ );
110
+ }
111
+ const sql = `
112
+ INSERT INTO ${this.table}
113
+ (agent_id, memory_id, memory_path, query, query_hash, score, day_bucket, recorded_at)
114
+ VALUES ${values.join(', ')}
115
+ ON CONFLICT (agent_id, memory_id, query_hash, day_bucket) DO UPDATE
116
+ SET score = GREATEST(${this.table}.score, EXCLUDED.score),
117
+ recorded_at = NOW()
118
+ `;
119
+ await this.pool.query(sql, args);
120
+ }
121
+ }
122
+
123
+ /** No-op tracker — used when recall tracking is disabled or the backend isn't pgvector. */
124
+ export class NullRecallTracker implements RecallTracker {
125
+ async record(): Promise<void> {}
126
+ async migrate(): Promise<void> {}
127
+ }
@@ -0,0 +1,51 @@
1
+ -- Autonomous memory — Postgres schema (v2: layered tier scoping).
2
+ --
3
+ -- Two-tier canonical-document model:
4
+ --
5
+ -- memory/agent/* → shared across every user of the agent (user_id = NULL)
6
+ -- memory/user/* → private to a specific caller (user_id = <callerId>)
7
+ --
8
+ -- Uniqueness key is `(agent_id, user_id, path)` with NULLS NOT DISTINCT
9
+ -- so two NULL user_id rows on the same path collide (exactly-one
10
+ -- agent-tier row per path) while per-user rows on the same path coexist
11
+ -- (one row per user for user-tier paths).
12
+ --
13
+ -- The read path filters with `(user_id IS NULL OR user_id = $caller)`,
14
+ -- so callers see agent-tier rows + only their own user-tier rows —
15
+ -- never another user's private memory. Enforcement is in SQL, not just
16
+ -- the UI.
17
+ --
18
+ -- NULLS NOT DISTINCT requires PostgreSQL 15+. Host's production
19
+ -- pgvector image is PG16, so this is safe. The migration in
20
+ -- `migrate.ts` will drop the legacy `(agent_id, path)` constraint
21
+ -- before creating the new one if it exists on an older database.
22
+
23
+ CREATE EXTENSION IF NOT EXISTS vector;
24
+
25
+ CREATE TABLE IF NOT EXISTS agent_memories (
26
+ id BIGSERIAL PRIMARY KEY,
27
+ agent_id TEXT NOT NULL,
28
+ user_id TEXT, -- NULL = agent-tier, shared
29
+ path TEXT NOT NULL,
30
+ content TEXT NOT NULL,
31
+ embedding VECTOR(1024),
32
+ tsv TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', content)) STORED,
33
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
34
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
35
+ last_user_id TEXT, -- latest session to append
36
+ CONSTRAINT agent_memories_agent_user_path_uq
37
+ UNIQUE NULLS NOT DISTINCT (agent_id, user_id, path)
38
+ );
39
+
40
+ -- Primary lookup index for list/search/generate-prompt — matches the
41
+ -- exact filter shape of every read query.
42
+ CREATE INDEX IF NOT EXISTS agent_memories_scope_idx
43
+ ON agent_memories (agent_id, user_id, path);
44
+
45
+ -- Vector ANN index — untouched by the scoping change.
46
+ CREATE INDEX IF NOT EXISTS agent_memories_vector_idx
47
+ ON agent_memories USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
48
+
49
+ -- Full-text GIN index — untouched.
50
+ CREATE INDEX IF NOT EXISTS agent_memories_tsv_idx
51
+ ON agent_memories USING GIN (tsv);
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Temporal decay — Phase 2.
3
+ *
4
+ * Ported from upstream `extensions/memory-core/src/memory/temporal-decay.ts`.
5
+ * Ages dated memory files (`memory/YYYY-MM-DD.md`) using exponential decay
6
+ * `multiplier = exp(-ln(2) / halfLifeDays * ageInDays)`. At half-life, the
7
+ * score is exactly halved.
8
+ *
9
+ * Evergreen files (MEMORY.md, memory/topics.md, any non-dated file inside
10
+ * memory/) do NOT decay — they represent durable knowledge and should stay
11
+ * hot regardless of age. This mirrors upstream's `isEvergreenMemoryPath`.
12
+ *
13
+ * Since our pgvector rows carry `createdAt`, we don't need filesystem stat
14
+ * fallback — the row timestamp is authoritative for any file without a
15
+ * date in the path.
16
+ */
17
+
18
+ export interface TemporalDecayConfig {
19
+ enabled: boolean;
20
+ halfLifeDays: number;
21
+ }
22
+
23
+ export const DEFAULT_TEMPORAL_DECAY_CONFIG: TemporalDecayConfig = {
24
+ enabled: false,
25
+ halfLifeDays: 30,
26
+ };
27
+
28
+ const DAY_MS = 24 * 60 * 60 * 1000;
29
+ const DATED_MEMORY_PATH_RE = /(?:^|\/)memory\/(\d{4})-(\d{2})-(\d{2})\.md$/;
30
+
31
+ export function toDecayLambda(halfLifeDays: number): number {
32
+ if (!Number.isFinite(halfLifeDays) || halfLifeDays <= 0) return 0;
33
+ return Math.LN2 / halfLifeDays;
34
+ }
35
+
36
+ export function calculateTemporalDecayMultiplier(params: {
37
+ ageInDays: number;
38
+ halfLifeDays: number;
39
+ }): number {
40
+ const lambda = toDecayLambda(params.halfLifeDays);
41
+ const age = Math.max(0, params.ageInDays);
42
+ if (lambda <= 0 || !Number.isFinite(age)) return 1;
43
+ return Math.exp(-lambda * age);
44
+ }
45
+
46
+ export function applyTemporalDecayToScore(params: {
47
+ score: number;
48
+ ageInDays: number;
49
+ halfLifeDays: number;
50
+ }): number {
51
+ return params.score * calculateTemporalDecayMultiplier(params);
52
+ }
53
+
54
+ function normalizePath(p: string): string {
55
+ return (p ?? '').replace(/\\/g, '/').replace(/^\.\//, '');
56
+ }
57
+
58
+ /** Parse a date out of `memory/YYYY-MM-DD.md` — returns null on non-match or invalid date. */
59
+ export function parseMemoryDateFromPath(filePath: string): Date | null {
60
+ const m = DATED_MEMORY_PATH_RE.exec(normalizePath(filePath));
61
+ if (!m) return null;
62
+ const y = Number(m[1]);
63
+ const mo = Number(m[2]);
64
+ const d = Number(m[3]);
65
+ if (!Number.isInteger(y) || !Number.isInteger(mo) || !Number.isInteger(d))
66
+ return null;
67
+ const ts = Date.UTC(y, mo - 1, d);
68
+ const parsed = new Date(ts);
69
+ if (
70
+ parsed.getUTCFullYear() !== y ||
71
+ parsed.getUTCMonth() !== mo - 1 ||
72
+ parsed.getUTCDate() !== d
73
+ ) {
74
+ return null;
75
+ }
76
+ return parsed;
77
+ }
78
+
79
+ /**
80
+ * Evergreen = durable knowledge file that should not decay.
81
+ * - `MEMORY.md` / `memory.md` at root
82
+ * - anything inside `memory/` that is NOT a dated `YYYY-MM-DD.md` file
83
+ */
84
+ export function isEvergreenMemoryPath(filePath: string): boolean {
85
+ const n = normalizePath(filePath);
86
+ if (n === 'MEMORY.md' || n === 'memory.md') return true;
87
+ if (!n.startsWith('memory/')) return false;
88
+ return !DATED_MEMORY_PATH_RE.test(n);
89
+ }
90
+
91
+ function ageInDays(timestamp: Date, nowMs: number): number {
92
+ return Math.max(0, nowMs - timestamp.getTime()) / DAY_MS;
93
+ }
94
+
95
+ export interface DecayCandidate {
96
+ path: string;
97
+ score: number;
98
+ createdAt?: Date;
99
+ }
100
+
101
+ /**
102
+ * Apply temporal decay to a list of memory hits.
103
+ *
104
+ * Priority for the effective timestamp:
105
+ * 1. Dated path (`memory/YYYY-MM-DD.md`) — use the date in the path
106
+ * 2. Otherwise, if the path is evergreen — NO decay
107
+ * 3. Otherwise, use the row's `createdAt`
108
+ */
109
+ export function applyTemporalDecayToHits<T extends DecayCandidate>(
110
+ hits: T[],
111
+ config: Partial<TemporalDecayConfig> = {},
112
+ nowMs: number = Date.now()
113
+ ): T[] {
114
+ const merged = { ...DEFAULT_TEMPORAL_DECAY_CONFIG, ...config };
115
+ if (!merged.enabled) return [...hits];
116
+
117
+ return hits.map((h) => {
118
+ const datedTs = parseMemoryDateFromPath(h.path);
119
+ let ts: Date | null = datedTs;
120
+ if (!ts) {
121
+ if (isEvergreenMemoryPath(h.path)) return h;
122
+ ts = h.createdAt ?? null;
123
+ }
124
+ if (!ts) return h;
125
+ return {
126
+ ...h,
127
+ score: applyTemporalDecayToScore({
128
+ score: h.score,
129
+ ageInDays: ageInDays(ts, nowMs),
130
+ halfLifeDays: merged.halfLifeDays,
131
+ }),
132
+ };
133
+ });
134
+ }