@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
@@ -7,14 +7,18 @@ var langgraph = require('@langchain/langgraph');
7
7
  require('../messages/core.cjs');
8
8
  require('nanoid');
9
9
  var _enum = require('../common/enum.cjs');
10
+ var spawnPath = require('../common/spawnPath.cjs');
10
11
  require('../tools/approval/constants.cjs');
11
12
  require('../utils/toonFormat.cjs');
12
13
  var summarize = require('../messages/summarize.cjs');
13
14
  var Graph = require('./Graph.cjs');
14
15
  var events = require('../utils/events.cjs');
16
+ var logging = require('../utils/logging.cjs');
17
+ var childAgentContext = require('../utils/childAgentContext.cjs');
15
18
  var ApprovalGateNode = require('../nodes/ApprovalGateNode.cjs');
16
- var HandoffRegistry = require('./HandoffRegistry.cjs');
17
19
 
20
+ // HandoffRegistry no longer needed — handoff tools use synchronous
21
+ // browser-tool callback pattern (spawn → wait → return result)
18
22
  /** Pattern to extract instructions from transfer ToolMessage content */
19
23
  const TRANSFER_INSTRUCTIONS_PATTERN = /(?:Instructions?|Context):\s*(.+)/is;
20
24
  /**
@@ -63,10 +67,10 @@ class MultiAgentGraph extends Graph.StandardGraph {
63
67
  lastActiveAgentId;
64
68
  /**
65
69
  * Registry for async handoff execution.
66
- * Enables OpenClaw-style autonomous orchestration: spawn children non-blocking,
70
+ * Enables autonomous orchestration: spawn children non-blocking,
67
71
  * orchestrator stays alive to reason and collect results when ready.
68
72
  */
69
- handoffRegistry = new HandoffRegistry.HandoffRegistry();
73
+ // HandoffRegistry removed — handoff tools are synchronous (callback pattern)
70
74
  /**
71
75
  * When set, the graph routes START to this agent instead of the default starting nodes.
72
76
  * Enables multi-turn resumption: follow-up messages go to the agent that last handled
@@ -81,7 +85,7 @@ class MultiAgentGraph extends Graph.StandardGraph {
81
85
  this.analyzeGraph();
82
86
  this.createTransferTools();
83
87
  this.createHandoffTools();
84
- console.debug(`[MultiAgentGraph] Constructor complete: ${this.agentContexts.size} agents, ${this.edges.length} edges`);
88
+ logging.mlog(`[MultiAgentGraph] Constructor complete: ${this.agentContexts.size} agents, ${this.edges.length} edges`);
85
89
  }
86
90
  /**
87
91
  * Categorize edges into handoff, transfer, and sequence types
@@ -94,7 +98,8 @@ class MultiAgentGraph extends Graph.StandardGraph {
94
98
  else if (edge.edgeType === _enum.EdgeType.SEQUENCE) {
95
99
  this.sequenceEdges.push(edge);
96
100
  }
97
- else if (edge.edgeType === _enum.EdgeType.TRANSFER || edge.condition != null) {
101
+ else if (edge.edgeType === _enum.EdgeType.TRANSFER ||
102
+ edge.condition != null) {
98
103
  this.transferEdges.push(edge);
99
104
  }
100
105
  else {
@@ -111,7 +116,7 @@ class MultiAgentGraph extends Graph.StandardGraph {
111
116
  }
112
117
  }
113
118
  }
114
- console.debug(`[MultiAgentGraph] Edge categorization: ${this.handoffEdges.length} handoff, ${this.transferEdges.length} transfer, ${this.sequenceEdges.length} sequence (of ${this.edges.length} total)`);
119
+ logging.mlog(`[MultiAgentGraph] Edge categorization: ${this.handoffEdges.length} handoff, ${this.transferEdges.length} transfer, ${this.sequenceEdges.length} sequence (of ${this.edges.length} total)`);
115
120
  }
116
121
  /**
117
122
  * Analyze graph structure to determine starting nodes and connections
@@ -133,7 +138,7 @@ class MultiAgentGraph extends Graph.StandardGraph {
133
138
  if (this.startingNodes.size === 0 && this.agentContexts.size > 0) {
134
139
  this.startingNodes.add(this.agentContexts.keys().next().value);
135
140
  }
136
- console.debug(`[MultiAgentGraph] Starting nodes identified: [${Array.from(this.startingNodes).join(', ')}]`);
141
+ logging.mlog(`[MultiAgentGraph] Starting nodes identified: [${Array.from(this.startingNodes).join(', ')}]`);
137
142
  // Determine if graph has parallel execution capability
138
143
  this.computeParallelCapability();
139
144
  }
@@ -268,7 +273,7 @@ class MultiAgentGraph extends Graph.StandardGraph {
268
273
  agentContext.graphTools = [];
269
274
  }
270
275
  agentContext.graphTools.push(...transferTools);
271
- console.debug(`[MultiAgentGraph] Transfer tools for "${agentId}": [${transferTools.map((t) => t.name).join(', ')}]`);
276
+ logging.mlog(`[MultiAgentGraph] Transfer tools for "${agentId}": [${transferTools.map((t) => t.name).join(', ')}]`);
272
277
  // Inject orchestration guidance for agents with transfer tools
273
278
  const childDescs = edges.flatMap((e) => {
274
279
  const dests = Array.isArray(e.to) ? e.to : [e.to];
@@ -302,7 +307,9 @@ class MultiAgentGraph extends Graph.StandardGraph {
302
307
  const toolDescription = edge.description ?? 'Conditionally transfer control based on state';
303
308
  /** Check if we have a prompt for handoff input */
304
309
  const hasTransferInput = edge.prompt != null && typeof edge.prompt === 'string';
305
- const transferInputDescription = hasTransferInput ? edge.prompt : undefined;
310
+ const transferInputDescription = hasTransferInput
311
+ ? edge.prompt
312
+ : undefined;
306
313
  const promptKey = edge.promptKey ?? 'instructions';
