@illuma-ai/agents 1.1.28 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  2. package/dist/cjs/common/spawnPath.cjs +104 -0
  3. package/dist/cjs/common/spawnPath.cjs.map +1 -0
  4. package/dist/cjs/graphs/Graph.cjs +84 -33
  5. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  6. package/dist/cjs/graphs/HandoffRegistry.cjs +47 -8
  7. package/dist/cjs/graphs/HandoffRegistry.cjs.map +1 -1
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs +493 -267
  9. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  10. package/dist/cjs/graphs/phases/flushLoop.cjs +214 -0
  11. package/dist/cjs/graphs/phases/flushLoop.cjs.map +1 -0
  12. package/dist/cjs/graphs/phases/memoryFlushPhase.cjs +102 -0
  13. package/dist/cjs/graphs/phases/memoryFlushPhase.cjs.map +1 -0
  14. package/dist/cjs/llm/bedrock/index.cjs +4 -3
  15. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  16. package/dist/cjs/main.cjs +113 -0
  17. package/dist/cjs/main.cjs.map +1 -1
  18. package/dist/cjs/memory/citations.cjs +69 -0
  19. package/dist/cjs/memory/citations.cjs.map +1 -0
  20. package/dist/cjs/memory/compositeBackend.cjs +60 -0
  21. package/dist/cjs/memory/compositeBackend.cjs.map +1 -0
  22. package/dist/cjs/memory/constants.cjs +232 -0
  23. package/dist/cjs/memory/constants.cjs.map +1 -0
  24. package/dist/cjs/memory/embeddings.cjs +151 -0
  25. package/dist/cjs/memory/embeddings.cjs.map +1 -0
  26. package/dist/cjs/memory/factory.cjs +95 -0
  27. package/dist/cjs/memory/factory.cjs.map +1 -0
  28. package/dist/cjs/memory/migrate.cjs +81 -0
  29. package/dist/cjs/memory/migrate.cjs.map +1 -0
  30. package/dist/cjs/memory/mmr.cjs +138 -0
  31. package/dist/cjs/memory/mmr.cjs.map +1 -0
  32. package/dist/cjs/memory/paths.cjs +217 -0
  33. package/dist/cjs/memory/paths.cjs.map +1 -0
  34. package/dist/cjs/memory/pgvectorStore.cjs +225 -0
  35. package/dist/cjs/memory/pgvectorStore.cjs.map +1 -0
  36. package/dist/cjs/memory/recallTracking.cjs +98 -0
  37. package/dist/cjs/memory/recallTracking.cjs.map +1 -0
  38. package/dist/cjs/memory/schema.sql +51 -0
  39. package/dist/cjs/memory/temporalDecay.cjs +118 -0
  40. package/dist/cjs/memory/temporalDecay.cjs.map +1 -0
  41. package/dist/cjs/nodes/ApprovalGateNode.cjs +1 -1
  42. package/dist/cjs/nodes/ApprovalGateNode.cjs.map +1 -1
  43. package/dist/cjs/prompts/memoryFlushPrompt.cjs +49 -0
  44. package/dist/cjs/prompts/memoryFlushPrompt.cjs.map +1 -0
  45. package/dist/cjs/run.cjs +16 -3
  46. package/dist/cjs/run.cjs.map +1 -1
  47. package/dist/cjs/tools/AskUser.cjs +6 -1
  48. package/dist/cjs/tools/AskUser.cjs.map +1 -1
  49. package/dist/cjs/tools/BrowserTools.cjs +1 -1
  50. package/dist/cjs/tools/BrowserTools.cjs.map +1 -1
  51. package/dist/cjs/tools/ToolNode.cjs +127 -10
  52. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  53. package/dist/cjs/tools/approval/constants.cjs +2 -2
  54. package/dist/cjs/tools/approval/constants.cjs.map +1 -1
  55. package/dist/cjs/tools/memory/index.cjs +58 -0
  56. package/dist/cjs/tools/memory/index.cjs.map +1 -0
  57. package/dist/cjs/tools/memory/memoryAppendTool.cjs +69 -0
  58. package/dist/cjs/tools/memory/memoryAppendTool.cjs.map +1 -0
  59. package/dist/cjs/tools/memory/memoryGetTool.cjs +49 -0
  60. package/dist/cjs/tools/memory/memoryGetTool.cjs.map +1 -0
  61. package/dist/cjs/tools/memory/memorySearchTool.cjs +65 -0
  62. package/dist/cjs/tools/memory/memorySearchTool.cjs.map +1 -0
  63. package/dist/cjs/tools/memory/shared.cjs +106 -0
  64. package/dist/cjs/tools/memory/shared.cjs.map +1 -0
  65. package/dist/cjs/types/graph.cjs.map +1 -1
  66. package/dist/cjs/utils/childAgentContext.cjs +242 -0
  67. package/dist/cjs/utils/childAgentContext.cjs.map +1 -0
  68. package/dist/cjs/utils/events.cjs +36 -7
  69. package/dist/cjs/utils/events.cjs.map +1 -1
  70. package/dist/cjs/utils/finishReasons.cjs +44 -0
  71. package/dist/cjs/utils/finishReasons.cjs.map +1 -0
  72. package/dist/cjs/utils/llm.cjs.map +1 -1
  73. package/dist/cjs/utils/logging.cjs +34 -0
  74. package/dist/cjs/utils/logging.cjs.map +1 -0
  75. package/dist/cjs/utils/toolCallNormalization.cjs +250 -0
  76. package/dist/cjs/utils/toolCallNormalization.cjs.map +1 -0
  77. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  78. package/dist/esm/common/spawnPath.mjs +95 -0
  79. package/dist/esm/common/spawnPath.mjs.map +1 -0
  80. package/dist/esm/graphs/Graph.mjs +84 -33
  81. package/dist/esm/graphs/Graph.mjs.map +1 -1
  82. package/dist/esm/graphs/HandoffRegistry.mjs +47 -8
  83. package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -1
  84. package/dist/esm/graphs/MultiAgentGraph.mjs +493 -267
  85. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  86. package/dist/esm/graphs/phases/flushLoop.mjs +209 -0
  87. package/dist/esm/graphs/phases/flushLoop.mjs.map +1 -0
  88. package/dist/esm/graphs/phases/memoryFlushPhase.mjs +99 -0
  89. package/dist/esm/graphs/phases/memoryFlushPhase.mjs.map +1 -0
  90. package/dist/esm/llm/bedrock/index.mjs +4 -3
  91. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  92. package/dist/esm/main.mjs +20 -0
  93. package/dist/esm/main.mjs.map +1 -1
  94. package/dist/esm/memory/citations.mjs +64 -0
  95. package/dist/esm/memory/citations.mjs.map +1 -0
  96. package/dist/esm/memory/compositeBackend.mjs +58 -0
  97. package/dist/esm/memory/compositeBackend.mjs.map +1 -0
  98. package/dist/esm/memory/constants.mjs +198 -0
  99. package/dist/esm/memory/constants.mjs.map +1 -0
  100. package/dist/esm/memory/embeddings.mjs +148 -0
  101. package/dist/esm/memory/embeddings.mjs.map +1 -0
  102. package/dist/esm/memory/factory.mjs +93 -0
  103. package/dist/esm/memory/factory.mjs.map +1 -0
  104. package/dist/esm/memory/migrate.mjs +78 -0
  105. package/dist/esm/memory/migrate.mjs.map +1 -0
  106. package/dist/esm/memory/mmr.mjs +130 -0
  107. package/dist/esm/memory/mmr.mjs.map +1 -0
  108. package/dist/esm/memory/paths.mjs +207 -0
  109. package/dist/esm/memory/paths.mjs.map +1 -0
  110. package/dist/esm/memory/pgvectorStore.mjs +223 -0
  111. package/dist/esm/memory/pgvectorStore.mjs.map +1 -0
  112. package/dist/esm/memory/recallTracking.mjs +94 -0
  113. package/dist/esm/memory/recallTracking.mjs.map +1 -0
  114. package/dist/esm/memory/schema.sql +51 -0
  115. package/dist/esm/memory/temporalDecay.mjs +110 -0
  116. package/dist/esm/memory/temporalDecay.mjs.map +1 -0
  117. package/dist/esm/nodes/ApprovalGateNode.mjs +1 -1
  118. package/dist/esm/nodes/ApprovalGateNode.mjs.map +1 -1
  119. package/dist/esm/prompts/memoryFlushPrompt.mjs +44 -0
  120. package/dist/esm/prompts/memoryFlushPrompt.mjs.map +1 -0
  121. package/dist/esm/run.mjs +16 -3
  122. package/dist/esm/run.mjs.map +1 -1
  123. package/dist/esm/tools/AskUser.mjs +6 -1
  124. package/dist/esm/tools/AskUser.mjs.map +1 -1
  125. package/dist/esm/tools/BrowserTools.mjs +1 -1
  126. package/dist/esm/tools/BrowserTools.mjs.map +1 -1
  127. package/dist/esm/tools/ToolNode.mjs +128 -11
  128. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  129. package/dist/esm/tools/approval/constants.mjs +2 -2
  130. package/dist/esm/tools/approval/constants.mjs.map +1 -1
  131. package/dist/esm/tools/memory/index.mjs +46 -0
  132. package/dist/esm/tools/memory/index.mjs.map +1 -0
  133. package/dist/esm/tools/memory/memoryAppendTool.mjs +67 -0
  134. package/dist/esm/tools/memory/memoryAppendTool.mjs.map +1 -0
  135. package/dist/esm/tools/memory/memoryGetTool.mjs +47 -0
  136. package/dist/esm/tools/memory/memoryGetTool.mjs.map +1 -0
  137. package/dist/esm/tools/memory/memorySearchTool.mjs +63 -0
  138. package/dist/esm/tools/memory/memorySearchTool.mjs.map +1 -0
  139. package/dist/esm/tools/memory/shared.mjs +98 -0
  140. package/dist/esm/tools/memory/shared.mjs.map +1 -0
  141. package/dist/esm/types/graph.mjs.map +1 -1
  142. package/dist/esm/utils/childAgentContext.mjs +237 -0
  143. package/dist/esm/utils/childAgentContext.mjs.map +1 -0
  144. package/dist/esm/utils/events.mjs +36 -8
  145. package/dist/esm/utils/events.mjs.map +1 -1
  146. package/dist/esm/utils/finishReasons.mjs +41 -0
  147. package/dist/esm/utils/finishReasons.mjs.map +1 -0
  148. package/dist/esm/utils/llm.mjs.map +1 -1
  149. package/dist/esm/utils/logging.mjs +31 -0
  150. package/dist/esm/utils/logging.mjs.map +1 -0
  151. package/dist/esm/utils/toolCallNormalization.mjs +247 -0
  152. package/dist/esm/utils/toolCallNormalization.mjs.map +1 -0
  153. package/dist/types/common/index.d.ts +1 -0
  154. package/dist/types/common/spawnPath.d.ts +59 -0
  155. package/dist/types/graphs/HandoffRegistry.d.ts +24 -7
  156. package/dist/types/graphs/MultiAgentGraph.d.ts +43 -23
  157. package/dist/types/graphs/phases/flushLoop.d.ts +106 -0
  158. package/dist/types/graphs/phases/memoryFlushPhase.d.ts +100 -0
  159. package/dist/types/index.d.ts +7 -0
  160. package/dist/types/memory/__tests__/mockBackend.d.ts +40 -0
  161. package/dist/types/memory/citations.d.ts +39 -0
  162. package/dist/types/memory/compositeBackend.d.ts +30 -0
  163. package/dist/types/memory/constants.d.ts +121 -0
  164. package/dist/types/memory/embeddings.d.ts +15 -0
  165. package/dist/types/memory/factory.d.ts +23 -0
  166. package/dist/types/memory/index.d.ts +21 -0
  167. package/dist/types/memory/migrate.d.ts +14 -0
  168. package/dist/types/memory/mmr.d.ts +50 -0
  169. package/dist/types/memory/paths.d.ts +107 -0
  170. package/dist/types/memory/pgvectorStore.d.ts +56 -0
  171. package/dist/types/memory/recallTracking.d.ts +30 -0
  172. package/dist/types/memory/temporalDecay.d.ts +53 -0
  173. package/dist/types/memory/types.d.ts +182 -0
  174. package/dist/types/prompts/memoryFlushPrompt.d.ts +54 -0
  175. package/dist/types/run.d.ts +1 -0
  176. package/dist/types/tools/AskUser.d.ts +1 -1
  177. package/dist/types/tools/BrowserTools.d.ts +2 -2
  178. package/dist/types/tools/approval/constants.d.ts +2 -2
  179. package/dist/types/tools/memory/index.d.ts +39 -0
  180. package/dist/types/tools/memory/memoryAppendTool.d.ts +27 -0
  181. package/dist/types/tools/memory/memoryGetTool.d.ts +22 -0
  182. package/dist/types/tools/memory/memorySearchTool.d.ts +22 -0
  183. package/dist/types/tools/memory/shared.d.ts +106 -0
  184. package/dist/types/types/graph.d.ts +10 -3
  185. package/dist/types/utils/childAgentContext.d.ts +99 -0
  186. package/dist/types/utils/events.d.ts +21 -0
  187. package/dist/types/utils/finishReasons.d.ts +32 -0
  188. package/dist/types/utils/logging.d.ts +2 -0
  189. package/dist/types/utils/toolCallNormalization.d.ts +44 -0
  190. package/package.json +6 -4
  191. package/src/agents/AgentContext.ts +12 -4
  192. package/src/common/__tests__/enum.test.ts +4 -2
  193. package/src/common/__tests__/spawnPath.test.ts +110 -0
  194. package/src/common/index.ts +1 -0
  195. package/src/common/spawnPath.ts +101 -0
  196. package/src/graphs/Graph.ts +90 -47
  197. package/src/graphs/HandoffRegistry.ts +48 -17
  198. package/src/graphs/MultiAgentGraph.ts +588 -327
  199. package/src/graphs/__tests__/HandoffRegistry.test.ts +4 -1
  200. package/src/graphs/__tests__/multi-agent-delegate.test.ts +61 -16
  201. package/src/graphs/__tests__/multi-agent-edges.test.ts +4 -2
  202. package/src/graphs/__tests__/multi-agent-nested-subgraph.test.ts +221 -0
  203. package/src/graphs/__tests__/structured-output.integration.test.ts +212 -118
  204. package/src/graphs/contextManagement.e2e.test.ts +1 -1
  205. package/src/graphs/phases/__tests__/flushLoop.test.ts +264 -0
  206. package/src/graphs/phases/__tests__/memoryFlushPhase.test.ts +37 -0
  207. package/src/graphs/phases/__tests__/runMemoryFlush.test.ts +150 -0
  208. package/src/graphs/phases/flushLoop.ts +303 -0
  209. package/src/graphs/phases/memoryFlushPhase.ts +209 -0
  210. package/src/index.ts +30 -1
  211. package/src/llm/bedrock/index.ts +4 -5
  212. package/src/memory/__tests__/citations.test.ts +61 -0
  213. package/src/memory/__tests__/compositeBackend.test.ts +79 -0
  214. package/src/memory/__tests__/isolation.test.ts +206 -0
  215. package/src/memory/__tests__/mmr.test.ts +148 -0
  216. package/src/memory/__tests__/mockBackend.ts +161 -0
  217. package/src/memory/__tests__/paths.test.ts +168 -0
  218. package/src/memory/__tests__/recallTracking.test.ts +96 -0
  219. package/src/memory/__tests__/temporalDecay.test.ts +151 -0
  220. package/src/memory/citations.ts +80 -0
  221. package/src/memory/compositeBackend.ts +99 -0
  222. package/src/memory/constants.ts +229 -0
  223. package/src/memory/embeddings.ts +188 -0
  224. package/src/memory/factory.ts +111 -0
  225. package/src/memory/index.ts +46 -0
  226. package/src/memory/migrate.ts +116 -0
  227. package/src/memory/mmr.ts +161 -0
  228. package/src/memory/paths.ts +258 -0
  229. package/src/memory/pgvectorStore.ts +324 -0
  230. package/src/memory/recallTracking.ts +127 -0
  231. package/src/memory/schema.sql +51 -0
  232. package/src/memory/temporalDecay.ts +134 -0
  233. package/src/memory/types.ts +185 -0
  234. package/src/nodes/ApprovalGateNode.ts +4 -10
  235. package/src/nodes/__tests__/ApprovalGateNode.test.ts +11 -20
  236. package/src/prompts/memoryFlushPrompt.ts +78 -0
  237. package/src/run.ts +17 -6
  238. package/src/scripts/test-bedrock-handoff-autonomous.ts +56 -20
  239. package/src/specs/agent-handoffs-bedrock.integration.test.ts +8 -5
  240. package/src/specs/agent-handoffs.test.ts +8 -2
  241. package/src/tools/AskUser.ts +7 -2
  242. package/src/tools/BrowserTools.ts +3 -5
  243. package/src/tools/ToolNode.ts +150 -13
  244. package/src/tools/__tests__/ToolApproval.test.ts +22 -9
  245. package/src/tools/approval/__tests__/constants.test.ts +1 -1
  246. package/src/tools/approval/constants.ts +2 -2
  247. package/src/tools/memory/__tests__/memoryTools.test.ts +205 -0
  248. package/src/tools/memory/index.ts +96 -0
  249. package/src/tools/memory/memoryAppendTool.ts +101 -0
  250. package/src/tools/memory/memoryGetTool.ts +53 -0
  251. package/src/tools/memory/memorySearchTool.ts +80 -0
  252. package/src/tools/memory/shared.ts +169 -0
  253. package/src/tools/search/search.test.ts +6 -1
  254. package/src/types/graph.ts +10 -3
  255. package/src/utils/__tests__/childAgentContext.test.ts +217 -0
  256. package/src/utils/__tests__/finishReasons.test.ts +55 -0
  257. package/src/utils/__tests__/toolCallNormalization.test.ts +181 -0
  258. package/src/utils/childAgentContext.ts +259 -0
  259. package/src/utils/events.ts +37 -7
  260. package/src/utils/finishReasons.ts +40 -0
  261. package/src/utils/llm.ts +0 -1
  262. package/src/utils/logging.ts +45 -8
  263. package/src/utils/toolCallNormalization.ts +271 -0
@@ -0,0 +1,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
  }
@@ -1498,11 +1498,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1498
1498
  });
1499
1499
 
1500
1500
  // DEBUG: Log which model and tools each agent uses during handoff
1501
- console.debug(
1501
+ mlog(
1502
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(', ')}]`,
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
1506
  );
1507
1507
 
1508
1508
  if (agentContext.systemRunnable) {
@@ -1515,7 +1515,15 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1515
1515
  if (!config.signal) {
1516
1516
  config.signal = this.signal;
1517
1517
  }
1518
- 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;
1519
1527
 
1520
1528
  let messagesToUse = messages;
1521
1529
 
@@ -1619,7 +1627,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1619
1627
 
1620
1628
  if (oldMessages.length > 0) {
1621
1629
  this._summaryInFlight = true;
1622
- console.debug(
1630
+ mlog(
1623
1631
  `[Graph:ProactiveSummary] Context at ${utilization.toFixed(1)}% (threshold ${threshold}%) — summarizing ${oldMessages.length} older msgs in background`
1624
1632
  );
1625
1633
 
@@ -1628,7 +1636,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1628
1636
  .then((updated) => {
1629
1637
  if (updated != null && updated !== '') {
1630
1638
  this._cachedRunSummary = updated;
1631
- console.debug(
1639
+ mlog(
1632
1640
  `[Graph:ProactiveSummary] Background summary ready (len=${updated.length})`
1633
1641
  );
1634
1642
  }
@@ -1838,7 +1846,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1838
1846
  }
1839
1847
  agentContext.indexTokenCountMap = viewTokenMap;
1840
1848
 
1841
- console.debug(
1849
+ mlog(
1842
1850
  `[Graph:Compaction] ${messages.length}→${viewParts.length} msgs | ` +
1843
1851
  `compacted=${compactedMessages.length} window=${recentMessages.length} | ` +
1844
1852
  `summary=${summarySource} | budget=${usedTokens}/${recentBudget}` +
@@ -1862,7 +1870,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1862
1870
  if (shouldSummarize) {
1863
1871
  if (this._summaryInFlight) {
1864
1872
  this._pendingMessagesToRefine.push(...compactedMessages);
1865
- console.debug(
1873
+ mlog(
1866
1874
  `[Graph:Compaction] Summary in-flight, queued ${compactedMessages.length} msgs (pending=${this._pendingMessagesToRefine.length})`
1867
1875
  );
1868
1876
  } else {
@@ -1915,7 +1923,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1915
1923
  deduplicateSystemMessages(messagesToUse);
1916
1924
  if (removedCount > 0) {
1917
1925
  messagesToUse = dedupedMessages;
1918
- console.debug(
1926
+ mlog(
1919
1927
  `[Graph:Dedup] Removed ${removedCount} duplicate system message(s)`
1920
1928
  );
1921
1929
  }
@@ -2039,7 +2047,6 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2039
2047
  );
2040
2048
  }
2041
2049
 
2042
-
2043
2050
  // Get model info for analytics
2044
2051
  const bedrockOpts = agentContext.clientOptions as
2045
2052
  | t.BedrockAnthropicClientOptions
@@ -2153,11 +2160,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2153
2160
 
2154
2161
  // Log when we detect the error
2155
2162
  if (isInputTooLongError) {
2156
- console.warn(
2163
+ mwarn(
2157
2164
  '[Graph] Detected input too long error:',
2158
2165
  errorMessage.substring(0, 200)
2159
2166
  );
2160
- console.warn('[Graph] Checking emergency pruning conditions:', {
2167
+ mwarn('[Graph] Checking emergency pruning conditions:', {
2161
2168
  hasPruneMessages: !!agentContext.pruneMessages,
2162
2169
  hasTokenCounter: !!agentContext.tokenCounter,
2163
2170
  maxContextTokens: agentContext.maxContextTokens,
@@ -2182,7 +2189,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2182
2189
  const reducedMaxTokens = Math.floor(
2183
2190
  agentContext.maxContextTokens! * reductionFactor
2184
2191
  );
2185
- console.warn(
2192
+ mwarn(
2186
2193
  `[Graph] Input too long. Retrying with ${reductionFactor * 100}% context (${reducedMaxTokens} tokens)...`
2187
2194
  );
2188
2195
 
@@ -2190,7 +2197,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2190
2197
  // This is needed when messages were dynamically added without updating the token map
2191
2198
  let tokenMapForPruning = agentContext.indexTokenCountMap;
2192
2199
  if (Object.keys(tokenMapForPruning).length < messages.length) {
2193
- console.warn(
2200
+ mwarn(
2194
2201
  '[Graph] Building fresh token count map for emergency pruning...'
2195
2202
  );
2196
2203
  tokenMapForPruning = {};
@@ -2215,7 +2222,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2215
2222
 
2216
2223
  // Skip if we can't fit any messages
2217
2224
  if (reducedMessages.length === 0) {
2218
- console.warn(
2225
+ mwarn(
2219
2226
  `[Graph] Cannot fit any messages at ${reductionFactor * 100}% reduction, trying next level...`
2220
2227
  );
2221
2228
  continue;
@@ -2298,7 +2305,7 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2298
2305
  retryErrorMsg.includes('validationexception');
2299
2306
 
2300
2307
  if (stillTooLong && reductionFactor > 0.1) {
2301
- console.warn(
2308
+ mwarn(
2302
2309
  `[Graph] Still too long at ${reductionFactor * 100}%, trying more aggressive pruning...`
2303
2310
  );
2304
2311
  } else {
@@ -2370,6 +2377,27 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2370
2377
  * handled everything — both paths become no-ops.
2371
2378
  */
2372
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
+
2373
2401
  const toolCalls = (responseMessage as AIMessageChunk | undefined)
2374
2402
  ?.tool_calls;
2375
2403
  const hasToolCalls = Array.isArray(toolCalls) && toolCalls.length > 0;
@@ -2485,12 +2513,22 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2485
2513
  const messageStop = meta.messageStop as
2486
2514
  | Record<string, unknown>
2487
2515
  | undefined;
2488
- this.lastFinishReason =
2516
+ const nextReason =
2489
2517
  (meta.finish_reason as string | undefined) ?? // OpenAI/Azure
2490
2518
  (meta.stop_reason as string | undefined) ?? // Anthropic direct API
2491
2519
  (meta.stopReason as string | undefined) ?? // Bedrock invoke (non-streaming)
2492
2520
  (messageStop?.stopReason as string | undefined) ?? // Bedrock streaming
2493
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
+ }
2494
2532
  }
2495
2533
 
2496
2534
  this.cleanupSignalListener();
@@ -2553,7 +2591,7 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2553
2591
  '[Graph] Deferred structured output failed after successful tool use:',
2554
2592
  structuredError
2555
2593
  );
2556
- console.warn(
2594
+ mwarn(
2557
2595
  '[Graph] Falling back to unstructured response from tool-use phase'
2558
2596
  );
2559
2597
  return result;
@@ -2578,7 +2616,10 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2578
2616
  state: t.BaseGraphState,
2579
2617
  config?: RunnableConfig
2580
2618
  ): string => {
2581
- 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;
2582
2623
  return toolsCondition(state, toolNode, this.invokedToolIds);
2583
2624
  };
2584
2625
 
@@ -2623,10 +2664,16 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2623
2664
  default: () => [],
2624
2665
  }),
2625
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.
2626
2673
  const workflow = new StateGraph(StateAnnotation)
2627
2674
  .addNode(this.defaultAgentId, agentNode, { ends: [END] })
2628
2675
  .addEdge(START, this.defaultAgentId)
2629
- .compile();
2676
+ .compile(this.compileOptions as unknown as never);
2630
2677
 
2631
2678
  return workflow;
2632
2679
  }
@@ -2709,7 +2756,7 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2709
2756
  }
2710
2757
  } catch (_e) {
2711
2758
  /** If we can't get agent context, that's okay - agentId remains undefined */
2712
- console.debug(
2759
+ mlog(
2713
2760
  `[dispatchRunStep] Could not resolve agentId from metadata.langgraph_node="${(metadata as Record<string, unknown>).langgraph_node}": ${(_e as Error).message}`
2714
2761
  );
2715
2762
  }
@@ -2717,11 +2764,11 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2717
2764
 
2718
2765
  this.contentData.push(runStep);
2719
2766
  this.contentIndexMap.set(stepId, runStep.index);
2720
- await safeDispatchCustomEvent(
2721
- GraphEvents.ON_RUN_STEP,
2722
- runStep,
2723
- this.config
2724
- );
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);
2725
2772
  return stepId;
2726
2773
  }
2727
2774
 
@@ -2862,7 +2909,7 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2862
2909
  }
2863
2910
 
2864
2911
  if (!data.id) {
2865
- console.warn('No Tool ID provided for Tool Error');
2912
+ mwarn('No Tool ID provided for Tool Error');
2866
2913
  return;
2867
2914
  }
2868
2915
 
@@ -2927,11 +2974,10 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2927
2974
  id,
2928
2975
  delta,
2929
2976
  };
2930
- await safeDispatchCustomEvent(
2931
- GraphEvents.ON_RUN_STEP_DELTA,
2932
- runStepDelta,
2933
- this.config
2934
- );
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);
2935
2981
  }
2936
2982
 
2937
2983
  async dispatchMessageDelta(id: string, delta: t.MessageDelta): Promise<void> {
@@ -2942,11 +2988,8 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2942
2988
  id,
2943
2989
  delta,
2944
2990
  };
2945
- await safeDispatchCustomEvent(
2946
- GraphEvents.ON_MESSAGE_DELTA,
2947
- messageDelta,
2948
- this.config
2949
- );
2991
+ // See dispatchRunStep note.
2992
+ await safeDispatchCustomEvent(GraphEvents.ON_MESSAGE_DELTA, messageDelta);
2950
2993
  }
2951
2994
 
2952
2995
  dispatchReasoningDelta = async (
@@ -2960,10 +3003,10 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
2960
3003
  id: stepId,
2961
3004
  delta,
2962
3005
  };
3006
+ // See dispatchRunStep note.
2963
3007
  await safeDispatchCustomEvent(
2964
3008
  GraphEvents.ON_REASONING_DELTA,
2965
- reasoningDelta,
2966
- this.config
3009
+ reasoningDelta
2967
3010
  );
2968
3011
  };
2969
3012
  }
@@ -3,11 +3,12 @@ import type * as t from '@/types';
3
3
 
4
4
  /**
5
5
  * Tracks the lifecycle of a spawned handoff child agent.
6
- * Mirrors OpenClaw's SubagentRunRecord pattern.
7
6
  */
8
7
  export type HandoffRecord = {
9
- /** Unique handoff ID (destination agentId) */
8
+ /** Agent identity (destination agentId) — stable across multiple spawns of the same agent */
10
9
  id: string;
10
+ /** Unique internal key for this specific spawn (agentId + monotonic counter) */
11
+ spawnKey: string;
11
12
  /** Display name of the child agent */
12
13
  name: string;
13
14
  /** Task description / instructions passed to child */
@@ -31,7 +32,7 @@ export type HandoffRecord = {
31
32
  /**
32
33
  * Registry for async handoff execution.
33
34
  *
34
- * Enables the OpenClaw-style autonomous orchestration pattern:
35
+ * Enables the autonomous orchestration pattern:
35
36
  * 1. Orchestrator spawns children (non-blocking)
36
37
  * 2. Orchestrator stays alive to reason, spawn more, or check status
37
38
  * 3. Orchestrator collects results when ready
@@ -40,10 +41,14 @@ export type HandoffRecord = {
40
41
  */
41
42
  export class HandoffRegistry {
42
43
  private records: Map<string, HandoffRecord> = new Map();
44
+ /** Monotonically increasing counter for unique spawn IDs */
45
+ private spawnCounter = 0;
43
46
 
44
47
  /**
45
48
  * Register a spawned handoff child.
46
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.
47
52
  */
48
53
  spawn(params: {
49
54
  id: string;
@@ -56,8 +61,13 @@ export class HandoffRegistry {
56
61
  /** Callback when child completes (for SSE events) */
57
62
  onComplete?: (record: HandoffRecord) => void;
58
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++}`;
59
68
  const record: HandoffRecord = {
60
69
  id: params.id,
70
+ spawnKey,
61
71
  name: params.name,
62
72
  task: params.task,
63
73
  spawnedAt: Date.now(),
@@ -68,10 +78,7 @@ export class HandoffRegistry {
68
78
  // Wire up the promise to update the record on completion
69
79
  params.promise
70
80
  .then((result) => {
71
- const resultText = params.extractResult(
72
- result.messages,
73
- params.id
74
- );
81
+ const resultText = params.extractResult(result.messages, params.id);
75
82
  const truncated = params.truncateResult(
76
83
  resultText,
77
84
  params.maxResultChars
@@ -89,7 +96,7 @@ export class HandoffRegistry {
89
96
  params.onComplete?.(record);
90
97
  });
91
98
 
92
- this.records.set(params.id, record);
99
+ this.records.set(spawnKey, record);
93
100
  }
94
101
 
95
102
  /** List all pending (running) handoffs */
@@ -111,9 +118,21 @@ export class HandoffRegistry {
111
118
  return Array.from(this.records.values());
112
119
  }
113
120
 
114
- /** Get a specific handoff by ID */
115
- get(id: string): HandoffRecord | undefined {
116
- return this.records.get(id);
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;
117
136
  }
118
137
 
119
138
  /** Check if any handoffs are still running */
@@ -122,15 +141,16 @@ export class HandoffRegistry {
122
141
  }
123
142
 
124
143
  /**
125
- * Wait for ALL pending handoffs to complete.
126
- * Returns all completed records (including previously completed ones).
144
+ * Wait for ALL pending handoffs to complete and return all records.
145
+ * Records are NOT auto-cleared caller removes collected records via remove().
127
146
  */
128
147
  async waitForAll(): Promise<HandoffRecord[]> {
129
148
  const pending = this.listPending();
130
149
  if (pending.length > 0) {
131
150
  await Promise.allSettled(pending.map((r) => r.promise));
132
151
  }
133
- return this.listAll();
152
+ const results = this.listAll();
153
+ return results;
134
154
  }
135
155
 
136
156
  /**
@@ -145,9 +165,7 @@ export class HandoffRegistry {
145
165
 
146
166
  // Race all pending promises — at least one will resolve
147
167
  await Promise.race(
148
- pending.map((r) =>
149
- r.promise.then(() => r).catch(() => r)
150
- )
168
+ pending.map((r) => r.promise.then(() => r).catch(() => r))
151
169
  );
152
170
 
153
171
  // Small yield to let promise handlers update records
@@ -156,6 +174,19 @@ export class HandoffRegistry {
156
174
  return this.listCompleted();
157
175
  }
158
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
+
159
190
  /** Clear all records (for cleanup between graph invocations) */
160
191
  clear(): void {
161
192
  this.records.clear();