@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,101 @@
1
+ /**
2
+ * spawnPath — hierarchical invocation identity for nested multi-agent orchestration.
3
+ *
4
+ * A spawnPath is a slash-separated chain of spawnKeys from the root of the current
5
+ * agent invocation to the current spawn. The root agent has an empty spawnPath;
6
+ * each handoff/transfer/sequence that spawns a new subgraph appends a new spawnKey.
7
+ *
8
+ * Examples:
9
+ * "" → primary agent (no spawn)
10
+ * "call_abc" → first-level handoff child
11
+ * "call_abc/call_def" → grandchild (depth 2)
12
+ * "call_abc/call_def/call_ghi" → depth 3
13
+ *
14
+ * These utilities are the single source of truth for path manipulation across:
15
+ * - @illuma-ai/agents (MultiAgentGraph, HandoffRegistry, callbacks)
16
+ * - host api (initialize.js, callbacks.js, ExecutionTrace writes)
17
+ * - host client (subagent store, sidebar rendering)
18
+ *
19
+ * See docs/multi-agent-nesting-architecture.md for the full design.
20
+ */
21
+
22
+ /** Separator between spawnKeys in a spawnPath. Chosen so that the path looks
23
+ * like a filesystem/URL path, which makes it easy to read in logs and traces. */
24
+ export const SPAWN_PATH_SEP = '/';
25
+
26
+ /** Hard cap on nested multi-agent invocations. Prevents runaway recursion.
27
+ * Can be overridden at the host api layer via MULTI_AGENT_MAX_NESTING_DEPTH. */
28
+ export const MAX_NESTING_DEPTH = 5;
29
+
30
+ /**
31
+ * Append a spawnKey to a parent spawnPath.
32
+ *
33
+ * @param parent - Parent spawnPath (may be undefined/null/empty for root)
34
+ * @param key - spawnKey to append
35
+ * @returns New spawnPath string
36
+ */
37
+ export function buildSpawnPath(
38
+ parent: string | undefined | null,
39
+ key: string
40
+ ): string {
41
+ if (!key) {
42
+ throw new Error('[spawnPath] buildSpawnPath called with empty key');
43
+ }
44
+ if (parent == null || parent === '') return key;
45
+ return `${parent}${SPAWN_PATH_SEP}${key}`;
46
+ }
47
+
48
+ /**
49
+ * Compute the depth of a spawnPath.
50
+ * Root (empty) → 0; single-segment → 1; etc.
51
+ */
52
+ export function spawnPathDepth(path: string | undefined | null): number {
53
+ if (path == null || path === '') return 0;
54
+ return path.split(SPAWN_PATH_SEP).filter(Boolean).length;
55
+ }
56
+
57
+ /**
58
+ * Return the parent spawnPath, or null if the input is already root.
59
+ *
60
+ * - parentSpawnPath("a/b/c") === "a/b"
61
+ * - parentSpawnPath("a") === ""
62
+ * - parentSpawnPath("") === null
63
+ */
64
+ export function parentSpawnPath(
65
+ path: string | undefined | null
66
+ ): string | null {
67
+ if (path == null || path === '') return null;
68
+ const parts = path.split(SPAWN_PATH_SEP).filter(Boolean);
69
+ if (parts.length <= 1) return '';
70
+ return parts.slice(0, -1).join(SPAWN_PATH_SEP);
71
+ }
72
+
73
+ /** Split a spawnPath into its constituent spawnKey segments. */
74
+ export function spawnPathParts(path: string | undefined | null): string[] {
75
+ if (path == null || path === '') return [];
76
+ return path.split(SPAWN_PATH_SEP).filter(Boolean);
77
+ }
78
+
79
+ /**
80
+ * Return the last spawnKey in a spawnPath (the "current" spawn).
81
+ * Returns null for root.
82
+ */
83
+ export function leafSpawnKey(path: string | undefined | null): string | null {
84
+ const parts = spawnPathParts(path);
85
+ return parts.length === 0 ? null : parts[parts.length - 1];
86
+ }
87
+
88
+ /**
89
+ * True if `ancestor` is a strict ancestor of `descendant`. Root ("") is
90
+ * ancestor of everything except itself.
91
+ */
92
+ export function isAncestorSpawnPath(
93
+ ancestor: string | undefined | null,
94
+ descendant: string | undefined | null
95
+ ): boolean {
96
+ const a = ancestor ?? '';
97
+ const d = descendant ?? '';
98
+ if (a === d) return false;
99
+ if (a === '') return d !== '';
100
+ return d.startsWith(a + SPAWN_PATH_SEP);
101
+ }
@@ -79,6 +79,9 @@ import { getChatModelClass, manualToolStreamProviders } from '@/llm/providers';
79
79
  import { ToolNode as CustomToolNode, toolsCondition } from '@/tools/ToolNode';
80
80
  import { ChatOpenAI, AzureChatOpenAI } from '@/llm/openai';
81
81
  import { safeDispatchCustomEvent } from '@/utils/events';
82
+ import { mlog, mwarn } from '@/utils/logging';
83
+ import { normalizeMessageToolCalls } from '@/utils/toolCallNormalization';
84
+ import { isTruncationReason } from '@/utils/finishReasons';
82
85
  import {
83
86
  detectDocuments,
84
87
  shouldInjectMultiDocHint,
@@ -1144,10 +1147,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1144
1147
  const resolved = agentContext.resolveStructuredOutputMode();
1145
1148
  method = resolved.method;
1146
1149
  if (resolved.warnings.length > 0) {
1147
- console.warn(
1148
- '[Graph] Structured output mode warnings:',
1149
- resolved.warnings
1150
- );
1150
+ mwarn('[Graph] Structured output mode warnings:', resolved.warnings);
1151
1151
  }
1152
1152
  } else {
1153
1153
  // Legacy fallback: use the old mode-based resolution
@@ -1172,7 +1172,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1172
1172
  );
1173
1173
  preparedSchema = prepared;
1174
1174
  if (warnings.length > 0) {
1175
- console.warn('[Graph] Schema preparation warnings:', warnings);
1175
+ mwarn('[Graph] Schema preparation warnings:', warnings);
1176
1176
  }
1177
1177
  }
1178
1178
 
@@ -1264,7 +1264,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1264
1264
  ? handleErrors
1265
1265
  : `The response did not match the expected schema. Error: ${lastError.message}. Please try again with a valid response.`;
1266
1266
 
1267
- console.warn(
1267
+ mwarn(
1268
1268
  `[Graph] Structured output attempt ${attempts} failed: ${lastError.message}. Retrying...`
1269
1269
  );
1270
1270
 
@@ -1467,7 +1467,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1467
1467
  this._toolDiscoveryCache.getNewDiscoveries(messages);
1468
1468
  if (cachedDiscoveries.length > 0) {
1469
1469
  agentContext.markToolsAsDiscovered(cachedDiscoveries);
1470
- console.debug(
1470
+ mlog(
1471
1471
  `[Graph:ToolDiscovery] Cached ${cachedDiscoveries.length} new tools (total: ${this._toolDiscoveryCache.size})`
1472
1472
  );
1473
1473
  }
@@ -1497,6 +1497,14 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1497
1497
  clientOptions: effectiveClientOptions,
1498
1498
  });
1499
1499
 
1500
+ // DEBUG: Log which model and tools each agent uses during handoff
1501
+ mlog(
1502
+ `[createCallModel] Agent "${agentId}" invoking LLM | provider=${agentContext.provider} | ` +
1503
+ `model=${(effectiveClientOptions as Record<string, unknown>).model ?? 'default'} | ` +
1504
+ `toolsForBinding=${toolsForBinding?.length ?? 0} | ` +
1505
+ `toolNames=[${(toolsForBinding ?? []).map((t) => (t as { name?: string }).name ?? 'unknown').join(', ')}]`
1506
+ );
1507
+
1500
1508
  if (agentContext.systemRunnable) {
1501
1509
  model = agentContext.systemRunnable.pipe(model as Runnable);
1502
1510
  }
@@ -1507,7 +1515,15 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1507
1515
  if (!config.signal) {
1508
1516
  config.signal = this.signal;
1509
1517
  }
1510
- this.config = config;
1518
+ // First-writer-wins: `this.config` is used ONLY as a "has a run started"
1519
+ // existence flag by the dispatch* methods (they never read its value —
1520
+ // they read the current RunnableConfig from LangChain AsyncLocalStorage).
1521
+ // Unconditionally reassigning here races across concurrent child
1522
+ // subgraph.invoke() calls under parallel multi-agent handoffs; the last
1523
+ // writer wins, and any dispatch firing between writes would historically
1524
+ // have been tagged with the wrong child's metadata. Keeping the first
1525
+ // write pinned makes this a true flag, eliminating the race.
1526
+ this.config ??= config;
1511
1527
 
1512
1528
  let messagesToUse = messages;
1513
1529
 
@@ -1611,7 +1627,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1611
1627
 
1612
1628
  if (oldMessages.length > 0) {
1613
1629
  this._summaryInFlight = true;
1614
- console.debug(
1630
+ mlog(
1615
1631
  `[Graph:ProactiveSummary] Context at ${utilization.toFixed(1)}% (threshold ${threshold}%) — summarizing ${oldMessages.length} older msgs in background`
1616
1632
  );
1617
1633
 
@@ -1620,7 +1636,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1620
1636
  .then((updated) => {
1621
1637
  if (updated != null && updated !== '') {
1622
1638
  this._cachedRunSummary = updated;
1623
- console.debug(
1639
+ mlog(
1624
1640
  `[Graph:ProactiveSummary] Background summary ready (len=${updated.length})`
1625
1641
  );
1626
1642
  }
@@ -1830,7 +1846,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1830
1846
  }
1831
1847
  agentContext.indexTokenCountMap = viewTokenMap;
1832
1848
 
1833
- console.debug(
1849
+ mlog(
1834
1850
  `[Graph:Compaction] ${messages.length}→${viewParts.length} msgs | ` +
1835
1851
  `compacted=${compactedMessages.length} window=${recentMessages.length} | ` +
1836
1852
  `summary=${summarySource} | budget=${usedTokens}/${recentBudget}` +
@@ -1854,7 +1870,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1854
1870
  if (shouldSummarize) {
1855
1871
  if (this._summaryInFlight) {
1856
1872
  this._pendingMessagesToRefine.push(...compactedMessages);
1857
- console.debug(
1873
+ mlog(
1858
1874
  `[Graph:Compaction] Summary in-flight, queued ${compactedMessages.length} msgs (pending=${this._pendingMessagesToRefine.length})`
1859
1875
  );
1860
1876
  } else {
@@ -1907,7 +1923,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1907
1923
  deduplicateSystemMessages(messagesToUse);
1908
1924
  if (removedCount > 0) {
1909
1925
  messagesToUse = dedupedMessages;
1910
- console.debug(
1926
+ mlog(
1911
1927
  `[Graph:Dedup] Removed ${removedCount} duplicate system message(s)`
1912
1928
  );
1913
1929
  }
@@ -2031,7 +2047,6 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2031
2047
  );
2032
2048
  }
2033
2049
 
2034
-
2035
2050
  // Get model info for analytics
2036
2051
  const bedrockOpts = agentContext.clientOptions as
2037
2052
  | t.BedrockAnthropicClientOptions
@@ -2145,11 +2160,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2145
2160
 
2146
2161
  // Log when we detect the error
2147
2162
  if (isInputTooLongError) {
2148
- console.warn(
2163
+ mwarn(
2149
2164
  '[Graph] Detected input too long error:',
2150
2165
  errorMessage.substring(0, 200)
2151
2166
  );
2152
- console.warn('[Graph] Checking emergency pruning conditions:', {
2167
+ mwarn('[Graph] Checking emergency pruning conditions:', {
2153
2168
  hasPruneMessages: !!agentContext.pruneMessages,
2154
2169
  hasTokenCounter: !!agentContext.tokenCounter,
2155
2170
  maxContextTokens: agentContext.maxContextTokens,
@@ -2174,7 +2189,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2174
2189
  const reducedMaxTokens = Math.floor(
2175
2190
  agentContext.maxContextTokens! * reductionFactor
2176
2191
  );
2177
- console.warn(
2192
+ mwarn(
2178
2193
  `[Graph] Input too long. Retrying with ${reductionFactor * 100}% context (${reducedMaxTokens} tokens)...`
2179
2194
  );
2180
2195
 
@@ -2182,7 +2197,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2182
2197
  // This is needed when messages were dynamically added without updating the token map
2183
2198
  let tokenMapForPruning = agentContext.indexTokenCountMap;
2184
2199
  if (Object.keys(tokenMapForPruning).length < messages.length) {
2185
- console.warn(
2200
+ mwarn(
2186
2201
  '[Graph] Building fresh token count map for emergency pruning...'
2187
2202
  );
2188
2203
  tokenMapForPruning = {};
@@ -2207,7 +2222,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2207
2222
 
2208
2223
  // Skip if we can't fit any messages
2209
2224
  if (reducedMessages.length === 0) {
2210
- console.warn(
2225
+ mwarn(
2211
2226
  `[Graph] Cannot fit any messages at ${reductionFactor * 100}% reduction, trying next level...`
2212
2227
  );
2213
2228
  continue;
@@ -2290,7 +2305,7 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2290
2305
  retryErrorMsg.includes('validationexception');
2291
2306
 
2292
2307
  if (stillTooLong && reductionFactor > 0.1) {
2293
- console.warn(
2308
+ mwarn(
2294
2309
  `[Graph] Still too long at ${reductionFactor * 100}%, trying more aggressive pruning...`
2295
2310
  );
2296
2311
  } else {
@@ -2362,6 +2377,27 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2362
2377
  * handled everything — both paths become no-ops.
2363
2378
  */
2364
2379
  const responseMessage = result.messages?.[0];
2380
+
2381
+ // Tool-call name normalization — catches LLM output that names tools
2382
+ // with wrong delimiters (outlook/operations), prefixes
2383
+ // (functions.outlook_operations), case drift, counter suffixes, or
2384
+ // empty names recoverable from the tool_call id. Mutates in place so
2385
+ // the downstream ToolNode dispatch sees the corrected names.
2386
+ if (responseMessage && agentContext.toolMap) {
2387
+ const allowedNames = new Set(Object.keys(agentContext.toolMap));
2388
+ if (allowedNames.size > 0) {
2389
+ const rewrote = normalizeMessageToolCalls(
2390
+ responseMessage,
2391
+ allowedNames
2392
+ );
2393
+ if (rewrote) {
2394
+ mlog(
2395
+ `[Graph] normalized tool_call names on agent "${agentId}" response`
2396
+ );
2397
+ }
2398
+ }
2399
+ }
2400
+
2365
2401
  const toolCalls = (responseMessage as AIMessageChunk | undefined)
2366
2402
  ?.tool_calls;
2367
2403
  const hasToolCalls = Array.isArray(toolCalls) && toolCalls.length > 0;
@@ -2477,12 +2513,22 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2477
2513
  const messageStop = meta.messageStop as
2478
2514
  | Record<string, unknown>
2479
2515
  | undefined;
2480
- this.lastFinishReason =
2516
+ const nextReason =
2481
2517
  (meta.finish_reason as string | undefined) ?? // OpenAI/Azure
2482
2518
  (meta.stop_reason as string | undefined) ?? // Anthropic direct API
2483
2519
  (meta.stopReason as string | undefined) ?? // Bedrock invoke (non-streaming)
2484
2520
  (messageStop?.stopReason as string | undefined) ?? // Bedrock streaming
2485
2521
  (meta.finishReason as string | undefined); // VertexAI/Google
2522
+
2523
+ // Sticky on truncation: a single Graph instance is reused across
2524
+ // every scoped-subgraph inner node invocation (see MultiAgentGraph
2525
+ // buildScopedSubgraph). If an earlier inner node hit max_tokens
2526
+ // but a later inner node finished cleanly, the host's continuation layer
2527
+ // would miss the truncation signal unless we preserve it. Keep the
2528
+ // truncation reason pinned so the outer caller can retry.
2529
+ if (!isTruncationReason(this.lastFinishReason)) {
2530
+ this.lastFinishReason = nextReason;
2531
+ }
2486
2532
  }
2487
2533
 
2488
2534
  this.cleanupSignalListener();
@@ -2545,7 +2591,7 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2545
2591
  '[Graph] Deferred structured output failed after successful tool use:',
2546
2592
  structuredError
2547
2593
  );
2548
- console.warn(
2594
+ mwarn(
2549
2595
  '[Graph] Falling back to unstructured response from tool-use phase'
2550
2596
  );
2551
2597
  return result;
@@ -2570,7 +2616,10 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2570
2616
  state: t.BaseGraphState,
2571
2617
  config?: RunnableConfig
2572
2618
  ): string => {
2573
- this.config = config;
2619
+ // First-writer-wins — see note in createCallModel. `this.config` is an
2620
+ // existence flag only; assigning unconditionally would race under
2621
+ // parallel child subgraph.invoke().
2622
+ this.config ??= config;
2574
2623
  return toolsCondition(state, toolNode, this.invokedToolIds);
2575
2624
  };
2576
2625
 
@@ -2615,10 +2664,16 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2615
2664
  default: () => [],
2616
2665
  }),
2617
2666
  });
2667
+ // Pass compileOptions (including the HITL checkpointer) to the OUTER
2668
+ // workflow — not just the inner agent subgraph. hasInterrupts() calls
2669
+ // getState() on the outer compiled graph; without a checkpointer here,
2670
+ // getState reports zero tasks and the HITL interrupt/resume loop breaks
2671
+ // out immediately even though interrupt() fired correctly inside the
2672
+ // agent subgraph.
2618
2673
  const workflow = new StateGraph(StateAnnotation)
2619
2674
  .addNode(this.defaultAgentId, agentNode, { ends: [END] })
2620
2675
  .addEdge(START, this.defaultAgentId)
2621
- .compile();
2676
+ .compile(this.compileOptions as unknown as never);
2622
2677
 
2623
2678
  return workflow;
2624
2679
  }
@@ -2701,7 +2756,7 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2701
2756
  }
2702
2757
  } catch (_e) {
2703
2758
  /** If we can't get agent context, that's okay - agentId remains undefined */
2704
- console.debug(
2759
+ mlog(
2705
2760
  `[dispatchRunStep] Could not resolve agentId from metadata.langgraph_node="${(metadata as Record<string, unknown>).langgraph_node}": ${(_e as Error).message}`
2706
2761
  );
2707
2762
  }
@@ -2709,11 +2764,11 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2709
2764
 
2710
2765
  this.contentData.push(runStep);
2711
2766
  this.contentIndexMap.set(stepId, runStep.index);
2712
- await safeDispatchCustomEvent(
2713
- GraphEvents.ON_RUN_STEP,
2714
- runStep,
2715
- this.config
2716
- );
2767
+ // Pass undefined so safeDispatchCustomEvent resolves the runnable config
2768
+ // from LangChain's AsyncLocalStorage. Using the shared `this.config` would
2769
+ // race across concurrent child subgraph.invoke calls under parallel
2770
+ // multi-agent handoffs and tag events with the wrong child's spawnKey.
2771
+ await safeDispatchCustomEvent(GraphEvents.ON_RUN_STEP, runStep);
2717
2772
  return stepId;
2718
2773
  }
2719
2774
 
@@ -2854,7 +2909,7 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2854
2909
  }
2855
2910
 
2856
2911
  if (!data.id) {
2857
- console.warn('No Tool ID provided for Tool Error');
2912
+ mwarn('No Tool ID provided for Tool Error');
2858
2913
  return;
2859
2914
  }
2860
2915
 
@@ -2919,11 +2974,10 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2919
2974
  id,
2920
2975
  delta,
2921
2976
  };
2922
- await safeDispatchCustomEvent(
2923
- GraphEvents.ON_RUN_STEP_DELTA,
2924
- runStepDelta,
2925
- this.config
2926
- );
2977
+ // See dispatchRunStep note: do not pass `this.config`. The implicit
2978
+ // AsyncLocalStorage config is the correct per-async-branch source under
2979
+ // parallel handoffs.
2980
+ await safeDispatchCustomEvent(GraphEvents.ON_RUN_STEP_DELTA, runStepDelta);
2927
2981
  }
2928
2982
 
2929
2983
  async dispatchMessageDelta(id: string, delta: t.MessageDelta): Promise<void> {
@@ -2934,11 +2988,8 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2934
2988
  id,
2935
2989
  delta,
2936
2990
  };
2937
- await safeDispatchCustomEvent(
2938
- GraphEvents.ON_MESSAGE_DELTA,
2939
- messageDelta,
2940
- this.config
2941
- );
2991
+ // See dispatchRunStep note.
2992
+ await safeDispatchCustomEvent(GraphEvents.ON_MESSAGE_DELTA, messageDelta);
2942
2993
  }
2943
2994
 
2944
2995
  dispatchReasoningDelta = async (
@@ -2952,10 +3003,10 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2952
3003
  id: stepId,
2953
3004
  delta,
2954
3005
  };
3006
+ // See dispatchRunStep note.
2955
3007
  await safeDispatchCustomEvent(
2956
3008
  GraphEvents.ON_REASONING_DELTA,
2957
- reasoningDelta,
2958
- this.config
3009
+ reasoningDelta
2959
3010
  );
2960
3011
  };
2961
3012
  }
@@ -0,0 +1,199 @@
1
+ import type { BaseMessage } from '@langchain/core/messages';
2
+ import type * as t from '@/types';
3
+
4
+ /**
5
+ * Tracks the lifecycle of a spawned handoff child agent.
6
+ */
7
+ export type HandoffRecord = {
8
+ /** Agent identity (destination agentId) — stable across multiple spawns of the same agent */
9
+ id: string;
10
+ /** Unique internal key for this specific spawn (agentId + monotonic counter) */
11
+ spawnKey: string;
12
+ /** Display name of the child agent */
13
+ name: string;
14
+ /** Task description / instructions passed to child */
15
+ task: string;
16
+ /** When the handoff was spawned */
17
+ spawnedAt: number;
18
+ /** Current status */
19
+ status: 'pending' | 'running' | 'completed' | 'failed';
20
+ /** The background promise executing the child subgraph */
21
+ promise: Promise<t.BaseGraphState>;
22
+ /** Resolved result text (populated on completion) */
23
+ resultText?: string;
24
+ /** Error message (populated on failure) */
25
+ error?: string;
26
+ /** Duration in ms (populated on completion/failure) */
27
+ durationMs?: number;
28
+ /** Number of messages in child's output */
29
+ resultMessageCount?: number;
30
+ };
31
+
32
+ /**
33
+ * Registry for async handoff execution.
34
+ *
35
+ * Enables the autonomous orchestration pattern:
36
+ * 1. Orchestrator spawns children (non-blocking)
37
+ * 2. Orchestrator stays alive to reason, spawn more, or check status
38
+ * 3. Orchestrator collects results when ready
39
+ *
40
+ * Scoped per MultiAgentGraph instance — each orchestrator graph gets its own registry.
41
+ */
42
+ export class HandoffRegistry {
43
+ private records: Map<string, HandoffRecord> = new Map();
44
+ /** Monotonically increasing counter for unique spawn IDs */
45
+ private spawnCounter = 0;
46
+
47
+ /**
48
+ * Register a spawned handoff child.
49
+ * The promise runs in the background — not awaited here.
50
+ * Uses a unique key per spawn so the same agent can be spawned multiple times
51
+ * across rounds without overwriting prior records.
52
+ */
53
+ spawn(params: {
54
+ id: string;
55
+ name: string;
56
+ task: string;
57
+ promise: Promise<t.BaseGraphState>;
58
+ extractResult: (messages: BaseMessage[], agentId: string) => string;
59
+ truncateResult: (text: string, maxChars: number) => string;
60
+ maxResultChars: number;
61
+ /** Callback when child completes (for SSE events) */
62
+ onComplete?: (record: HandoffRecord) => void;
63
+ }): void {
64
+ // Unique internal key: agentId + counter to support multiple spawns of same agent.
65
+ // record.id stays as the agent identity so callers/observability can attribute
66
+ // results to the agent regardless of which spawn round they came from.
67
+ const spawnKey = `${params.id}__${this.spawnCounter++}`;
68
+ const record: HandoffRecord = {
69
+ id: params.id,
70
+ spawnKey,
71
+ name: params.name,
72
+ task: params.task,
73
+ spawnedAt: Date.now(),
74
+ status: 'running',
75
+ promise: params.promise,
76
+ };
77
+
78
+ // Wire up the promise to update the record on completion
79
+ params.promise
80
+ .then((result) => {
81
+ const resultText = params.extractResult(result.messages, params.id);
82
+ const truncated = params.truncateResult(
83
+ resultText,
84
+ params.maxResultChars
85
+ );
86
+ record.status = 'completed';
87
+ record.resultText = truncated;
88
+ record.durationMs = Date.now() - record.spawnedAt;
89
+ record.resultMessageCount = result.messages.length;
90
+ params.onComplete?.(record);
91
+ })
92
+ .catch((err) => {
93
+ record.status = 'failed';
94
+ record.error = err instanceof Error ? err.message : String(err);
95
+ record.durationMs = Date.now() - record.spawnedAt;
96
+ params.onComplete?.(record);
97
+ });
98
+
99
+ this.records.set(spawnKey, record);
100
+ }
101
+
102
+ /** List all pending (running) handoffs */
103
+ listPending(): HandoffRecord[] {
104
+ return Array.from(this.records.values()).filter(
105
+ (r) => r.status === 'running'
106
+ );
107
+ }
108
+
109
+ /** List all completed handoffs (not yet collected) */
110
+ listCompleted(): HandoffRecord[] {
111
+ return Array.from(this.records.values()).filter(
112
+ (r) => r.status === 'completed' || r.status === 'failed'
113
+ );
114
+ }
115
+
116
+ /** List all handoffs regardless of status */
117
+ listAll(): HandoffRecord[] {
118
+ return Array.from(this.records.values());
119
+ }
120
+
121
+ /**
122
+ * Get a handoff record by either its unique spawnKey or by agentId.
123
+ * - Exact spawnKey match wins (O(1)).
124
+ * - Falls back to the most recently spawned record for that agentId — matching
125
+ * lookups by callers that only know the agent identity, not the spawn round.
126
+ */
127
+ get(idOrSpawnKey: string): HandoffRecord | undefined {
128
+ const direct = this.records.get(idOrSpawnKey);
129
+ if (direct) return direct;
130
+ let latest: HandoffRecord | undefined;
131
+ for (const record of this.records.values()) {
132
+ if (record.id !== idOrSpawnKey) continue;
133
+ if (!latest || record.spawnedAt >= latest.spawnedAt) latest = record;
134
+ }
135
+ return latest;
136
+ }
137
+
138
+ /** Check if any handoffs are still running */
139
+ hasPending(): boolean {
140
+ return this.listPending().length > 0;
141
+ }
142
+
143
+ /**
144
+ * Wait for ALL pending handoffs to complete and return all records.
145
+ * Records are NOT auto-cleared — caller removes collected records via remove().
146
+ */
147
+ async waitForAll(): Promise<HandoffRecord[]> {
148
+ const pending = this.listPending();
149
+ if (pending.length > 0) {
150
+ await Promise.allSettled(pending.map((r) => r.promise));
151
+ }
152
+ const results = this.listAll();
153
+ return results;
154
+ }
155
+
156
+ /**
157
+ * Wait for ANY pending handoff to complete.
158
+ * Returns the newly completed record(s).
159
+ */
160
+ async waitForAny(): Promise<HandoffRecord[]> {
161
+ const pending = this.listPending();
162
+ if (pending.length === 0) {
163
+ return this.listCompleted();
164
+ }
165
+
166
+ // Race all pending promises — at least one will resolve
167
+ await Promise.race(
168
+ pending.map((r) => r.promise.then(() => r).catch(() => r))
169
+ );
170
+
171
+ // Small yield to let promise handlers update records
172
+ await new Promise((resolve) => setTimeout(resolve, 0));
173
+
174
+ return this.listCompleted();
175
+ }
176
+
177
+ /**
178
+ * Remove record(s) by spawnKey or agentId.
179
+ * - Exact spawnKey match removes only that record.
180
+ * - agentId match removes ALL records for that agent (covers callers that
181
+ * want to forget everything tied to a given agent).
182
+ */
183
+ remove(idOrSpawnKey: string): void {
184
+ if (this.records.delete(idOrSpawnKey)) return;
185
+ for (const [key, record] of this.records) {
186
+ if (record.id === idOrSpawnKey) this.records.delete(key);
187
+ }
188
+ }
189
+
190
+ /** Clear all records (for cleanup between graph invocations) */
191
+ clear(): void {
192
+ this.records.clear();
193
+ }
194
+
195
+ /** Number of total tracked handoffs */
196
+ get size(): number {
197
+ return this.records.size;
198
+ }
199
+ }