307
314
  tools$1.push(tools.tool(async (rawInput, config) => {
308
315
  const input = rawInput;
@@ -481,10 +488,14 @@ class MultiAgentGraph extends Graph.StandardGraph {
481
488
  * Builds orchestration guidance injected into the system message of agents
482
489
  * that have handoff or transfer tools (i.e., orchestrator agents).
483
490
  *
484
- * Modeled after OpenClaw's battle-tested subagent orchestration patterns:
485
- * - Push-based completion (results auto-return from child agents)
486
- * - Multi-round execution for dependent tasks
487
- * - Explicit rules against hallucinating data or acting on unavailable context
491
+ * Implements two orchestration primitives:
492
+ * - Execution bias guidance injected into the system prompt
493
+ * - Multi-round autonomous execution for dependent tasks
494
+ *
495
+ * Handoff tools are synchronous (browser-tool callback pattern): spawn the
496
+ * child, await completion, return the real text as the tool output. Parallel
497
+ * handoff tool calls in one turn run concurrently via LangGraph's ToolNode,
498
+ * so independent children run in parallel without explicit orchestration.
488
499
  *
489
500
  * @param childDescs - Display names (with optional descriptions) of child agents
490
501
  * @param toolCount - Number of handoff/transfer tools available
@@ -496,20 +507,58 @@ class MultiAgentGraph extends Graph.StandardGraph {
496
507
  `You have ${toolCount} specialist agent(s) available for delegation:`,
497
508
  ...childDescs.map((d) => `- ${d}`),
498
509
  '',
499
- 'If a task is more complex or takes longer, delegate it to a specialist agent. Completion is push-based: it will auto-return its result when done.',
500
- 'Use `check_agents` to check status without waiting. Use `collect_results` to wait for and retrieve agent outputs.',
501
- 'Default workflow: spawn work, continue reasoning, and call `collect_results` when ready.',
502
- 'Coordinate agent work and synthesize results before responding to the user.',
503
- 'For non-trivial multi-step work, keep a short plan updated for the user.',
504
- 'Do not poll `check_agents` in a loop; only check status on-demand (for debugging or when explicitly asked).',
510
+ '## Execution Bias',
511
+ 'If the user asks you to do the work, start doing it in the same turn.',
512
+ 'Use a real tool call or concrete action first when the task is actionable; do not stop at a plan or promise-to-act reply.',
513
+ 'Commentary-only turns are incomplete when tools are available and the next action is clear.',
514
+ 'If the work will take multiple steps or a while to finish, send one short progress update before or while acting.',
515
+ '',
516
+ '## How Delegation Works',
517
+ 'Each handoff tool call spawns a sub-agent, waits for it to complete, and returns the real result directly — like a function call.',
518
+ 'Independent tasks MAY be called in parallel (multiple handoff tool calls in one turn). They run concurrently and all results return together.',
519
+ 'Dependent tasks MUST be sequential: call one agent, get the result, then call the next agent using that real data.',
520
+ '',
521
+ '## Agent Isolation',
522
+ "Sub-agents CANNOT see your conversation or the user's original message. They ONLY see what you write in the `instructions` parameter.",
523
+ "When writing instructions, include ALL data the agent needs. Copy exact values from the user's message — email addresses, names, URLs, dates, numbers.",
524
+ 'When delegating follow-up work, include the real data from prior agent results directly in the instructions text.',
525
+ 'Do NOT re-delegate a task that was already completed. If you have the data, pass it directly.',
526
+ ].join('\n');
527
+ }
528
+ /**
529
+ * Builds subagent context instructions injected into child agents that are
530
+ * handoff destinations. This tells the child agent it is a subagent with
531
+ * a focused task.
532
+ *
533
+ * @param orchestratorName - Display name of the parent orchestrator agent
534
+ */
535
+ buildChildAgentContext(orchestratorName) {
536
+ return [
537
+ '# Subagent Context',
538
+ '',
539
+ `You are a **subagent** spawned by the ${orchestratorName} for a specific task.`,
540
+ '',
541
+ '## Your Role',
542
+ "- Complete this task. That's your entire purpose.",
543
+ `- You are NOT the ${orchestratorName}. Don't try to be.`,
505
544
  '',
506
- '### Delegation Rules',
507
- '- Delegate one clear, specific task per agent call.',
508
- '- Independent tasks MAY be spawned in parallel (multiple calls in one turn, then one `collect_results`).',
509
- '- Dependent tasks MUST be spawned in separate rounds — spawn, collect, analyze, then spawn the next with REAL data from prior results.',
510
- '- NEVER fabricate, guess, or use placeholder data. Only pass real data from collected agent results.',
511
- '- After collecting results, analyze them before proceeding. Explain what the agent found and what you will do next.',
512
- '- If an agent fails, analyze the error and retry with clearer instructions before reporting failure.',
545
+ '## Rules',
546
+ '1. **Stay focused** - Do your assigned task, nothing else',
547
+ `2. **Complete the task** - Your final message will be automatically reported to the ${orchestratorName}`,
548
+ "3. **Don't initiate** - No heartbeats, no proactive actions, no side quests",
549
+ "4. **Be ephemeral** - You may be terminated after task completion. That's fine.",
550
+ '',
551
+ '## Output Format',
552
+ 'When complete, your final response should include:',
553
+ '- What you accomplished or found',
554
+ `- Any relevant details the ${orchestratorName} should know`,
555
+ '- Keep it concise but informative',
556
+ '',
557
+ "## What You DON'T Do",
558
+ `- NO user conversations (that's ${orchestratorName}'s job)`,
559
+ '- NO external messages (email, tweets, etc.) unless explicitly tasked with a specific recipient/channel',
560
+ '- NO cron jobs or persistent state',
561
+ `- NO pretending to be the ${orchestratorName}`,
513
562
  ].join('\n');
514
563
  }
515
564
  /**
@@ -559,70 +608,11 @@ class MultiAgentGraph extends Graph.StandardGraph {
559
608
  agentContext.graphTools = [];
560
609
  }
561
610
  agentContext.graphTools.push(...handoffTools);
562
- /**
563
- * Add orchestrator coordination tools: collect_results and check_agents.
564
- * These enable the OpenClaw-style autonomous loop:
565
- * spawn reason check/collect reason → spawn more → synthesize
566
- */
567
- const handoffReg = this.handoffRegistry;
568
- agentContext.graphTools.push(tools.tool(async () => {
569
- if (!handoffReg.hasPending() && handoffReg.size === 0) {
570
- return 'No agents have been spawned yet.';
571
- }
572
- /** Wait for all pending handoffs to complete */
573
- const records = await handoffReg.waitForAll();
574
- const parts = [];
575
- for (const record of records) {
576
- if (record.status === 'completed') {
577
- parts.push(`## ${record.name} (completed in ${record.durationMs}ms)\n${record.resultText}`);
578
- }
579
- else if (record.status === 'failed') {
580
- parts.push(`## ${record.name} (FAILED after ${record.durationMs}ms)\nError: ${record.error}`);
581
- }
582
- else {
583
- parts.push(`## ${record.name} (still running, ${Date.now() - record.spawnedAt}ms elapsed)`);
584
- }
585
- }
586
- return parts.join('\n\n---\n\n');
587
- }, {
588
- name: 'collect_results',
589
- schema: { type: 'object', properties: {}, required: [] },
590
- description: 'Wait for all spawned agents to complete and collect their results. ' +
591
- 'Call this after spawning one or more agents to get their output.',
592
- }), tools.tool(async () => {
593
- const all = handoffReg.listAll();
594
- if (all.length === 0) {
595
- return 'No agents tracked.';
596
- }
597
- const lines = all.map((r) => {
598
- const elapsed = Date.now() - r.spawnedAt;
599
- if (r.status === 'running') {
600
- return `- **${r.name}**: running (${elapsed}ms elapsed) — task: ${r.task.substring(0, 100)}`;
601
- }
602
- else if (r.status === 'completed') {
603
- return `- **${r.name}**: completed (${r.durationMs}ms, ${r.resultText?.length ?? 0} chars)`;
604
- }
605
- else {
606
- return `- **${r.name}**: failed (${r.durationMs}ms) — ${r.error}`;
607
- }
608
- });
609
- const pending = all.filter((r) => r.status === 'running').length;
610
- const completed = all.filter((r) => r.status === 'completed').length;
611
- const failed = all.filter((r) => r.status === 'failed').length;
612
- return [
613
- `**Agent Status**: ${pending} running, ${completed} completed, ${failed} failed`,
614
- '',
615
- ...lines,
616
- ].join('\n');
617
- }, {
618
- name: 'check_agents',
619
- schema: { type: 'object', properties: {}, required: [] },
620
- description: 'Check the status of all spawned agents without waiting. ' +
621
- 'Shows which agents are running, completed, or failed.',
622
- }));
623
- console.debug(`[MultiAgentGraph] Handoff tools for "${agentId}": [${handoffTools.map((t) => t.name).join(', ')}, collect_results, check_agents]`);
611
+ // No collect_results tool needed — handoff tools use the browser-tool
612
+ // callback pattern: spawn child, wait for completion, return real result.
613
+ // The LLM naturally gets child results as tool return values.
614
+ logging.mlog(`[MultiAgentGraph] Handoff tools for "${agentId}": [${handoffTools.map((t) => t.name).join(', ')}]`);
624
615
  // Inject autonomous orchestration guidance for agents with handoff tools.
625
- // Modeled after OpenClaw's battle-tested subagent orchestration patterns.
626
616
  const childDescs = edges.flatMap((e) => {
627
617
  const dests = Array.isArray(e.to) ? e.to : [e.to];
628
618
  return dests.map((d) => {
@@ -637,14 +627,34 @@ class MultiAgentGraph extends Graph.StandardGraph {
637
627
  agentContext.additionalInstructions = existing
638
628
  ? `${existing}\n\n${orchestrationGuidance}`
639
629
  : orchestrationGuidance;
630
+ // Inject subagent context into each child/destination agent.
631
+ // This tells child agents they are subagents with a focused task — stay focused,
632
+ // execute (don't plan), and return results to the orchestrator.
633
+ const orchestratorName = agentContext.name ?? agentId;
634
+ const childAgentContext = this.buildChildAgentContext(orchestratorName);
635
+ for (const edge of edges) {
636
+ const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
637
+ for (const destId of dests) {
638
+ const destCtx = this.agentContexts.get(destId);
639
+ if (!destCtx)
640
+ continue;
641
+ const existingChild = destCtx.additionalInstructions ?? '';
642
+ // Avoid duplicate injection if agent is destination of multiple edges
643
+ if (existingChild.includes('# Subagent Context'))
644
+ continue;
645
+ destCtx.additionalInstructions = existingChild
646
+ ? `${existingChild}\n\n${childAgentContext}`
647
+ : childAgentContext;
648
+ }
649
+ }
640
650
  }
641
651
  }
642
652
  /**
643
653
  * Create handoff tools for an edge (handles multiple destinations).
644
654
  * Each handoff tool spawns the child agent's compiled subgraph asynchronously
645
655
  * and returns immediately. The orchestrator uses `collect_results` to retrieve
646
- * outputs and `check_agents` to monitor status — matching OpenClaw's
647
- * push-based autonomous orchestration pattern.
656
+ * outputs and `check_agents` to monitor status — a push-based autonomous
657
+ * orchestration pattern.
648
658
  *
649
659
  * @param edge - The graph edge defining the handoff
650
660
  * @param sourceAgentId - The ID of the parent/supervisor agent
@@ -658,12 +668,19 @@ class MultiAgentGraph extends Graph.StandardGraph {
658
668
  const destContext = this.agentContexts.get(destination);
659
669
  const toolDescription = edge.description ??
660
670
  this.buildDefaultHandoffDescription(destContext, destination);
661
- const hasPromptInput = edge.prompt != null && typeof edge.prompt === 'string';
662
- const promptInputDescription = hasPromptInput ? edge.prompt : undefined;
671
+ /**
672
+ * Always include an instructions parameter so the orchestrator can
673
+ * pass scoped task descriptions to each child agent. Without this,
674
+ * the child gets no context about what to do.
675
+ */
663
676
  const promptKey = edge.promptKey ?? 'instructions';
664
- /** Capture registry references — Map populated in createWorkflow() */
677
+ const destDesc = destContext?.description;
678
+ const promptInputDescription = edge.prompt ??
679
+ (destDesc
680
+ ? `Task instructions for this agent (${destDesc}). Describe exactly what it should do.`
681
+ : 'Specific task instructions for this agent. Describe exactly what it should do and what data to use.');
682
+ /** Capture registry reference — Map populated in createWorkflow() */
665
683
  const subgraphReg = this.subgraphRegistry;
666
- const handoffReg = this.handoffRegistry;
667
684
  tools$1.push(tools.tool(async (rawInput, config) => {
668
685
  const input = rawInput;
669
686
  const subgraph = subgraphReg.get(destination);
@@ -671,16 +688,62 @@ class MultiAgentGraph extends Graph.StandardGraph {
671
688
  throw new Error(`Handoff target "${destination}" subgraph not found in registry. ` +
672
689
  'This is a bug: createWorkflow() should have populated the subgraph registry.');
673
690
  }
674
- const state = langgraph.getCurrentTaskInput();
675
- let childMessages = MultiAgentGraph.prepareHandoffMessages([...state.messages]);
676
- /** Inject instructions as HumanMessage if provided by the parent LLM */
677
- const taskDescription = (hasPromptInput && promptKey in input && input[promptKey] != null)
691
+ /**
692
+ * Per-spawn unique key = the orchestrator's tool_call.id.
693
+ * LangChain's ToolNode passes `config.toolCall.id` to the tool
694
+ * function; this is the same id the frontend sees on the parent's
695
+ * handoff content part, so the UI can match each AgentHandoff
696
+ * indicator to its own sidebar task without collision when the
697
+ * same agent is invoked multiple times.
698
+ */
699
+ const toolCallCfg = config?.toolCall;
700
+ const spawnKey = toolCallCfg?.id ??
701
+ `${destination}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
702
+ /**
703
+ * Hierarchical spawnPath: parent's spawnPath (from metadata) + this spawnKey.
704
+ * Root invocations have empty parentSpawnPath. Threaded through childConfig
705
+ * so nested handoffs/sequences inherit the full ancestry.
706
+ * See docs/multi-agent-nesting-architecture.md §4.
707
+ */
708
+ const parentMetadata = config?.metadata;
709
+ const parentSpawnPath = typeof parentMetadata?.spawnPath === 'string'
710
+ ? parentMetadata.spawnPath
711
+ : '';
712
+ const childSpawnPath = spawnPath.buildSpawnPath(parentSpawnPath, spawnKey);
713
+ const childDepth = spawnPath.spawnPathDepth(childSpawnPath);
714
+ /**
715
+ * Child agent message construction — three modes:
716
+ *
717
+ * 1. Default (isolated-session pattern): Child gets ONLY the orchestrator's
718
+ * task instructions as a single HumanMessage. No parent conversation
719
+ * leaks. Orchestrator controls exactly what context the child sees.
720
+ *
721
+ * 2. Passthrough (edge.passthrough = true): Child gets the full parent
722
+ * conversation + orchestrator's instructions appended. Use this when
723
+ * the child needs the full user context (e.g., a transfer-like handoff).
724
+ *
725
+ * 3. Fallback: If no instructions provided AND not passthrough, child
726
+ * gets the agent's description as its task.
727
+ */
728
+ const taskDescription = promptKey in input && input[promptKey] != null
678
729
  ? String(input[promptKey])
679
730
  : '';
680
- if (taskDescription) {
731
+ let childMessages;
732
+ if (edge.passthrough) {
733
+ // Passthrough: full parent context + instructions appended
734
+ const state = langgraph.getCurrentTaskInput();
735
+ childMessages = MultiAgentGraph.prepareHandoffMessages([
736
+ ...state.messages,
737
+ ]);
738
+ if (taskDescription) {
739
+ childMessages.push(new messages.HumanMessage(taskDescription));
740
+ }
741
+ }
742
+ else {
743
+ // Default: isolated — only orchestrator's instructions
744
+ const fallbackTask = destContext?.description ?? 'Complete your assigned task.';
681
745
  childMessages = [
682
- ...childMessages,
683
- new messages.HumanMessage(taskDescription),
746
+ new messages.HumanMessage(taskDescription || fallbackTask),
684
747
  ];
685
748
  }
686
749
  const childState = {
@@ -688,15 +751,17 @@ class MultiAgentGraph extends Graph.StandardGraph {
688
751
  };
689
752
  const childContext = this.agentContexts.get(destination);
690
753
  const destName = destContext?.name ?? destination;
691
- console.debug(`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" SPAWN (async)\n` +
754
+ logging.mlog(`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" SPAWN (async)\n` +
692
755
  ` messages: ${childMessages.length}\n` +
693
756
  ` childTools: ${childContext?.tools?.length ?? 0} instances\n` +
694
757
  ` childToolDefs: ${childContext?.toolDefinitions?.length ?? 0} definitions`);
695
758
  /**
696
759
  * Dispatch transition BEFORE spawning the child subgraph so that
697
760
  * callbacks.js sets multiAgentTrace.isMultiAgent = true before the
698
- * child's ON_RUN_STEP events fire.
761
+ * child's ON_RUN_STEP events fire. spawnKey lets the UI create a
762
+ * distinct sidebar task for this specific invocation.
699
763
  */
764
+ logging.mlog(`[MultiAgentGraph] Handoff SPAWN "${sourceAgentId}" -> "${destination}" spawnKey=${spawnKey}`);
700
765
  await events.safeDispatchCustomEvent(_enum.GraphEvents.ON_AGENT_TRANSITION, {
701
766
  sourceAgentId: sourceAgentId,
702
767
  sourceAgentName: this.agentContexts.get(sourceAgentId)?.name ?? sourceAgentId,
@@ -704,59 +769,134 @@ class MultiAgentGraph extends Graph.StandardGraph {
704
769
  destinationAgentName: destName,
705
770
  edgeType: _enum.EdgeType.HANDOFF,
706
771
  timestamp: Date.now(),
772
+ spawnKey,
773
+ spawnPath: childSpawnPath,
774
+ parentSpawnPath: parentSpawnPath || null,
775
+ spawnDepth: childDepth,
707
776
  }, config);
708
777
  /**
709
- * Spawn child execution as a background promise (non-blocking).
710
- * The orchestrator gets an immediate response and can reason,
711
- * spawn more agents, or call collect_results when ready.
778
+ * Child events need to carry spawnKey so callbacks.js can route
779
+ * them to the correct child aggregator. LangChain propagates
780
+ * `metadata` and `tags` from the parent config to all descendants,
781
+ * so everything dispatched inside subgraph.invoke will have
782
+ * metadata.spawnKey populated on the event's runtime metadata.
783
+ */
784
+ const childConfig = {
785
+ ...(config ?? {}),
786
+ metadata: {
787
+ ...(config?.metadata ?? {}),
788
+ spawnKey,
789
+ spawnAgentId: destination,
790
+ /** Hierarchical identity — see spawnPath.ts */
791
+ spawnPath: childSpawnPath,
792
+ parentSpawnPath: parentSpawnPath || null,
793
+ spawnDepth: childDepth,
794
+ },
795
+ tags: [
796
+ ...(config?.tags ?? []),
797
+ `spawn:${spawnKey}`,
798
+ `depth:${childDepth}`,
799
+ ],
800
+ };
801
+ /**
802
+ * Callback pattern: spawn child, WAIT for completion, return real
803
+ * result. The parent naturally sees child results in its tool
804
+ * return, so no manual "collect_results" step is needed.
712
805
  *
713
- * The config is passed through so SSE callbacks, abort signal,
714
- * and configurable data still propagate to the child.
806
+ * Parallelism still works: when the LLM emits multiple handoff
807
+ * tool calls in one response, LangGraph runs all tool functions
808
+ * concurrently. Each waits for its child. All results land in
809
+ * the LLM's next turn together.
715
810
  */
716
- const childPromise = subgraph.invoke(childState, config);
717
- const capturedConfig = config;
718
- const parentAgentId = sourceAgentId;
719
- const parentCtx = this.agentContexts.get(sourceAgentId);
720
- handoffReg.spawn({
721
- id: destination,
722
- name: destName,
723
- task: taskDescription || '(no explicit instructions)',
724
- promise: childPromise,
725
- extractResult: MultiAgentGraph.extractHandoffResult,
726
- truncateResult: MultiAgentGraph.truncateHandoffResult,
727
- maxResultChars,
728
- onComplete: (record) => {
729
- console.debug(`[MultiAgentGraph] Handoff "${parentAgentId}" -> "${destination}" ${record.status.toUpperCase()} ` +
730
- `(${record.durationMs}ms, ${record.resultText?.length ?? 0} chars)`);
731
- /** Dispatch completion event for UI update */
811
+ const spawnedAt = Date.now();
812
+ try {
813
+ const result = await subgraph.invoke(childState, childConfig);
814
+ const durationMs = Date.now() - spawnedAt;
815
+ const resultText = MultiAgentGraph.extractHandoffResult(result.messages, destination);
816
+ const truncated = MultiAgentGraph.truncateHandoffResult(resultText, maxResultChars);
817
+ logging.mlog(`[MultiAgentGraph] Handoff COMPLETED "${sourceAgentId}" -> "${destination}" ` +
818
+ `spawnKey=${spawnKey} (${durationMs}ms, ${truncated.length} chars)`);
819
+ /** Dispatch completion event for UI update — carries spawnKey so
820
+ * the frontend can mark the correct sidebar task as completed. */
821
+ events.safeDispatchCustomEvent(_enum.GraphEvents.ON_AGENT_TRANSITION, {
822
+ sourceAgentId: destination,
823
+ sourceAgentName: destName,
824
+ destinationAgentId: sourceAgentId,
825
+ destinationAgentName: this.agentContexts.get(sourceAgentId)?.name ??
826
+ sourceAgentId,
827
+ edgeType: _enum.EdgeType.HANDOFF,
828
+ timestamp: Date.now(),
829
+ isCompletion: true,
830
+ durationMs,
831
+ resultLength: truncated.length,
832
+ spawnKey,
833
+ spawnPath: childSpawnPath,
834
+ parentSpawnPath: parentSpawnPath || null,
835
+ spawnDepth: childDepth,
836
+ }, config).catch(() => {
837
+ /* best-effort event dispatch */
838
+ });
839
+ return truncated;
840
+ }
841
+ catch (err) {
842
+ const durationMs = Date.now() - spawnedAt;
843
+ const errMsg = err instanceof Error ? err.message : String(err);
844
+ // EPIPE from console.debug is non-fatal
845
+ if (errMsg.includes('EPIPE')) {
846
+ logging.mwarn(`[MultiAgentGraph] Child "${destination}" hit EPIPE (non-fatal) spawnKey=${spawnKey}`);
732
847
  events.safeDispatchCustomEvent(_enum.GraphEvents.ON_AGENT_TRANSITION, {
733
848
  sourceAgentId: destination,
734
849
  sourceAgentName: destName,
735
- destinationAgentId: parentAgentId,
736
- destinationAgentName: parentCtx?.name ?? parentAgentId,
850
+ destinationAgentId: sourceAgentId,
851
+ destinationAgentName: this.agentContexts.get(sourceAgentId)?.name ??
852
+ sourceAgentId,
737
853
  edgeType: _enum.EdgeType.HANDOFF,
738
854
  timestamp: Date.now(),
739
855
  isCompletion: true,
740
- durationMs: record.durationMs,
741
- resultLength: record.resultText?.length ?? 0,
742
- }, capturedConfig).catch(() => { });
743
- },
744
- });
745
- return `[Agent "${destName}" spawned. Use collect_results to get the output when ready.]`;
856
+ durationMs,
857
+ spawnKey,
858
+ spawnPath: childSpawnPath,
859
+ parentSpawnPath: parentSpawnPath || null,
860
+ spawnDepth: childDepth,
861
+ }, config).catch(() => {
862
+ /* best-effort */
863
+ });
864
+ return `Agent "${destName}" completed but output was lost due to stream closure.`;
865
+ }
866
+ console.error(`[MultiAgentGraph] Handoff FAILED "${sourceAgentId}" -> "${destination}" ` +
867
+ `spawnKey=${spawnKey} (${durationMs}ms): ${errMsg}`);
868
+ events.safeDispatchCustomEvent(_enum.GraphEvents.ON_AGENT_TRANSITION, {
869
+ sourceAgentId: destination,
870
+ sourceAgentName: destName,
871
+ destinationAgentId: sourceAgentId,
872
+ destinationAgentName: this.agentContexts.get(sourceAgentId)?.name ??
873
+ sourceAgentId,
874
+ edgeType: _enum.EdgeType.HANDOFF,
875
+ timestamp: Date.now(),
876
+ isCompletion: true,
877
+ durationMs,
878
+ spawnKey,
879
+ spawnPath: childSpawnPath,
880
+ parentSpawnPath: parentSpawnPath || null,
881
+ spawnDepth: childDepth,
882
+ error: errMsg,
883
+ }, config).catch(() => {
884
+ /* best-effort */
885
+ });
886
+ return `Agent "${destName}" failed after ${durationMs}ms: ${errMsg}`;
887
+ }
746
888
  }, {
747
889
  name: toolName,
748
- schema: hasPromptInput
749
- ? {
750
- type: 'object',
751
- properties: {
752
- [promptKey]: {
753
- type: 'string',
754
- description: promptInputDescription,
755
- },
890
+ schema: {
891
+ type: 'object',
892
+ properties: {
893
+ [promptKey]: {
894
+ type: 'string',
895
+ description: promptInputDescription,
756
896
  },
757
- required: [],
758
- }
759
- : { type: 'object', properties: {}, required: [] },
897
+ },
898
+ required: [promptKey],
899
+ },
760
900
  description: toolDescription,
761
901
  }));
762
902
  }
@@ -798,7 +938,7 @@ class MultiAgentGraph extends Graph.StandardGraph {
798
938
  /**
799
939
  * Truncate handoff result using head/tail strategy (60/40 split).
800
940
  * Preserves the beginning (key findings) and end (conclusions).
801
- * Matches the TaskTool.truncateResult pattern from Ranger.
941
+ * Matches the TaskTool.truncateResult pattern used by host orchestrators.
802
942
  * @param result - The full result text
803
943
  * @param maxChars - Maximum allowed characters
804
944
  */
@@ -834,8 +974,140 @@ class MultiAgentGraph extends Graph.StandardGraph {
834
974
  * Create a complete agent subgraph (similar to createReactAgent)
835
975
  */
836
976
  createAgentSubgraph(agentId) {
837
- /** This is essentially the same as `createAgentNode` from `StandardGraph` */
838
- return this.createAgentNode(agentId);
977
+ /**
978
+ * Scoped subgraph build for handoff targets.
979
+ *
980
+ * If the handoff target has outgoing sequence/transfer edges (e.g. a
981
+ * "researcher" agent with its own sequence `[researcher → prod_assistant]`),
982
+ * we compile a mini-StateGraph containing the agent and all agents reachable
983
+ * from it via non-handoff edges. This way, when the parent hands off to
984
+ * researcher via `subgraph.invoke()`, the nested sequence runs to completion
985
+ * before the result is returned to the parent.
986
+ *
987
+ * Fast path: if no downstream agents are reachable, fall back to the
988
+ * previous single-node behavior (`createAgentNode`).
989
+ *
990
+ * See docs/multi-agent-nesting-architecture.md §6.
991
+ */
992
+ const reachable = this.computeReachableViaNonHandoff(agentId);
993
+ if (reachable.size === 1) {
994
+ return this.createAgentNode(agentId);
995
+ }
996
+ logging.mlog(`[MultiAgentGraph] Scoped subgraph for "${agentId}": ${reachable.size} nodes [${Array.from(reachable).join(', ')}]`);
997
+ return this.buildScopedSubgraph(agentId, reachable);
998
+ }
999
+ /**
1000
+ * BFS from `rootAgentId` across sequence + transfer edges (NOT handoff edges).
1001
+ * Returns the set of agents reachable in this agent's "local workflow".
1002
+ */
1003
+ computeReachableViaNonHandoff(rootAgentId) {
1004
+ const reachable = new Set([rootAgentId]);
1005
+ const queue = [rootAgentId];
1006
+ const localEdges = [...this.sequenceEdges, ...this.transferEdges];
1007
+ while (queue.length > 0) {
1008
+ const current = queue.shift();
1009
+ for (const edge of localEdges) {
1010
+ const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
1011
+ if (!sources.includes(current))
1012
+ continue;
1013
+ const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
1014
+ for (const dest of dests) {
1015
+ if (!reachable.has(dest) && this.agentContexts.has(dest)) {
1016
+ reachable.add(dest);
1017
+ queue.push(dest);
1018
+ }
1019
+ }
1020
+ }
1021
+ }
1022
+ return reachable;
1023
+ }
1024
+ /**
1025
+ * Build a compiled scoped StateGraph containing `agentIds` as nodes, rooted
1026
+ * at `rootAgentId`. Linear sequence edges where both endpoints are in scope
1027
+ * are wired directly; nodes with no outgoing in-scope edges route to END.
1028
+ *
1029
+ * Each node is wrapped around the per-agent `createAgentNode` compiled
1030
+ * workflow (agent + tools loop) to preserve isolated tool context.
1031
+ */
1032
+ buildScopedSubgraph(rootAgentId, agentIds) {
1033
+ const StateAnnotation = langgraph.Annotation.Root({
1034
+ messages: langgraph.Annotation({
1035
+ reducer: langgraph.messagesStateReducer,
1036
+ default: () => [],
1037
+ }),
1038
+ });
1039
+ const builder = new langgraph.StateGraph(StateAnnotation);
1040
+ // Precompile each scoped agent's inner workflow and wrap as a node.
1041
+ //
1042
+ // Two different isolation strategies depending on position:
1043
+ //
1044
+ // • ROOT node (the handoff target itself): receives the parent
1045
+ // orchestrator's handoff frame. Use `prepareHandoffMessages` — drops
1046
+ // orphaned tool_use, compacts paired tool calls, guarantees trailing
1047
+ // HumanMessage for Bedrock/VertexAI compatibility. The root needs
1048
+ // orchestrator context because it's responding to the handoff.
1049
+ //
1050
+ // • DOWNSTREAM nodes (sequence targets of the root): run as FULLY
1051
+ // ISOLATED child sessions. They receive only:
1052
+ // [original user request, synthetic HumanMessage describing what
1053
+ // the upstream agent produced and asking them to act]
1054
+ // No raw tool_use / tool_result blocks from the upstream agent —
1055
+ // prevents schema confusion when a downstream agent sees noisy
1056
+ // upstream context and produces malformed tool_use JSON.
1057
+ //
1058
+ // Each wrapper returns only the DELTA (new messages produced by the
1059
+ // inner invoke), not the prepared input — otherwise messagesStateReducer
1060
+ // would double-append the synthetic instruction into the scoped state.
1061
+ for (const aid of agentIds) {
1062
+ const inner = this.createAgentNode(aid);
1063
+ const isRoot = aid === rootAgentId;
1064
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1065
+ builder.addNode(aid, async (state, config) => {
1066
+ const prepared = isRoot
1067
+ ? MultiAgentGraph.prepareHandoffMessages(state.messages)
1068
+ : MultiAgentGraph.prepareIsolatedChildMessages(state.messages);
1069
+ logging.mlog(`[MultiAgentGraph] scoped node "${aid}" entering (isRoot=${isRoot}, stateMessages=${state.messages.length}, prepared=${prepared.length})`);
1070
+ const result = await inner.invoke({ ...state, messages: prepared }, config);
1071
+ // Return only the messages the inner node appended beyond its input,
1072
+ // so messagesStateReducer doesn't duplicate the synthetic wrapper
1073
+ // prompt into the scoped state.
1074
+ const delta = result.messages.length > prepared.length
1075
+ ? result.messages.slice(prepared.length)
1076
+ : result.messages;
1077
+ return { messages: delta };
1078
+ });
1079
+ }
1080
+ // START → root
1081
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1082
+ // @ts-ignore — LangGraph string typing is too strict for dynamic agent ids
1083
+ builder.addEdge(langgraph.START, rootAgentId);
1084
+ // Wire sequence edges in scope (linear chain support)
1085
+ const hasOutgoing = new Set();
1086
+ for (const edge of this.sequenceEdges) {
1087
+ const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
1088
+ const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
1089
+ for (const source of sources) {
1090
+ if (!agentIds.has(source))
1091
+ continue;
1092
+ for (const dest of dests) {
1093
+ if (!agentIds.has(dest))
1094
+ continue;
1095
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1096
+ // @ts-ignore
1097
+ builder.addEdge(source, dest);
1098
+ hasOutgoing.add(source);
1099
+ }
1100
+ }
1101
+ }
1102
+ // Leaves (no outgoing in-scope edges) route to END
1103
+ for (const aid of agentIds) {
1104
+ if (!hasOutgoing.has(aid)) {
1105
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1106
+ // @ts-ignore
1107
+ builder.addEdge(aid, langgraph.END);
1108
+ }
1109
+ }
1110
+ return builder.compile(this.compileOptions);
839
1111
  }
840
1112
  /**
841
1113
  * Detects if the current agent is receiving a handoff and processes the messages accordingly.
@@ -850,108 +1122,20 @@ class MultiAgentGraph extends Graph.StandardGraph {
850
1122
  * @returns Object with filtered messages, extracted instructions, source agent, and parallel siblings
851
1123
  */
852
1124
  /**
853
- * Prepare messages for a handoff child agent.
854
- *
855
- * Handles two problems:
856
- * 1. **Orphaned tool_use**: The parent's AI message contains a `tool_use` block
857
- * for the handoff tool itself, with no matching `tool_result`. Providers
858
- * (Bedrock/Anthropic) reject this.
859
- * 2. **Paired tool_use/tool_result in history**: The child may not have the same
860
- * tools as the parent. Bedrock requires `toolConfig` when tool_use/tool_result
861
- * blocks exist in the message history. Compacting these into text summaries
862
- * avoids the requirement and reduces context bloat.
863
- *
864
- * Strategy:
865
- * - Remove orphaned tool_use blocks (no matching tool_result)
866
- * - Compact paired tool_use/tool_result interactions into text summaries
1125
+ * Prepare messages for a handoff child agent. See
1126
+ * {@link prepareHandoffMessagesUtil} for the full implementation and
1127
+ * semantics this static method is a thin delegate preserved for
1128
+ * backward compatibility with existing call sites and unit tests.
867
1129
  */
868
- static prepareHandoffMessages(messages$1) {
869
- if (messages$1.length === 0)
870
- return messages$1;
871
- /** Collect all tool_result IDs so we know which tool_use blocks are paired */
872
- const pairedToolCallIds = new Set();
873
- for (const msg of messages$1) {
874
- if (msg.getType() === 'tool') {
875
- const tm = msg;
876
- if (tm.tool_call_id) {
877
- pairedToolCallIds.add(tm.tool_call_id);
878
- }
879
- }
880
- }
881
- /**
882
- * Pass 1: Remove orphaned tool_use blocks (no matching tool_result).
883
- * Also skip ToolMessages since we'll compact paired ones in pass 2.
884
- */
885
- const cleaned = [];
886
- for (const msg of messages$1) {
887
- /** Skip all ToolMessages — paired ones will be compacted in pass 2 */
888
- if (msg.getType() === 'tool') {
889
- continue;
890
- }
891
- if (msg.getType() !== 'ai') {
892
- cleaned.push(msg);
893
- continue;
894
- }
895
- const aiMsg = msg;
896
- const toolCalls = aiMsg.tool_calls ?? [];
897
- if (toolCalls.length === 0) {
898
- cleaned.push(msg);
899
- continue;
900
- }
901
- /** Extract text content from the AI message */
902
- const textContent = typeof aiMsg.content === 'string'
903
- ? aiMsg.content
904
- : Array.isArray(aiMsg.content)
905
- ? aiMsg.content
906
- .filter((b) => b.type === 'text' && 'text' in b)
907
- .map((b) => b.text ?? '')
908
- .join('\n')
909
- : '';
910
- /** Build text summaries of paired tool calls */
911
- const toolSummaries = [];
912
- for (const tc of toolCalls) {
913
- if (tc.id != null && pairedToolCallIds.has(tc.id)) {
914
- /** Find the matching ToolMessage result */
915
- const toolResult = messages$1.find((m) => m.getType() === 'tool' && m.tool_call_id === tc.id);
916
- const resultContent = toolResult
917
- ? typeof toolResult.content === 'string'
918
- ? toolResult.content.slice(0, 500)
919
- : '[complex result]'
920
- : '[no result]';
921
- toolSummaries.push(`[Tool "${tc.name}": ${resultContent}]`);
922
- }
923
- // Orphaned tool_use blocks (no matching result) are silently dropped
924
- }
925
- /** Reconstruct as plain text AI message (no tool_calls) */
926
- const parts = [textContent, ...toolSummaries].filter(Boolean);
927
- if (parts.length > 0) {
928
- cleaned.push(new messages.AIMessage({
929
- content: parts.join('\n\n'),
930
- id: aiMsg.id,
931
- }));
932
- }
933
- }
934
- /**
935
- * Ensure messages end with a HumanMessage.
936
- * After stripping tool artifacts, the last message may be an AIMessage
937
- * (orchestrator's reasoning before the handoff). Some providers (Bedrock,
938
- * Google/VertexAI) reject conversations ending with an assistant message.
939
- * Convert the trailing AIMessage to a HumanMessage to preserve any useful
940
- * context (e.g., compacted tool summaries) while satisfying the API requirement.
941
- */
942
- if (cleaned.length > 0 && cleaned[cleaned.length - 1].getType() === 'ai') {
943
- const lastAI = cleaned[cleaned.length - 1];
944
- const content = typeof lastAI.content === 'string'
945
- ? lastAI.content
946
- : '';
947
- if (content.trim()) {
948
- cleaned[cleaned.length - 1] = new messages.HumanMessage(`[Context from orchestrator]: ${content}`);
949
- }
950
- else {
951
- cleaned.pop();
952
- }
953
- }
954
- return cleaned;
1130
+ static prepareHandoffMessages(messages) {
1131
+ return childAgentContext.prepareHandoffMessages(messages);
1132
+ }
1133
+ /**
1134
+ * Build an isolated message context for a downstream scoped-subgraph
1135
+ * node. See {@link prepareIsolatedChildMessagesUtil} for details.
1136
+ */
1137
+ static prepareIsolatedChildMessages(messages) {
1138
+ return childAgentContext.prepareIsolatedChildMessages(messages);
955
1139
  }
956
1140
  processTransferReception(messages$1, agentId) {
957
1141
  if (messages$1.length === 0)
@@ -983,7 +1167,8 @@ class MultiAgentGraph extends Graph.StandardGraph {
983
1167
  }
984
1168
  else if (isConditionalTransfer) {
985
1169
  const transferDest = candidateMsg.additional_kwargs.handoff_destination;
986
- destinationAgent = typeof transferDest === 'string' ? transferDest : null;
1170
+ destinationAgent =
1171
+ typeof transferDest === 'string' ? transferDest : null;
987
1172
  }
988
1173
  /** Check if this transfer targets our agent */
989
1174
  if (destinationAgent === agentId) {
@@ -1224,8 +1409,35 @@ class MultiAgentGraph extends Graph.StandardGraph {
1224
1409
  for (const startNode of this.startingNodes) {
1225
1410
  handoffOnlyDestinations.delete(startNode);
1226
1411
  }
1412
+ /**
1413
+ * Nested-sequence expansion: for each handoff-only target, its downstream
1414
+ * sequence/transfer agents MUST also become handoff-only — they exist only
1415
+ * inside the target's scoped subgraph, not at top level. Without this,
1416
+ * those downstream nodes would be added as top-level orphans and LangGraph
1417
+ * would fail compilation (UNREACHABLE_NODE).
1418
+ *
1419
+ * See docs/multi-agent-nesting-architecture.md §6.
1420
+ */
1421
+ const nestedHandoffOnly = new Set();
1422
+ for (const target of handoffOnlyDestinations) {
1423
+ const reachable = this.computeReachableViaNonHandoff(target);
1424
+ for (const agent of reachable) {
1425
+ if (agent === target)
1426
+ continue;
1427
+ // Skip if this agent is legitimately a top-level starting node
1428
+ if (this.startingNodes.has(agent))
1429
+ continue;
1430
+ nestedHandoffOnly.add(agent);
1431
+ }
1432
+ }
1433
+ for (const agent of nestedHandoffOnly) {
1434
+ handoffOnlyDestinations.add(agent);
1435
+ }
1436
+ if (nestedHandoffOnly.size > 0) {
1437
+ logging.mlog(`[MultiAgentGraph] Nested handoff-only (scoped subgraph downstream): [${Array.from(nestedHandoffOnly).join(', ')}]`);
1438
+ }
1227
1439
  if (handoffOnlyDestinations.size > 0) {
1228
- console.debug(`[MultiAgentGraph] Handoff-only children (subgraph only, no top-level node): [${Array.from(handoffOnlyDestinations).join(', ')}]`);
1440
+ logging.mlog(`[MultiAgentGraph] Handoff-only children (subgraph only, no top-level node): [${Array.from(handoffOnlyDestinations).join(', ')}]`);
1229
1441
  }
1230
1442
  // Add agents as nodes — skip handoff-only children (they exist as subgraphs only)
1231
1443
  for (const [agentId] of this.agentContexts) {
@@ -1274,7 +1486,7 @@ class MultiAgentGraph extends Graph.StandardGraph {
1274
1486
  }
1275
1487
  /** Wrapper function that handles agentMessages channel, handoff reception, and conditional routing */
1276
1488
  const agentWrapper = async (state, config) => {
1277
- console.debug(`[MultiAgentGraph] Agent "${agentId}" wrapper ENTRY (messages: ${state.messages.length}, needsCommandRouting: ${needsCommandRouting})`);
1489
+ logging.mlog(`[MultiAgentGraph] Agent "${agentId}" wrapper ENTRY (messages: ${state.messages.length}, needsCommandRouting: ${needsCommandRouting})`);
1278
1490
  let result;
1279
1491
  /**
1280
1492
  * Check if this agent is receiving a transfer.
@@ -1285,7 +1497,7 @@ class MultiAgentGraph extends Graph.StandardGraph {
1285
1497
  const transferContext = this.processTransferReception(state.messages, agentId);
1286
1498
  if (transferContext !== null) {
1287
1499
  const { filteredMessages, instructions, sourceAgentName, parallelSiblings, } = transferContext;
1288
- console.debug(`[MultiAgentGraph] Agent "${agentId}" receiving transfer from "${sourceAgentName}" (instructions: ${instructions != null}, parallelSiblings: ${parallelSiblings.length})`);
1500
+ logging.mlog(`[MultiAgentGraph] Agent "${agentId}" receiving transfer from "${sourceAgentName}" (instructions: ${instructions != null}, parallelSiblings: ${parallelSiblings.length})`);
1289
1501
  /**
1290
1502
  * Set handoff context on the receiving agent.
1291
1503
  * Uses pre-computed graph position for depth and parallel info.
@@ -1401,7 +1613,7 @@ class MultiAgentGraph extends Graph.StandardGraph {
1401
1613
  }
1402
1614
  /** Track the last agent that produced output for continuation support */
1403
1615
  this.lastActiveAgentId = agentId;
1404
- console.debug(`[MultiAgentGraph] Agent "${agentId}" wrapper EXIT (result messages: ${result.messages.length})`);
1616
+ logging.mlog(`[MultiAgentGraph] Agent "${agentId}" wrapper EXIT (result messages: ${result.messages.length})`);
1405
1617
  /** If agent has both transfer and sequence edges, use Command for exclusive routing */
1406
1618
  if (needsCommandRouting) {
1407
1619
  /** Check if a transfer occurred */
@@ -1412,7 +1624,7 @@ class MultiAgentGraph extends Graph.StandardGraph {
1412
1624
  lastMessage.name.startsWith(_enum.Constants.LC_TRANSFER_TO_)) {
1413
1625
  /** Transfer occurred - extract destination and navigate there exclusively */
1414
1626
  const transferDest = lastMessage.name.replace(_enum.Constants.LC_TRANSFER_TO_, '');
1415
- console.debug(`[MultiAgentGraph] Command routing: "${agentId}" -> transfer to "${transferDest}" (sequence edges skipped: [${Array.from(sequenceDestinations).join(', ')}])`);
1627
+ logging.mlog(`[MultiAgentGraph] Command routing: "${agentId}" -> transfer to "${transferDest}" (sequence edges skipped: [${Array.from(sequenceDestinations).join(', ')}])`);
1416
1628
  /** Validate destination agent exists */
1417
1629
  if (!this.agentContexts.has(transferDest)) {
1418
1630
  const availableAgents = Array.from(this.agentContexts.keys()).join(', ');
@@ -1440,7 +1652,7 @@ class MultiAgentGraph extends Graph.StandardGraph {
1440
1652
  }
1441
1653
  const receiverBudget = receiverContext.maxContextTokens;
1442
1654
  if (currentSize > receiverBudget * 0.7) {
1443
- console.warn(`[MultiAgentGraph] Pre-handoff compaction: context (${currentSize} tokens) exceeds ` +
1655
+ logging.mwarn(`[MultiAgentGraph] Pre-handoff compaction: context (${currentSize} tokens) exceeds ` +
1444
1656
  `70% of receiver "${transferDest}" budget (${receiverBudget} tokens)`);
1445
1657
  /** Generate handoff briefing */
1446
1658
  const senderName = senderContext.name ?? agentId;
@@ -1505,7 +1717,7 @@ class MultiAgentGraph extends Graph.StandardGraph {
1505
1717
  }
1506
1718
  else {
1507
1719
  /** No transfer - proceed with sequence edges */
1508
- console.debug(`[MultiAgentGraph] Command routing: "${agentId}" -> no transfer, following sequence edges: [${Array.from(sequenceDestinations).join(', ')}]`);
1720
+ logging.mlog(`[MultiAgentGraph] Command routing: "${agentId}" -> no transfer, following sequence edges: [${Array.from(sequenceDestinations).join(', ')}]`);
1509
1721
  const directDests = Array.from(sequenceDestinations);
1510
1722
  for (const dest of directDests) {
1511
1723
  await events.safeDispatchCustomEvent(_enum.GraphEvents.ON_AGENT_TRANSITION, {
@@ -1537,9 +1749,14 @@ class MultiAgentGraph extends Graph.StandardGraph {
1537
1749
  * destinations so callbacks.js can register child agents for event
1538
1750
  * isolation BEFORE they start streaming.
1539
1751
  */
1540
- const allDests = new Set([...transferDestinations, ...sequenceDestinations]);
1752
+ const allDests = new Set([
1753
+ ...transferDestinations,
1754
+ ...sequenceDestinations,
1755
+ ]);
1541
1756
  if (allDests.size > 0) {
1542
- const edgeType = hasTransferEdges ? _enum.EdgeType.TRANSFER : _enum.EdgeType.SEQUENCE;
1757
+ const edgeType = hasTransferEdges
1758
+ ? _enum.EdgeType.TRANSFER
1759
+ : _enum.EdgeType.SEQUENCE;
1543
1760
  for (const dest of allDests) {
1544
1761
  await events.safeDispatchCustomEvent(_enum.GraphEvents.ON_AGENT_TRANSITION, {
1545
1762
  sourceAgentId: agentId,
@@ -1572,16 +1789,13 @@ class MultiAgentGraph extends Graph.StandardGraph {
1572
1789
  this.agentContexts.has(this.resumeFromAgentId);
1573
1790
  if (validResumeAgent) {
1574
1791
  const resumeAgentId = this.resumeFromAgentId;
1575
- console.debug(`[MultiAgentGraph] Multi-turn resumption: routing START → "${resumeAgentId}" (skipping default starting nodes: [${Array.from(this.startingNodes).join(', ')}])`);
1792
+ logging.mlog(`[MultiAgentGraph] Multi-turn resumption: routing START → "${resumeAgentId}" (skipping default starting nodes: [${Array.from(this.startingNodes).join(', ')}])`);
1576
1793
  /**
1577
1794
  * Build route map containing both the resume agent and default starting
1578
1795
  * nodes. This is required by LangGraph — all possible destinations must
1579
1796
  * be declared even if the router always picks one.
1580
1797
  */
1581
- const allPossibleStarts = new Set([
1582
- ...this.startingNodes,
1583
- resumeAgentId,
1584
- ]);
1798
+ const allPossibleStarts = new Set([...this.startingNodes, resumeAgentId]);
1585
1799
  const routeMap = {};
1586
1800
  for (const nodeId of allPossibleStarts) {
1587
1801
  routeMap[nodeId] = nodeId;
@@ -1590,7 +1804,7 @@ class MultiAgentGraph extends Graph.StandardGraph {
1590
1804
  }
1591
1805
  else {
1592
1806
  if (this.resumeFromAgentId != null) {
1593
- console.warn(`[MultiAgentGraph] resumeFromAgentId "${this.resumeFromAgentId}" not found in graph — falling back to default starting nodes`);
1807
+ logging.mwarn(`[MultiAgentGraph] resumeFromAgentId "${this.resumeFromAgentId}" not found in graph — falling back to default starting nodes`);
1594
1808
  }
1595
1809
  for (const startNode of this.startingNodes) {
1596
1810
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -1654,8 +1868,19 @@ class MultiAgentGraph extends Graph.StandardGraph {
1654
1868
  if (gatedEdges.has(edge)) {
1655
1869
  continue;
1656
1870
  }
1657
- const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
1658
- for (const destination of destinations) {
1871
+ /**
1872
+ * Skip sequence edges where either endpoint lives only inside a scoped
1873
+ * handoff subgraph. Those edges are wired inside `buildScopedSubgraph`,
1874
+ * not at the top level — adding them here would reference non-existent
1875
+ * top-level nodes and fail compilation.
1876
+ */
1877
+ const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
1878
+ const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
1879
+ const anyEndpointHandoffOnly = [...sources, ...dests].some((n) => handoffOnlyDestinations.has(n));
1880
+ if (anyEndpointHandoffOnly) {
1881
+ continue;
1882
+ }
1883
+ for (const destination of dests) {
1659
1884
  if (!edgesByDestination.has(destination)) {
1660
1885
  edgesByDestination.set(destination, []);
1661
1886
  }
@@ -1752,7 +1977,8 @@ class MultiAgentGraph extends Graph.StandardGraph {
1752
1977
  return eSources.includes(source);
1753
1978
  });
1754
1979
  /** Skip adding edge if source uses Command routing (has both types) */
1755
- if (sourceTransferEdges.length > 0 && sourceSequenceEdges.length > 0) {
1980
+ if (sourceTransferEdges.length > 0 &&
1981
+ sourceSequenceEdges.length > 0) {
1756
1982
  continue;
1757
1983
  }
1758
1984
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment