@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,303 @@
1
+ /**
2
+ * Agentic tool-execution loop for the memory-flush reflection turn.
3
+ *
4
+ * Extracted from {@link runMemoryFlush} so the loop primitive is
5
+ * testable in isolation and reusable from any reflection-style phase
6
+ * that wants "invoke → execute tool_calls → feed results back → repeat".
7
+ *
8
+ * Design:
9
+ * - Bounded iterations — caller passes `maxIterations` (default
10
+ * {@link DEFAULT_MAX_FLUSH_ITERATIONS}). The loop exits as soon as
11
+ * the model stops emitting `tool_calls`, hits the cap, or the reply
12
+ * matches {@link SILENT_REPLY_TOKEN}.
13
+ * - Rich result — returns iteration count, attempted/successful
14
+ * appends, per-tool errors, silent-reply flag, and raw final text.
15
+ * The caller logs this; nothing is swallowed.
16
+ * - Self-contained tool execution — does not depend on LangGraph's
17
+ * ToolNode or any graph runtime. The only contract is the
18
+ * LangChain v0.3 `StructuredToolInterface.invoke(args)` shape.
19
+ * - Tool errors are captured as {@link ToolMessage}s with the raw
20
+ * JSON error, so the model can read them and self-correct (e.g.
21
+ * retry with a different path if the schema rejected its first
22
+ * attempt).
23
+ */
24
+ import {
25
+ AIMessage,
26
+ type BaseMessage,
27
+ ToolMessage,
28
+ } from '@langchain/core/messages';
29
+ import type { StructuredToolInterface } from '@langchain/core/tools';
30
+ import {
31
+ DEFAULT_MAX_FLUSH_ITERATIONS,
32
+ MEMORY_APPEND_TOOL_NAME,
33
+ SILENT_REPLY_TOKEN,
34
+ } from '@/memory/constants';
35
+
36
+ /** Minimal model contract the loop needs. */
37
+ export interface InvokableModel {
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ invoke(messages: BaseMessage[], options?: any): Promise<AIMessage>;
40
+ }
41
+
42
+ export interface FlushLoopParams {
43
+ model: InvokableModel;
44
+ tools: StructuredToolInterface[];
45
+ /** Initial messages — typically [SystemMessage, HumanMessage]. */
46
+ initialMessages: BaseMessage[];
47
+ /** Upper bound on model.invoke() calls. Default 8. */
48
+ maxIterations?: number;
49
+ /**
50
+ * Optional debug hook — called once per iteration with `{ i, ai, toolCalls }`.
51
+ * Use for INFO-level tracing during rollout; pass `undefined` in prod.
52
+ */
53
+ onIteration?: (event: {
54
+ i: number;
55
+ ai: AIMessage;
56
+ toolCalls: ToolCallLike[];
57
+ }) => void;
58
+ }
59
+
60
+ /** Shape LangChain emits on AIMessage.tool_calls (duck-typed). */
61
+ export interface ToolCallLike {
62
+ name: string;
63
+ args: Record<string, unknown>;
64
+ id?: string;
65
+ }
66
+
67
+ export interface ToolErrorRecord {
68
+ iteration: number;
69
+ toolName: string;
70
+ toolCallId?: string;
71
+ error: string;
72
+ /** Raw args the model passed — useful for diagnosing schema drift. */
73
+ args?: Record<string, unknown>;
74
+ }
75
+
76
+ export interface FlushLoopResult {
77
+ /** Number of model.invoke() calls actually made. */
78
+ iterations: number;
79
+ /** Every `memory_append` tool_call the model emitted across all iterations. */
80
+ appendsAttempted: number;
81
+ /** How many of those returned `{ ok: true, ... }`. */
82
+ appendsSucceeded: number;
83
+ /** Tool errors the model saw (and may have reacted to). */
84
+ toolErrors: ToolErrorRecord[];
85
+ /** True if the final AIMessage text matched {@link SILENT_REPLY_TOKEN}. */
86
+ silentReply: boolean;
87
+ /** Whether the loop stopped because it hit `maxIterations`. */
88
+ hitIterationCap: boolean;
89
+ /** Final AIMessage content as plain text (best-effort). */
90
+ finalText: string;
91
+ /** Full message transcript — useful for debugging / tests. */
92
+ messages: BaseMessage[];
93
+ }
94
+
95
+ /**
96
+ * Extract a flat text string from any AIMessage content shape
97
+ * (string, array of content blocks, etc).
98
+ */
99
+ export function extractText(message: AIMessage): string {
100
+ const c = message.content;
101
+ if (typeof c === 'string') return c;
102
+ if (!Array.isArray(c)) return '';
103
+ return c
104
+ .map((block) => {
105
+ if (typeof block === 'string') return block;
106
+ if (block && typeof block === 'object' && 'text' in block) {
107
+ return String((block as { text?: unknown }).text ?? '');
108
+ }
109
+ return '';
110
+ })
111
+ .join('')
112
+ .trim();
113
+ }
114
+
115
+ /**
116
+ * Parse a tool result string. Memory tools return JSON with
117
+ * `{ ok: boolean, error?: string, path?: string }`. Non-JSON payloads
118
+ * are treated as opaque success strings.
119
+ */
120
+ export function parseToolResult(raw: string): {
121
+ ok: boolean;
122
+ error?: string;
123
+ path?: string;
124
+ } {
125
+ try {
126
+ const parsed = JSON.parse(raw);
127
+ if (parsed && typeof parsed === 'object' && 'ok' in parsed) {
128
+ return {
129
+ ok: Boolean(parsed.ok),
130
+ error: typeof parsed.error === 'string' ? parsed.error : undefined,
131
+ path: typeof parsed.path === 'string' ? parsed.path : undefined,
132
+ };
133
+ }
134
+ return { ok: true };
135
+ } catch {
136
+ return { ok: true };
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Bind a tool array to a model if `bindTools` exists on the model.
142
+ * Exported so the caller (runMemoryFlush) can do it once and pass the
143
+ * bound model in, keeping this loop free of LangChain model-specific
144
+ * extensions.
145
+ */
146
+ export function bindToolsIfSupported(
147
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
148
+ model: any,
149
+ tools: StructuredToolInterface[]
150
+ ): InvokableModel {
151
+ if (typeof model?.bindTools === 'function') {
152
+ return model.bindTools(tools) as InvokableModel;
153
+ }
154
+ return model as InvokableModel;
155
+ }
156
+
157
+ /**
158
+ * Run the agentic reflection loop until the model stops emitting
159
+ * tool_calls, the iteration cap is hit, or the final reply matches
160
+ * {@link SILENT_REPLY_TOKEN}.
161
+ */
162
+ export async function runFlushLoop(
163
+ params: FlushLoopParams
164
+ ): Promise<FlushLoopResult> {
165
+ const {
166
+ model,
167
+ tools,
168
+ initialMessages,
169
+ maxIterations = DEFAULT_MAX_FLUSH_ITERATIONS,
170
+ onIteration,
171
+ } = params;
172
+
173
+ const toolMap = new Map<string, StructuredToolInterface>();
174
+ for (const t of tools) {
175
+ toolMap.set(t.name, t);
176
+ }
177
+
178
+ const messages: BaseMessage[] = [...initialMessages];
179
+ const toolErrors: ToolErrorRecord[] = [];
180
+ let appendsAttempted = 0;
181
+ let appendsSucceeded = 0;
182
+ let iterations = 0;
183
+ let finalText = '';
184
+ let hitIterationCap = false;
185
+ // Tracks whether the most recent iteration ended with unresolved
186
+ // tool_calls. If we exit the while via the cap (not via the natural
187
+ // `break`), this stays true and we flag hitIterationCap.
188
+ let lastIterationHadPendingCalls = false;
189
+
190
+ while (iterations < maxIterations) {
191
+ iterations += 1;
192
+ const ai = await model.invoke(messages);
193
+ messages.push(ai);
194
+
195
+ const toolCalls = (ai.tool_calls ?? []) as ToolCallLike[];
196
+ onIteration?.({ i: iterations, ai, toolCalls });
197
+
198
+ if (toolCalls.length === 0) {
199
+ finalText = extractText(ai);
200
+ lastIterationHadPendingCalls = false;
201
+ break;
202
+ }
203
+ lastIterationHadPendingCalls = true;
204
+
205
+ for (const call of toolCalls) {
206
+ if (call.name === MEMORY_APPEND_TOOL_NAME) {
207
+ appendsAttempted += 1;
208
+ }
209
+ const tool = toolMap.get(call.name);
210
+ if (!tool) {
211
+ const errStr = `tool_not_found: ${call.name}`;
212
+ toolErrors.push({
213
+ iteration: iterations,
214
+ toolName: call.name,
215
+ toolCallId: call.id,
216
+ error: errStr,
217
+ args: call.args,
218
+ });
219
+ messages.push(
220
+ new ToolMessage({
221
+ content: JSON.stringify({ ok: false, error: errStr }),
222
+ tool_call_id: call.id ?? '',
223
+ name: call.name,
224
+ })
225
+ );
226
+ continue;
227
+ }
228
+
229
+ let rawResult: string;
230
+ try {
231
+ // LangChain tools accept the parsed args object.
232
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
233
+ const res = await (tool as any).invoke(call.args);
234
+ rawResult = typeof res === 'string' ? res : JSON.stringify(res);
235
+ } catch (err) {
236
+ const errStr = err instanceof Error ? err.message : String(err);
237
+ toolErrors.push({
238
+ iteration: iterations,
239
+ toolName: call.name,
240
+ toolCallId: call.id,
241
+ error: errStr,
242
+ args: call.args,
243
+ });
244
+ messages.push(
245
+ new ToolMessage({
246
+ content: JSON.stringify({ ok: false, error: errStr }),
247
+ tool_call_id: call.id ?? '',
248
+ name: call.name,
249
+ })
250
+ );
251
+ continue;
252
+ }
253
+
254
+ const parsed = parseToolResult(rawResult);
255
+ if (call.name === MEMORY_APPEND_TOOL_NAME) {
256
+ if (parsed.ok) {
257
+ appendsSucceeded += 1;
258
+ } else if (parsed.error) {
259
+ toolErrors.push({
260
+ iteration: iterations,
261
+ toolName: call.name,
262
+ toolCallId: call.id,
263
+ error: parsed.error,
264
+ args: call.args,
265
+ });
266
+ }
267
+ }
268
+
269
+ messages.push(
270
+ new ToolMessage({
271
+ content: rawResult,
272
+ tool_call_id: call.id ?? '',
273
+ name: call.name,
274
+ })
275
+ );
276
+ }
277
+ }
278
+
279
+ if (iterations >= maxIterations && lastIterationHadPendingCalls) {
280
+ hitIterationCap = true;
281
+ // Surface the last AIMessage text (if any) so callers can still log it.
282
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
283
+ const m = messages[i];
284
+ if (m instanceof AIMessage) {
285
+ finalText = extractText(m);
286
+ break;
287
+ }
288
+ }
289
+ }
290
+
291
+ const silentReply = finalText.trim() === SILENT_REPLY_TOKEN;
292
+
293
+ return {
294
+ iterations,
295
+ appendsAttempted,
296
+ appendsSucceeded,
297
+ toolErrors,
298
+ silentReply,
299
+ hitIterationCap,
300
+ finalText,
301
+ messages,
302
+ };
303
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Memory flush phase — trigger logic + reflection invocation.
3
+ *
4
+ * Ported from upstream's post-turn flush handler. The agent is re-invoked
5
+ * with a reflection system prompt and the `memory_append` tool unlocked;
6
+ * it writes notes to its future self, then the graph returns to normal.
7
+ *
8
+ * This module is INTENTIONALLY decoupled from the specific graph runtime —
9
+ * it exposes pure functions (`shouldFlushMemory`) plus a runner that takes
10
+ * the model as a parameter, so the same logic works from `createAgentNode`,
11
+ * `MultiAgentGraph`, or a future graph backend that wants to override
12
+ * flush behaviour.
13
+ *
14
+ * The agentic tool-execution loop (invoke → run tool_calls → feed results
15
+ * back → repeat until stop) lives in {@link ./flushLoop}. This module wires
16
+ * it up: build tools, resolve prompts, flip the phase, call the loop,
17
+ * shape the rich result.
18
+ */
19
+ import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
20
+ import { HumanMessage, SystemMessage } from '@langchain/core/messages';
21
+ import {
22
+ DEFAULT_FLUSH_RESERVE_FLOOR_TOKENS,
23
+ DEFAULT_FLUSH_SOFT_THRESHOLD_TOKENS,
24
+ DEFAULT_MAX_FLUSH_ITERATIONS,
25
+ MEMORY_PHASE_FLUSHING,
26
+ MEMORY_PHASE_NORMAL,
27
+ } from '@/memory/constants';
28
+ import type { MemoryConfig } from '@/memory/types';
29
+ import { buildMemoryTools } from '@/tools/memory';
30
+ import { resolveFlushPrompts } from '@/prompts/memoryFlushPrompt';
31
+ import {
32
+ bindToolsIfSupported,
33
+ runFlushLoop,
34
+ type FlushLoopResult,
35
+ type ToolErrorRecord,
36
+ } from './flushLoop';
37
+
38
+ export interface ShouldFlushInput {
39
+ currentTokens: number;
40
+ windowTokens: number;
41
+ reserveFloorTokens?: number;
42
+ softThresholdTokens?: number;
43
+ }
44
+
45
+ /**
46
+ * Pure trigger function: fires when the current context is within
47
+ * `softThreshold + reserveFloor` tokens of the model window. Matches
48
+ * upstream's formula.
49
+ */
50
+ export function shouldFlushMemory(input: ShouldFlushInput): boolean {
51
+ if (
52
+ !Number.isFinite(input.currentTokens) ||
53
+ !Number.isFinite(input.windowTokens)
54
+ ) {
55
+ return false;
56
+ }
57
+ if (input.windowTokens <= 0) return false;
58
+ const reserve =
59
+ input.reserveFloorTokens ?? DEFAULT_FLUSH_RESERVE_FLOOR_TOKENS;
60
+ const soft = input.softThresholdTokens ?? DEFAULT_FLUSH_SOFT_THRESHOLD_TOKENS;
61
+ return input.currentTokens >= input.windowTokens - reserve - soft;
62
+ }
63
+
64
+ export interface RunFlushParams {
65
+ model: BaseChatModel;
66
+ memory: MemoryConfig;
67
+ /** A compact summary of the conversation — last N turns, not raw history. */
68
+ conversationSummary: string;
69
+ /**
70
+ * Accessor the graph runtime uses to expose the current phase to the
71
+ * append tool. The runner sets this to `memory_flushing` for the duration
72
+ * of the reflection turn and restores it on exit.
73
+ */
74
+ setPhase: (
75
+ phase: typeof MEMORY_PHASE_NORMAL | typeof MEMORY_PHASE_FLUSHING
76
+ ) => void;
77
+ /**
78
+ * @deprecated No longer used — the 8-path canonical-document model
79
+ * does not date-stamp files. Kept in the params shape for one release
80
+ * so host's callers don't break on the type signature.
81
+ */
82
+ timezone?: string;
83
+ /**
84
+ * @deprecated Same as `timezone` — unused in the canonical-document model.
85
+ */
86
+ nowMs?: number;
87
+ /** Override for the agentic-loop iteration cap. */
88
+ maxIterations?: number;
89
+ /**
90
+ * Optional per-iteration callback for structured debug logging.
91
+ * Caller is expected to demote this to debug once the rollout is stable.
92
+ */
93
+ onIteration?: (event: {
94
+ i: number;
95
+ toolCallCount: number;
96
+ toolNames: string[];
97
+ }) => void;
98
+ }
99
+
100
+ export interface RunFlushResult {
101
+ /** Did the flush actually run (false if disabled via config). */
102
+ ran: boolean;
103
+ /** Model.invoke() iterations actually performed. */
104
+ iterations?: number;
105
+ /** Total `memory_append` calls the model emitted. */
106
+ appendsAttempted?: number;
107
+ /** Subset that returned `{ ok: true }` from the backend. */
108
+ appendsSucceeded?: number;
109
+ /** Tool errors the model saw during the flush — surfaced for logging. */
110
+ toolErrors?: ToolErrorRecord[];
111
+ /** Final text reply equalled SILENT_REPLY_TOKEN (`NO_REPLY`). */
112
+ silentReply?: boolean;
113
+ /** Loop hit its iteration cap with tool_calls still pending. */
114
+ hitIterationCap?: boolean;
115
+ /** Final text reply from the last AIMessage. */
116
+ finalText?: string;
117
+ /** Error message if the flush threw. */
118
+ error?: string;
119
+ }
120
+
121
+ /**
122
+ * Run the reflection turn. The model is re-invoked with just the flush
123
+ * prompt + compact summary + the append tool unlocked. We intentionally
124
+ * drop the full history — the prompt tells the agent to write notes based
125
+ * on what it learned, not to continue the conversation.
126
+ *
127
+ * The loop keeps invoking the model until it stops emitting tool_calls,
128
+ * hits the iteration cap, or replies with {@link SILENT_REPLY_TOKEN}. Each
129
+ * `memory_append` tool_call is executed against the pgvector backend and
130
+ * the result (success path or error) is fed back as a ToolMessage so the
131
+ * model can self-correct (e.g. retry with a valid path).
132
+ */
133
+ export async function runMemoryFlush(
134
+ params: RunFlushParams
135
+ ): Promise<RunFlushResult> {
136
+ const {
137
+ model,
138
+ memory,
139
+ conversationSummary,
140
+ setPhase,
141
+ maxIterations,
142
+ onIteration,
143
+ } = params;
144
+ if (memory.flush?.enabled === false) {
145
+ return { ran: false };
146
+ }
147
+
148
+ setPhase(MEMORY_PHASE_FLUSHING);
149
+ try {
150
+ const tools = buildMemoryTools({
151
+ ...memory,
152
+ readEnabled: false,
153
+ writeEnabled: true,
154
+ getPhase: () => MEMORY_PHASE_FLUSHING,
155
+ });
156
+
157
+ // Bind tools to the caller-provided model. If the model already came
158
+ // bound (some graph wrappers do), bindToolsIfSupported is a no-op.
159
+ const bound = bindToolsIfSupported(model, tools);
160
+
161
+ // Resolve flush prompts with the scope-aware rubric. For an
162
+ // isolated agent (no userId in scope), the user-tier paths are
163
+ // filtered out of the rubric so the LLM never sees them — it
164
+ // physically cannot route a write to `memory/user/*` because the
165
+ // prompt never lists those paths.
166
+ const { systemPrompt, prompt } = resolveFlushPrompts({
167
+ scope: memory.scope,
168
+ });
169
+ const initialMessages = [
170
+ new SystemMessage(systemPrompt),
171
+ new HumanMessage(
172
+ `${prompt}\n\nConversation context:\n\n${conversationSummary}`
173
+ ),
174
+ ];
175
+
176
+ const loop: FlushLoopResult = await runFlushLoop({
177
+ model: bound,
178
+ tools,
179
+ initialMessages,
180
+ maxIterations: maxIterations ?? DEFAULT_MAX_FLUSH_ITERATIONS,
181
+ onIteration: onIteration
182
+ ? (ev): void =>
183
+ onIteration({
184
+ i: ev.i,
185
+ toolCallCount: ev.toolCalls.length,
186
+ toolNames: ev.toolCalls.map((c) => c.name),
187
+ })
188
+ : undefined,
189
+ });
190
+
191
+ return {
192
+ ran: true,
193
+ iterations: loop.iterations,
194
+ appendsAttempted: loop.appendsAttempted,
195
+ appendsSucceeded: loop.appendsSucceeded,
196
+ toolErrors: loop.toolErrors,
197
+ silentReply: loop.silentReply,
198
+ hitIterationCap: loop.hitIterationCap,
199
+ finalText: loop.finalText,
200
+ };
201
+ } catch (err) {
202
+ return {
203
+ ran: false,
204
+ error: err instanceof Error ? err.message : String(err),
205
+ };
206
+ } finally {
207
+ setPhase(MEMORY_PHASE_NORMAL);
208
+ }
209
+ }
package/src/index.ts CHANGED
@@ -5,7 +5,7 @@ export * from './splitStream';
5
5
  export * from './events';
6
6
  export * from './messages';
7
7
 
8
- /* Observability types re-exported for ranger consumption */
8
+ /* Observability types re-exported for host consumption */
9
9
  export type {
10
10
  GuardrailTraceData,
11
11
  GuardrailOutcome,
@@ -25,6 +25,35 @@ export * from './tools/AskUser';
25
25
  export * from './tools/schema';
26
26
  export * from './tools/handlers';
27
27
  export * from './tools/search';
28
+ export * from './tools/memory';
29
+
30
+ /* Memory (storage + factory) */
31
+ export * from './memory';
32
+
33
+ /* Prompts */
34
+ export { MEMORY_FLUSH_SYSTEM_PROMPT } from './prompts/memoryFlushPrompt';
35
+
36
+ /* Phases */
37
+ export {
38
+ shouldFlushMemory,
39
+ runMemoryFlush,
40
+ } from './graphs/phases/memoryFlushPhase';
41
+ export type {
42
+ RunFlushParams,
43
+ RunFlushResult,
44
+ } from './graphs/phases/memoryFlushPhase';
45
+ export {
46
+ runFlushLoop,
47
+ bindToolsIfSupported,
48
+ extractText as extractFlushText,
49
+ parseToolResult as parseFlushToolResult,
50
+ } from './graphs/phases/flushLoop';
51
+ export type {
52
+ FlushLoopParams,
53
+ FlushLoopResult,
54
+ ToolCallLike,
55
+ ToolErrorRecord,
56
+ } from './graphs/phases/flushLoop';
28
57
 
29
58
  /* Schemas */
30
59
  export * from './schemas';
@@ -172,14 +172,13 @@ export class IllumaBedrockConverse extends ChatBedrockConverse {
172
172
  * Some tools (e.g., MCP-sourced or dynamically created) may have empty or
173
173
  * missing descriptions. Patch them here to avoid Bedrock validation errors.
174
174
  */
175
- if (
176
- params.toolConfig?.tools &&
177
- Array.isArray(params.toolConfig.tools)
178
- ) {
175
+ if (params.toolConfig?.tools && Array.isArray(params.toolConfig.tools)) {
179
176
  for (const t of params.toolConfig.tools) {
180
177
  const spec = (t as { toolSpec?: { description?: string } }).toolSpec;
181
178
  if (spec && (!spec.description || spec.description === '')) {
182
- spec.description = spec.description || `Tool: ${(spec as { name?: string }).name ?? 'unknown'}`;
179
+ spec.description =
180
+ spec.description ||
181
+ `Tool: ${(spec as { name?: string }).name ?? 'unknown'}`;
183
182
  }
184
183
  }
185
184
  }
@@ -0,0 +1,61 @@
1
+ import {
2
+ decorateCitations,
3
+ resolveMemoryCitationsMode,
4
+ shouldIncludeCitations,
5
+ type CitationCandidate,
6
+ } from '../citations';
7
+
8
+ describe('resolveMemoryCitationsMode', () => {
9
+ it('accepts on/off/auto', () => {
10
+ expect(resolveMemoryCitationsMode('on')).toBe('on');
11
+ expect(resolveMemoryCitationsMode('off')).toBe('off');
12
+ expect(resolveMemoryCitationsMode('auto')).toBe('auto');
13
+ });
14
+ it('falls back to auto for anything else', () => {
15
+ expect(resolveMemoryCitationsMode(undefined)).toBe('auto');
16
+ expect(resolveMemoryCitationsMode('nonsense')).toBe('auto');
17
+ });
18
+ });
19
+
20
+ describe('shouldIncludeCitations', () => {
21
+ it('on → true, off → false, auto → true in direct chat', () => {
22
+ expect(shouldIncludeCitations('on')).toBe(true);
23
+ expect(shouldIncludeCitations('off')).toBe(false);
24
+ expect(shouldIncludeCitations('auto')).toBe(true);
25
+ });
26
+ });
27
+
28
+ describe('decorateCitations', () => {
29
+ it('strips citation when include=false', () => {
30
+ const hits = [
31
+ { path: 'memory/x.md', content: 'hello', citation: 'memory/x.md#L1' },
32
+ ];
33
+ const out = decorateCitations(hits, false);
34
+ expect(out[0].citation).toBeUndefined();
35
+ });
36
+
37
+ it('formats single-line citation as #L1', () => {
38
+ const hits: CitationCandidate[] = [
39
+ { path: 'memory/x.md', content: 'hello' },
40
+ ];
41
+ const out = decorateCitations(hits, true);
42
+ expect(out[0].citation).toBe('memory/x.md#L1');
43
+ expect(out[0].content).toContain('Source: memory/x.md#L1');
44
+ });
45
+
46
+ it('formats multi-line citation as #L1-LN', () => {
47
+ const hits: CitationCandidate[] = [
48
+ { path: 'memory/x.md', content: 'line1\nline2\nline3' },
49
+ ];
50
+ const out = decorateCitations(hits, true);
51
+ expect(out[0].citation).toBe('memory/x.md#L1-L3');
52
+ });
53
+
54
+ it('preserves caller-provided line range', () => {
55
+ const hits: CitationCandidate[] = [
56
+ { path: 'memory/x.md', content: 'ignored', startLine: 5, endLine: 12 },
57
+ ];
58
+ const out = decorateCitations(hits, true);
59
+ expect(out[0].citation).toBe('memory/x.md#L5-L12');
60
+ });
61
+ });