@illuma-ai/agents 1.1.25 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +20 -3
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/spawnPath.cjs +104 -0
  4. package/dist/cjs/common/spawnPath.cjs.map +1 -0
  5. package/dist/cjs/graphs/Graph.cjs +87 -31
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/HandoffRegistry.cjs +143 -0
  8. package/dist/cjs/graphs/HandoffRegistry.cjs.map +1 -0
  9. package/dist/cjs/graphs/MultiAgentGraph.cjs +587 -184
  10. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  11. package/dist/cjs/graphs/phases/flushLoop.cjs +214 -0
  12. package/dist/cjs/graphs/phases/flushLoop.cjs.map +1 -0
  13. package/dist/cjs/graphs/phases/memoryFlushPhase.cjs +102 -0
  14. package/dist/cjs/graphs/phases/memoryFlushPhase.cjs.map +1 -0
  15. package/dist/cjs/llm/bedrock/index.cjs +4 -3
  16. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  17. package/dist/cjs/main.cjs +115 -0
  18. package/dist/cjs/main.cjs.map +1 -1
  19. package/dist/cjs/memory/citations.cjs +69 -0
  20. package/dist/cjs/memory/citations.cjs.map +1 -0
  21. package/dist/cjs/memory/compositeBackend.cjs +60 -0
  22. package/dist/cjs/memory/compositeBackend.cjs.map +1 -0
  23. package/dist/cjs/memory/constants.cjs +232 -0
  24. package/dist/cjs/memory/constants.cjs.map +1 -0
  25. package/dist/cjs/memory/embeddings.cjs +151 -0
  26. package/dist/cjs/memory/embeddings.cjs.map +1 -0
  27. package/dist/cjs/memory/factory.cjs +95 -0
  28. package/dist/cjs/memory/factory.cjs.map +1 -0
  29. package/dist/cjs/memory/migrate.cjs +81 -0
  30. package/dist/cjs/memory/migrate.cjs.map +1 -0
  31. package/dist/cjs/memory/mmr.cjs +138 -0
  32. package/dist/cjs/memory/mmr.cjs.map +1 -0
  33. package/dist/cjs/memory/paths.cjs +217 -0
  34. package/dist/cjs/memory/paths.cjs.map +1 -0
  35. package/dist/cjs/memory/pgvectorStore.cjs +225 -0
  36. package/dist/cjs/memory/pgvectorStore.cjs.map +1 -0
  37. package/dist/cjs/memory/recallTracking.cjs +98 -0
  38. package/dist/cjs/memory/recallTracking.cjs.map +1 -0
  39. package/dist/cjs/memory/schema.sql +51 -0
  40. package/dist/cjs/memory/temporalDecay.cjs +118 -0
  41. package/dist/cjs/memory/temporalDecay.cjs.map +1 -0
  42. package/dist/cjs/nodes/ApprovalGateNode.cjs +1 -1
  43. package/dist/cjs/nodes/ApprovalGateNode.cjs.map +1 -1
  44. package/dist/cjs/prompts/memoryFlushPrompt.cjs +49 -0
  45. package/dist/cjs/prompts/memoryFlushPrompt.cjs.map +1 -0
  46. package/dist/cjs/run.cjs +16 -3
  47. package/dist/cjs/run.cjs.map +1 -1
  48. package/dist/cjs/stream.cjs +4 -4
  49. package/dist/cjs/stream.cjs.map +1 -1
  50. package/dist/cjs/tools/AskUser.cjs +6 -1
  51. package/dist/cjs/tools/AskUser.cjs.map +1 -1
  52. package/dist/cjs/tools/BrowserTools.cjs +1 -1
  53. package/dist/cjs/tools/BrowserTools.cjs.map +1 -1
  54. package/dist/cjs/tools/ToolNode.cjs +127 -10
  55. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  56. package/dist/cjs/tools/approval/constants.cjs +2 -2
  57. package/dist/cjs/tools/approval/constants.cjs.map +1 -1
  58. package/dist/cjs/tools/memory/index.cjs +58 -0
  59. package/dist/cjs/tools/memory/index.cjs.map +1 -0
  60. package/dist/cjs/tools/memory/memoryAppendTool.cjs +69 -0
  61. package/dist/cjs/tools/memory/memoryAppendTool.cjs.map +1 -0
  62. package/dist/cjs/tools/memory/memoryGetTool.cjs +49 -0
  63. package/dist/cjs/tools/memory/memoryGetTool.cjs.map +1 -0
  64. package/dist/cjs/tools/memory/memorySearchTool.cjs +65 -0
  65. package/dist/cjs/tools/memory/memorySearchTool.cjs.map +1 -0
  66. package/dist/cjs/tools/memory/shared.cjs +106 -0
  67. package/dist/cjs/tools/memory/shared.cjs.map +1 -0
  68. package/dist/cjs/types/graph.cjs.map +1 -1
  69. package/dist/cjs/utils/childAgentContext.cjs +242 -0
  70. package/dist/cjs/utils/childAgentContext.cjs.map +1 -0
  71. package/dist/cjs/utils/events.cjs +36 -4
  72. package/dist/cjs/utils/events.cjs.map +1 -1
  73. package/dist/cjs/utils/finishReasons.cjs +44 -0
  74. package/dist/cjs/utils/finishReasons.cjs.map +1 -0
  75. package/dist/cjs/utils/llm.cjs.map +1 -1
  76. package/dist/cjs/utils/logging.cjs +34 -0
  77. package/dist/cjs/utils/logging.cjs.map +1 -0
  78. package/dist/cjs/utils/toolCallNormalization.cjs +250 -0
  79. package/dist/cjs/utils/toolCallNormalization.cjs.map +1 -0
  80. package/dist/esm/agents/AgentContext.mjs +20 -3
  81. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  82. package/dist/esm/common/spawnPath.mjs +95 -0
  83. package/dist/esm/common/spawnPath.mjs.map +1 -0
  84. package/dist/esm/graphs/Graph.mjs +87 -31
  85. package/dist/esm/graphs/Graph.mjs.map +1 -1
  86. package/dist/esm/graphs/HandoffRegistry.mjs +141 -0
  87. package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -0
  88. package/dist/esm/graphs/MultiAgentGraph.mjs +587 -184
  89. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  90. package/dist/esm/graphs/phases/flushLoop.mjs +209 -0
  91. package/dist/esm/graphs/phases/flushLoop.mjs.map +1 -0
  92. package/dist/esm/graphs/phases/memoryFlushPhase.mjs +99 -0
  93. package/dist/esm/graphs/phases/memoryFlushPhase.mjs.map +1 -0
  94. package/dist/esm/llm/bedrock/index.mjs +4 -3
  95. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  96. package/dist/esm/main.mjs +21 -0
  97. package/dist/esm/main.mjs.map +1 -1
  98. package/dist/esm/memory/citations.mjs +64 -0
  99. package/dist/esm/memory/citations.mjs.map +1 -0
  100. package/dist/esm/memory/compositeBackend.mjs +58 -0
  101. package/dist/esm/memory/compositeBackend.mjs.map +1 -0
  102. package/dist/esm/memory/constants.mjs +198 -0
  103. package/dist/esm/memory/constants.mjs.map +1 -0
  104. package/dist/esm/memory/embeddings.mjs +148 -0
  105. package/dist/esm/memory/embeddings.mjs.map +1 -0
  106. package/dist/esm/memory/factory.mjs +93 -0
  107. package/dist/esm/memory/factory.mjs.map +1 -0
  108. package/dist/esm/memory/migrate.mjs +78 -0
  109. package/dist/esm/memory/migrate.mjs.map +1 -0
  110. package/dist/esm/memory/mmr.mjs +130 -0
  111. package/dist/esm/memory/mmr.mjs.map +1 -0
  112. package/dist/esm/memory/paths.mjs +207 -0
  113. package/dist/esm/memory/paths.mjs.map +1 -0
  114. package/dist/esm/memory/pgvectorStore.mjs +223 -0
  115. package/dist/esm/memory/pgvectorStore.mjs.map +1 -0
  116. package/dist/esm/memory/recallTracking.mjs +94 -0
  117. package/dist/esm/memory/recallTracking.mjs.map +1 -0
  118. package/dist/esm/memory/schema.sql +51 -0
  119. package/dist/esm/memory/temporalDecay.mjs +110 -0
  120. package/dist/esm/memory/temporalDecay.mjs.map +1 -0
  121. package/dist/esm/nodes/ApprovalGateNode.mjs +1 -1
  122. package/dist/esm/nodes/ApprovalGateNode.mjs.map +1 -1
  123. package/dist/esm/prompts/memoryFlushPrompt.mjs +44 -0
  124. package/dist/esm/prompts/memoryFlushPrompt.mjs.map +1 -0
  125. package/dist/esm/run.mjs +16 -3
  126. package/dist/esm/run.mjs.map +1 -1
  127. package/dist/esm/stream.mjs +4 -4
  128. package/dist/esm/stream.mjs.map +1 -1
  129. package/dist/esm/tools/AskUser.mjs +6 -1
  130. package/dist/esm/tools/AskUser.mjs.map +1 -1
  131. package/dist/esm/tools/BrowserTools.mjs +1 -1
  132. package/dist/esm/tools/BrowserTools.mjs.map +1 -1
  133. package/dist/esm/tools/ToolNode.mjs +128 -11
  134. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  135. package/dist/esm/tools/approval/constants.mjs +2 -2
  136. package/dist/esm/tools/approval/constants.mjs.map +1 -1
  137. package/dist/esm/tools/memory/index.mjs +46 -0
  138. package/dist/esm/tools/memory/index.mjs.map +1 -0
  139. package/dist/esm/tools/memory/memoryAppendTool.mjs +67 -0
  140. package/dist/esm/tools/memory/memoryAppendTool.mjs.map +1 -0
  141. package/dist/esm/tools/memory/memoryGetTool.mjs +47 -0
  142. package/dist/esm/tools/memory/memoryGetTool.mjs.map +1 -0
  143. package/dist/esm/tools/memory/memorySearchTool.mjs +63 -0
  144. package/dist/esm/tools/memory/memorySearchTool.mjs.map +1 -0
  145. package/dist/esm/tools/memory/shared.mjs +98 -0
  146. package/dist/esm/tools/memory/shared.mjs.map +1 -0
  147. package/dist/esm/types/graph.mjs.map +1 -1
  148. package/dist/esm/utils/childAgentContext.mjs +237 -0
  149. package/dist/esm/utils/childAgentContext.mjs.map +1 -0
  150. package/dist/esm/utils/events.mjs +36 -5
  151. package/dist/esm/utils/events.mjs.map +1 -1
  152. package/dist/esm/utils/finishReasons.mjs +41 -0
  153. package/dist/esm/utils/finishReasons.mjs.map +1 -0
  154. package/dist/esm/utils/llm.mjs.map +1 -1
  155. package/dist/esm/utils/logging.mjs +31 -0
  156. package/dist/esm/utils/logging.mjs.map +1 -0
  157. package/dist/esm/utils/toolCallNormalization.mjs +247 -0
  158. package/dist/esm/utils/toolCallNormalization.mjs.map +1 -0
  159. package/dist/types/common/index.d.ts +1 -0
  160. package/dist/types/common/spawnPath.d.ts +59 -0
  161. package/dist/types/graphs/HandoffRegistry.d.ts +97 -0
  162. package/dist/types/graphs/MultiAgentGraph.d.ts +58 -18
  163. package/dist/types/graphs/index.d.ts +1 -0
  164. package/dist/types/graphs/phases/flushLoop.d.ts +106 -0
  165. package/dist/types/graphs/phases/memoryFlushPhase.d.ts +100 -0
  166. package/dist/types/index.d.ts +7 -0
  167. package/dist/types/memory/__tests__/mockBackend.d.ts +40 -0
  168. package/dist/types/memory/citations.d.ts +39 -0
  169. package/dist/types/memory/compositeBackend.d.ts +30 -0
  170. package/dist/types/memory/constants.d.ts +121 -0
  171. package/dist/types/memory/embeddings.d.ts +15 -0
  172. package/dist/types/memory/factory.d.ts +23 -0
  173. package/dist/types/memory/index.d.ts +21 -0
  174. package/dist/types/memory/migrate.d.ts +14 -0
  175. package/dist/types/memory/mmr.d.ts +50 -0
  176. package/dist/types/memory/paths.d.ts +107 -0
  177. package/dist/types/memory/pgvectorStore.d.ts +56 -0
  178. package/dist/types/memory/recallTracking.d.ts +30 -0
  179. package/dist/types/memory/temporalDecay.d.ts +53 -0
  180. package/dist/types/memory/types.d.ts +182 -0
  181. package/dist/types/prompts/memoryFlushPrompt.d.ts +54 -0
  182. package/dist/types/run.d.ts +1 -0
  183. package/dist/types/tools/AskUser.d.ts +1 -1
  184. package/dist/types/tools/BrowserTools.d.ts +2 -2
  185. package/dist/types/tools/approval/constants.d.ts +2 -2
  186. package/dist/types/tools/memory/index.d.ts +39 -0
  187. package/dist/types/tools/memory/memoryAppendTool.d.ts +27 -0
  188. package/dist/types/tools/memory/memoryGetTool.d.ts +22 -0
  189. package/dist/types/tools/memory/memorySearchTool.d.ts +22 -0
  190. package/dist/types/tools/memory/shared.d.ts +106 -0
  191. package/dist/types/types/graph.d.ts +16 -3
  192. package/dist/types/utils/childAgentContext.d.ts +99 -0
  193. package/dist/types/utils/events.d.ts +21 -0
  194. package/dist/types/utils/finishReasons.d.ts +32 -0
  195. package/dist/types/utils/logging.d.ts +2 -0
  196. package/dist/types/utils/toolCallNormalization.d.ts +44 -0
  197. package/package.json +6 -4
  198. package/src/agents/AgentContext.ts +26 -3
  199. package/src/common/__tests__/enum.test.ts +4 -2
  200. package/src/common/__tests__/spawnPath.test.ts +110 -0
  201. package/src/common/index.ts +1 -0
  202. package/src/common/spawnPath.ts +101 -0
  203. package/src/graphs/Graph.ts +94 -43
  204. package/src/graphs/HandoffRegistry.ts +199 -0
  205. package/src/graphs/MultiAgentGraph.ts +694 -226
  206. package/src/graphs/__tests__/HandoffRegistry.test.ts +410 -0
  207. package/src/graphs/__tests__/multi-agent-delegate.test.ts +61 -16
  208. package/src/graphs/__tests__/multi-agent-edges.test.ts +4 -2
  209. package/src/graphs/__tests__/multi-agent-nested-subgraph.test.ts +221 -0
  210. package/src/graphs/__tests__/structured-output.integration.test.ts +212 -118
  211. package/src/graphs/contextManagement.e2e.test.ts +1 -1
  212. package/src/graphs/index.ts +1 -0
  213. package/src/graphs/phases/__tests__/flushLoop.test.ts +264 -0
  214. package/src/graphs/phases/__tests__/memoryFlushPhase.test.ts +37 -0
  215. package/src/graphs/phases/__tests__/runMemoryFlush.test.ts +150 -0
  216. package/src/graphs/phases/flushLoop.ts +303 -0
  217. package/src/graphs/phases/memoryFlushPhase.ts +209 -0
  218. package/src/index.ts +30 -1
  219. package/src/llm/bedrock/index.ts +4 -5
  220. package/src/memory/__tests__/citations.test.ts +61 -0
  221. package/src/memory/__tests__/compositeBackend.test.ts +79 -0
  222. package/src/memory/__tests__/isolation.test.ts +206 -0
  223. package/src/memory/__tests__/mmr.test.ts +148 -0
  224. package/src/memory/__tests__/mockBackend.ts +161 -0
  225. package/src/memory/__tests__/paths.test.ts +168 -0
  226. package/src/memory/__tests__/recallTracking.test.ts +96 -0
  227. package/src/memory/__tests__/temporalDecay.test.ts +151 -0
  228. package/src/memory/citations.ts +80 -0
  229. package/src/memory/compositeBackend.ts +99 -0
  230. package/src/memory/constants.ts +229 -0
  231. package/src/memory/embeddings.ts +188 -0
  232. package/src/memory/factory.ts +111 -0
  233. package/src/memory/index.ts +46 -0
  234. package/src/memory/migrate.ts +116 -0
  235. package/src/memory/mmr.ts +161 -0
  236. package/src/memory/paths.ts +258 -0
  237. package/src/memory/pgvectorStore.ts +324 -0
  238. package/src/memory/recallTracking.ts +127 -0
  239. package/src/memory/schema.sql +51 -0
  240. package/src/memory/temporalDecay.ts +134 -0
  241. package/src/memory/types.ts +185 -0
  242. package/src/nodes/ApprovalGateNode.ts +4 -10
  243. package/src/nodes/__tests__/ApprovalGateNode.test.ts +11 -20
  244. package/src/prompts/memoryFlushPrompt.ts +78 -0
  245. package/src/run.ts +17 -6
  246. package/src/scripts/test-bedrock-handoff-autonomous.ts +56 -20
  247. package/src/specs/agent-handoffs-bedrock.integration.test.ts +8 -5
  248. package/src/specs/agent-handoffs.test.ts +8 -2
  249. package/src/stream.ts +4 -6
  250. package/src/tools/AskUser.ts +7 -2
  251. package/src/tools/BrowserTools.ts +3 -5
  252. package/src/tools/ToolNode.ts +150 -13
  253. package/src/tools/__tests__/ToolApproval.test.ts +22 -9
  254. package/src/tools/approval/__tests__/constants.test.ts +4 -4
  255. package/src/tools/approval/constants.ts +2 -2
  256. package/src/tools/memory/__tests__/memoryTools.test.ts +205 -0
  257. package/src/tools/memory/index.ts +96 -0
  258. package/src/tools/memory/memoryAppendTool.ts +101 -0
  259. package/src/tools/memory/memoryGetTool.ts +53 -0
  260. package/src/tools/memory/memorySearchTool.ts +80 -0
  261. package/src/tools/memory/shared.ts +169 -0
  262. package/src/tools/search/search.test.ts +6 -1
  263. package/src/types/graph.ts +16 -3
  264. package/src/utils/__tests__/childAgentContext.test.ts +217 -0
  265. package/src/utils/__tests__/finishReasons.test.ts +55 -0
  266. package/src/utils/__tests__/toolCallNormalization.test.ts +181 -0
  267. package/src/utils/childAgentContext.ts +259 -0
  268. package/src/utils/events.ts +37 -4
  269. package/src/utils/finishReasons.ts +40 -0
  270. package/src/utils/llm.ts +0 -1
  271. package/src/utils/logging.ts +45 -8
  272. package/src/utils/toolCallNormalization.ts +271 -0
@@ -1,17 +1,22 @@
1
1
  import { tool } from '@langchain/core/tools';
2
2
  import { PromptTemplate } from '@langchain/core/prompts';
3
3
  import { ToolMessage, AIMessage, HumanMessage, getBufferString, SystemMessage } from '@langchain/core/messages';
4
- import { getCurrentTaskInput, Command, Annotation, messagesStateReducer, StateGraph, END, START } from '@langchain/langgraph';
4
+ import { getCurrentTaskInput, Command, Annotation, messagesStateReducer, StateGraph, START, END } from '@langchain/langgraph';
5
5
  import '../messages/core.mjs';
6
6
  import 'nanoid';
7
7
  import { EdgeType, Constants, DEFAULT_HANDOFF_MAX_RESULT_CHARS, GraphEvents } from '../common/enum.mjs';
8
+ import { buildSpawnPath, spawnPathDepth } from '../common/spawnPath.mjs';
8
9
  import '../tools/approval/constants.mjs';
9
10
  import '../utils/toonFormat.mjs';
10
11
  import { summarize, createEmergencySummary } from '../messages/summarize.mjs';
11
12
  import { StandardGraph } from './Graph.mjs';
12
13
  import { safeDispatchCustomEvent } from '../utils/events.mjs';
14
+ import { mlog, mwarn } from '../utils/logging.mjs';
15
+ import { prepareHandoffMessages, prepareIsolatedChildMessages } from '../utils/childAgentContext.mjs';
13
16
  import { getApprovalGateNodeId, createApprovalGateNode } from '../nodes/ApprovalGateNode.mjs';
14
17
 
18
+ // HandoffRegistry no longer needed — handoff tools use synchronous
19
+ // browser-tool callback pattern (spawn → wait → return result)
15
20
  /** Pattern to extract instructions from transfer ToolMessage content */
16
21
  const TRANSFER_INSTRUCTIONS_PATTERN = /(?:Instructions?|Context):\s*(.+)/is;
17
22
  /**
@@ -58,6 +63,12 @@ class MultiAgentGraph extends StandardGraph {
58
63
  * Used by auto-continuation to know which agent's context to preserve after handoff.
59
64
  */
60
65
  lastActiveAgentId;
66
+ /**
67
+ * Registry for async handoff execution.
68
+ * Enables autonomous orchestration: spawn children non-blocking,
69
+ * orchestrator stays alive to reason and collect results when ready.
70
+ */
71
+ // HandoffRegistry removed — handoff tools are synchronous (callback pattern)
61
72
  /**
62
73
  * When set, the graph routes START to this agent instead of the default starting nodes.
63
74
  * Enables multi-turn resumption: follow-up messages go to the agent that last handled
@@ -72,7 +83,7 @@ class MultiAgentGraph extends StandardGraph {
72
83
  this.analyzeGraph();
73
84
  this.createTransferTools();
74
85
  this.createHandoffTools();
75
- console.debug(`[MultiAgentGraph] Constructor complete: ${this.agentContexts.size} agents, ${this.edges.length} edges`);
86
+ mlog(`[MultiAgentGraph] Constructor complete: ${this.agentContexts.size} agents, ${this.edges.length} edges`);
76
87
  }
77
88
  /**
78
89
  * Categorize edges into handoff, transfer, and sequence types
@@ -85,7 +96,8 @@ class MultiAgentGraph extends StandardGraph {
85
96
  else if (edge.edgeType === EdgeType.SEQUENCE) {
86
97
  this.sequenceEdges.push(edge);
87
98
  }
88
- else if (edge.edgeType === EdgeType.TRANSFER || edge.condition != null) {
99
+ else if (edge.edgeType === EdgeType.TRANSFER ||
100
+ edge.condition != null) {
89
101
  this.transferEdges.push(edge);
90
102
  }
91
103
  else {
@@ -102,7 +114,7 @@ class MultiAgentGraph extends StandardGraph {
102
114
  }
103
115
  }
104
116
  }
105
- console.debug(`[MultiAgentGraph] Edge categorization: ${this.handoffEdges.length} handoff, ${this.transferEdges.length} transfer, ${this.sequenceEdges.length} sequence (of ${this.edges.length} total)`);
117
+ mlog(`[MultiAgentGraph] Edge categorization: ${this.handoffEdges.length} handoff, ${this.transferEdges.length} transfer, ${this.sequenceEdges.length} sequence (of ${this.edges.length} total)`);
106
118
  }
107
119
  /**
108
120
  * Analyze graph structure to determine starting nodes and connections
@@ -124,7 +136,7 @@ class MultiAgentGraph extends StandardGraph {
124
136
  if (this.startingNodes.size === 0 && this.agentContexts.size > 0) {
125
137
  this.startingNodes.add(this.agentContexts.keys().next().value);
126
138
  }
127
- console.debug(`[MultiAgentGraph] Starting nodes identified: [${Array.from(this.startingNodes).join(', ')}]`);
139
+ mlog(`[MultiAgentGraph] Starting nodes identified: [${Array.from(this.startingNodes).join(', ')}]`);
128
140
  // Determine if graph has parallel execution capability
129
141
  this.computeParallelCapability();
130
142
  }
@@ -259,7 +271,22 @@ class MultiAgentGraph extends StandardGraph {
259
271
  agentContext.graphTools = [];
260
272
  }
261
273
  agentContext.graphTools.push(...transferTools);
262
- console.debug(`[MultiAgentGraph] Transfer tools for "${agentId}": [${transferTools.map((t) => t.name).join(', ')}]`);
274
+ mlog(`[MultiAgentGraph] Transfer tools for "${agentId}": [${transferTools.map((t) => t.name).join(', ')}]`);
275
+ // Inject orchestration guidance for agents with transfer tools
276
+ const childDescs = edges.flatMap((e) => {
277
+ const dests = Array.isArray(e.to) ? e.to : [e.to];
278
+ return dests.map((d) => {
279
+ const ctx = this.agentContexts.get(d);
280
+ const name = ctx?.name ?? d;
281
+ const desc = ctx?.description ? ` — ${ctx.description}` : '';
282
+ return `${name}${desc}`;
283
+ });
284
+ });
285
+ const guidance = this.buildOrchestratorGuidance(childDescs, transferTools.length);
286
+ const existing = agentContext.additionalInstructions ?? '';
287
+ agentContext.additionalInstructions = existing
288
+ ? `${existing}\n\n${guidance}`
289
+ : guidance;
263
290
  }
264
291
  }
265
292
  /**
@@ -278,7 +305,9 @@ class MultiAgentGraph extends StandardGraph {
278
305
  const toolDescription = edge.description ?? 'Conditionally transfer control based on state';
279
306
  /** Check if we have a prompt for handoff input */
280
307
  const hasTransferInput = edge.prompt != null && typeof edge.prompt === 'string';
281
- const transferInputDescription = hasTransferInput ? edge.prompt : undefined;
308
+ const transferInputDescription = hasTransferInput
309
+ ? edge.prompt
310
+ : undefined;
282
311
  const promptKey = edge.promptKey ?? 'instructions';
283
312
  tools.push(tool(async (rawInput, config) => {
284
313
  const input = rawInput;
@@ -453,6 +482,83 @@ class MultiAgentGraph extends StandardGraph {
453
482
  }
454
483
  return tools;
455
484
  }
485
+ /**
486
+ * Builds orchestration guidance injected into the system message of agents
487
+ * that have handoff or transfer tools (i.e., orchestrator agents).
488
+ *
489
+ * Implements two orchestration primitives:
490
+ * - Execution bias guidance injected into the system prompt
491
+ * - Multi-round autonomous execution for dependent tasks
492
+ *
493
+ * Handoff tools are synchronous (browser-tool callback pattern): spawn the
494
+ * child, await completion, return the real text as the tool output. Parallel
495
+ * handoff tool calls in one turn run concurrently via LangGraph's ToolNode,
496
+ * so independent children run in parallel without explicit orchestration.
497
+ *
498
+ * @param childDescs - Display names (with optional descriptions) of child agents
499
+ * @param toolCount - Number of handoff/transfer tools available
500
+ */
501
+ buildOrchestratorGuidance(childDescs, toolCount) {
502
+ return [
503
+ '## Agent Orchestration',
504
+ '',
505
+ `You have ${toolCount} specialist agent(s) available for delegation:`,
506
+ ...childDescs.map((d) => `- ${d}`),
507
+ '',
508
+ '## Execution Bias',
509
+ 'If the user asks you to do the work, start doing it in the same turn.',
510
+ '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.',
511
+ 'Commentary-only turns are incomplete when tools are available and the next action is clear.',
512
+ 'If the work will take multiple steps or a while to finish, send one short progress update before or while acting.',
513
+ '',
514
+ '## How Delegation Works',
515
+ 'Each handoff tool call spawns a sub-agent, waits for it to complete, and returns the real result directly — like a function call.',
516
+ 'Independent tasks MAY be called in parallel (multiple handoff tool calls in one turn). They run concurrently and all results return together.',
517
+ 'Dependent tasks MUST be sequential: call one agent, get the result, then call the next agent using that real data.',
518
+ '',
519
+ '## Agent Isolation',
520
+ "Sub-agents CANNOT see your conversation or the user's original message. They ONLY see what you write in the `instructions` parameter.",
521
+ "When writing instructions, include ALL data the agent needs. Copy exact values from the user's message — email addresses, names, URLs, dates, numbers.",
522
+ 'When delegating follow-up work, include the real data from prior agent results directly in the instructions text.',
523
+ 'Do NOT re-delegate a task that was already completed. If you have the data, pass it directly.',
524
+ ].join('\n');
525
+ }
526
+ /**
527
+ * Builds subagent context instructions injected into child agents that are
528
+ * handoff destinations. This tells the child agent it is a subagent with
529
+ * a focused task.
530
+ *
531
+ * @param orchestratorName - Display name of the parent orchestrator agent
532
+ */
533
+ buildChildAgentContext(orchestratorName) {
534
+ return [
535
+ '# Subagent Context',
536
+ '',
537
+ `You are a **subagent** spawned by the ${orchestratorName} for a specific task.`,
538
+ '',
539
+ '## Your Role',
540
+ "- Complete this task. That's your entire purpose.",
541
+ `- You are NOT the ${orchestratorName}. Don't try to be.`,
542
+ '',
543
+ '## Rules',
544
+ '1. **Stay focused** - Do your assigned task, nothing else',
545
+ `2. **Complete the task** - Your final message will be automatically reported to the ${orchestratorName}`,
546
+ "3. **Don't initiate** - No heartbeats, no proactive actions, no side quests",
547
+ "4. **Be ephemeral** - You may be terminated after task completion. That's fine.",
548
+ '',
549
+ '## Output Format',
550
+ 'When complete, your final response should include:',
551
+ '- What you accomplished or found',
552
+ `- Any relevant details the ${orchestratorName} should know`,
553
+ '- Keep it concise but informative',
554
+ '',
555
+ "## What You DON'T Do",
556
+ `- NO user conversations (that's ${orchestratorName}'s job)`,
557
+ '- NO external messages (email, tweets, etc.) unless explicitly tasked with a specific recipient/channel',
558
+ '- NO cron jobs or persistent state',
559
+ `- NO pretending to be the ${orchestratorName}`,
560
+ ].join('\n');
561
+ }
456
562
  /**
457
563
  * Builds a meaningful default description for a transfer tool when no explicit
458
564
  * edge.description is provided. Uses the destination agent's name and description
@@ -500,14 +606,53 @@ class MultiAgentGraph extends StandardGraph {
500
606
  agentContext.graphTools = [];
501
607
  }
502
608
  agentContext.graphTools.push(...handoffTools);
503
- console.debug(`[MultiAgentGraph] Handoff tools for "${agentId}": [${handoffTools.map((t) => t.name).join(', ')}]`);
609
+ // No collect_results tool needed handoff tools use the browser-tool
610
+ // callback pattern: spawn child, wait for completion, return real result.
611
+ // The LLM naturally gets child results as tool return values.
612
+ mlog(`[MultiAgentGraph] Handoff tools for "${agentId}": [${handoffTools.map((t) => t.name).join(', ')}]`);
613
+ // Inject autonomous orchestration guidance for agents with handoff tools.
614
+ const childDescs = edges.flatMap((e) => {
615
+ const dests = Array.isArray(e.to) ? e.to : [e.to];
616
+ return dests.map((d) => {
617
+ const ctx = this.agentContexts.get(d);
618
+ const name = ctx?.name ?? d;
619
+ const desc = ctx?.description ? ` — ${ctx.description}` : '';
620
+ return `${name}${desc}`;
621
+ });
622
+ });
623
+ const orchestrationGuidance = this.buildOrchestratorGuidance(childDescs, handoffTools.length);
624
+ const existing = agentContext.additionalInstructions ?? '';
625
+ agentContext.additionalInstructions = existing
626
+ ? `${existing}\n\n${orchestrationGuidance}`
627
+ : orchestrationGuidance;
628
+ // Inject subagent context into each child/destination agent.
629
+ // This tells child agents they are subagents with a focused task — stay focused,
630
+ // execute (don't plan), and return results to the orchestrator.
631
+ const orchestratorName = agentContext.name ?? agentId;
632
+ const childAgentContext = this.buildChildAgentContext(orchestratorName);
633
+ for (const edge of edges) {
634
+ const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
635
+ for (const destId of dests) {
636
+ const destCtx = this.agentContexts.get(destId);
637
+ if (!destCtx)
638
+ continue;
639
+ const existingChild = destCtx.additionalInstructions ?? '';
640
+ // Avoid duplicate injection if agent is destination of multiple edges
641
+ if (existingChild.includes('# Subagent Context'))
642
+ continue;
643
+ destCtx.additionalInstructions = existingChild
644
+ ? `${existingChild}\n\n${childAgentContext}`
645
+ : childAgentContext;
646
+ }
647
+ }
504
648
  }
505
649
  }
506
650
  /**
507
651
  * Create handoff tools for an edge (handles multiple destinations).
508
- * Each handoff tool invokes the child agent's compiled subgraph inline,
509
- * extracts the final AI message text, truncates it, and returns it as
510
- * a string (which becomes a ToolMessage in the parent's context).
652
+ * Each handoff tool spawns the child agent's compiled subgraph asynchronously
653
+ * and returns immediately. The orchestrator uses `collect_results` to retrieve
654
+ * outputs and `check_agents` to monitor status a push-based autonomous
655
+ * orchestration pattern.
511
656
  *
512
657
  * @param edge - The graph edge defining the handoff
513
658
  * @param sourceAgentId - The ID of the parent/supervisor agent
@@ -521,81 +666,235 @@ class MultiAgentGraph extends StandardGraph {
521
666
  const destContext = this.agentContexts.get(destination);
522
667
  const toolDescription = edge.description ??
523
668
  this.buildDefaultHandoffDescription(destContext, destination);
524
- const hasPromptInput = edge.prompt != null && typeof edge.prompt === 'string';
525
- const promptInputDescription = hasPromptInput ? edge.prompt : undefined;
669
+ /**
670
+ * Always include an instructions parameter so the orchestrator can
671
+ * pass scoped task descriptions to each child agent. Without this,
672
+ * the child gets no context about what to do.
673
+ */
526
674
  const promptKey = edge.promptKey ?? 'instructions';
675
+ const destDesc = destContext?.description;
676
+ const promptInputDescription = edge.prompt ??
677
+ (destDesc
678
+ ? `Task instructions for this agent (${destDesc}). Describe exactly what it should do.`
679
+ : 'Specific task instructions for this agent. Describe exactly what it should do and what data to use.');
527
680
  /** Capture registry reference — Map populated in createWorkflow() */
528
- const registry = this.subgraphRegistry;
681
+ const subgraphReg = this.subgraphRegistry;
529
682
  tools.push(tool(async (rawInput, config) => {
530
683
  const input = rawInput;
531
- const subgraph = registry.get(destination);
684
+ const subgraph = subgraphReg.get(destination);
532
685
  if (!subgraph) {
533
686
  throw new Error(`Handoff target "${destination}" subgraph not found in registry. ` +
534
687
  'This is a bug: createWorkflow() should have populated the subgraph registry.');
535
688
  }
536
- const state = getCurrentTaskInput();
537
- let childMessages = MultiAgentGraph.prepareHandoffMessages([...state.messages]);
538
- /** Inject instructions as HumanMessage if provided by the parent LLM */
539
- if (hasPromptInput &&
540
- promptKey in input &&
541
- input[promptKey] != null) {
689
+ /**
690
+ * Per-spawn unique key = the orchestrator's tool_call.id.
691
+ * LangChain's ToolNode passes `config.toolCall.id` to the tool
692
+ * function; this is the same id the frontend sees on the parent's
693
+ * handoff content part, so the UI can match each AgentHandoff
694
+ * indicator to its own sidebar task without collision when the
695
+ * same agent is invoked multiple times.
696
+ */
697
+ const toolCallCfg = config?.toolCall;
698
+ const spawnKey = toolCallCfg?.id ??
699
+ `${destination}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
700
+ /**
701
+ * Hierarchical spawnPath: parent's spawnPath (from metadata) + this spawnKey.
702
+ * Root invocations have empty parentSpawnPath. Threaded through childConfig
703
+ * so nested handoffs/sequences inherit the full ancestry.
704
+ * See docs/multi-agent-nesting-architecture.md §4.
705
+ */
706
+ const parentMetadata = config?.metadata;
707
+ const parentSpawnPath = typeof parentMetadata?.spawnPath === 'string'
708
+ ? parentMetadata.spawnPath
709
+ : '';
710
+ const childSpawnPath = buildSpawnPath(parentSpawnPath, spawnKey);
711
+ const childDepth = spawnPathDepth(childSpawnPath);
712
+ /**
713
+ * Child agent message construction — three modes:
714
+ *
715
+ * 1. Default (isolated-session pattern): Child gets ONLY the orchestrator's
716
+ * task instructions as a single HumanMessage. No parent conversation
717
+ * leaks. Orchestrator controls exactly what context the child sees.
718
+ *
719
+ * 2. Passthrough (edge.passthrough = true): Child gets the full parent
720
+ * conversation + orchestrator's instructions appended. Use this when
721
+ * the child needs the full user context (e.g., a transfer-like handoff).
722
+ *
723
+ * 3. Fallback: If no instructions provided AND not passthrough, child
724
+ * gets the agent's description as its task.
725
+ */
726
+ const taskDescription = promptKey in input && input[promptKey] != null
727
+ ? String(input[promptKey])
728
+ : '';
729
+ let childMessages;
730
+ if (edge.passthrough) {
731
+ // Passthrough: full parent context + instructions appended
732
+ const state = getCurrentTaskInput();
733
+ childMessages = MultiAgentGraph.prepareHandoffMessages([
734
+ ...state.messages,
735
+ ]);
736
+ if (taskDescription) {
737
+ childMessages.push(new HumanMessage(taskDescription));
738
+ }
739
+ }
740
+ else {
741
+ // Default: isolated — only orchestrator's instructions
742
+ const fallbackTask = destContext?.description ?? 'Complete your assigned task.';
542
743
  childMessages = [
543
- ...childMessages,
544
- new HumanMessage(String(input[promptKey])),
744
+ new HumanMessage(taskDescription || fallbackTask),
545
745
  ];
546
746
  }
547
747
  const childState = {
548
748
  messages: childMessages,
549
749
  };
550
- console.debug(`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" START ` +
551
- `(messages: ${childMessages.length})`);
750
+ const childContext = this.agentContexts.get(destination);
751
+ const destName = destContext?.name ?? destination;
752
+ mlog(`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" SPAWN (async)\n` +
753
+ ` messages: ${childMessages.length}\n` +
754
+ ` childTools: ${childContext?.tools?.length ?? 0} instances\n` +
755
+ ` childToolDefs: ${childContext?.toolDefinitions?.length ?? 0} definitions`);
756
+ /**
757
+ * Dispatch transition BEFORE spawning the child subgraph so that
758
+ * callbacks.js sets multiAgentTrace.isMultiAgent = true before the
759
+ * child's ON_RUN_STEP events fire. spawnKey lets the UI create a
760
+ * distinct sidebar task for this specific invocation.
761
+ */
762
+ mlog(`[MultiAgentGraph] Handoff SPAWN "${sourceAgentId}" -> "${destination}" spawnKey=${spawnKey}`);
763
+ await safeDispatchCustomEvent(GraphEvents.ON_AGENT_TRANSITION, {
764
+ sourceAgentId: sourceAgentId,
765
+ sourceAgentName: this.agentContexts.get(sourceAgentId)?.name ?? sourceAgentId,
766
+ destinationAgentId: destination,
767
+ destinationAgentName: destName,
768
+ edgeType: EdgeType.HANDOFF,
769
+ timestamp: Date.now(),
770
+ spawnKey,
771
+ spawnPath: childSpawnPath,
772
+ parentSpawnPath: parentSpawnPath || null,
773
+ spawnDepth: childDepth,
774
+ }, config);
775
+ /**
776
+ * Child events need to carry spawnKey so callbacks.js can route
777
+ * them to the correct child aggregator. LangChain propagates
778
+ * `metadata` and `tags` from the parent config to all descendants,
779
+ * so everything dispatched inside subgraph.invoke will have
780
+ * metadata.spawnKey populated on the event's runtime metadata.
781
+ */
782
+ const childConfig = {
783
+ ...(config ?? {}),
784
+ metadata: {
785
+ ...(config?.metadata ?? {}),
786
+ spawnKey,
787
+ spawnAgentId: destination,
788
+ /** Hierarchical identity — see spawnPath.ts */
789
+ spawnPath: childSpawnPath,
790
+ parentSpawnPath: parentSpawnPath || null,
791
+ spawnDepth: childDepth,
792
+ },
793
+ tags: [
794
+ ...(config?.tags ?? []),
795
+ `spawn:${spawnKey}`,
796
+ `depth:${childDepth}`,
797
+ ],
798
+ };
799
+ /**
800
+ * Callback pattern: spawn child, WAIT for completion, return real
801
+ * result. The parent naturally sees child results in its tool
802
+ * return, so no manual "collect_results" step is needed.
803
+ *
804
+ * Parallelism still works: when the LLM emits multiple handoff
805
+ * tool calls in one response, LangGraph runs all tool functions
806
+ * concurrently. Each waits for its child. All results land in
807
+ * the LLM's next turn together.
808
+ */
809
+ const spawnedAt = Date.now();
552
810
  try {
553
- /**
554
- * Dispatch transition BEFORE invoking the child subgraph so that
555
- * callbacks.js sets multiAgentTrace.isMultiAgent = true before the
556
- * child's ON_RUN_STEP events fire. This ensures child tool calls
557
- * are attributed to the correct agent in admin traces.
558
- */
559
- await safeDispatchCustomEvent(GraphEvents.ON_AGENT_TRANSITION, {
560
- sourceAgentId: sourceAgentId,
561
- sourceAgentName: this.agentContexts.get(sourceAgentId)?.name ?? sourceAgentId,
562
- destinationAgentId: destination,
563
- destinationAgentName: destContext?.name ?? destination,
811
+ const result = await subgraph.invoke(childState, childConfig);
812
+ const durationMs = Date.now() - spawnedAt;
813
+ const resultText = MultiAgentGraph.extractHandoffResult(result.messages, destination);
814
+ const truncated = MultiAgentGraph.truncateHandoffResult(resultText, maxResultChars);
815
+ mlog(`[MultiAgentGraph] Handoff COMPLETED "${sourceAgentId}" -> "${destination}" ` +
816
+ `spawnKey=${spawnKey} (${durationMs}ms, ${truncated.length} chars)`);
817
+ /** Dispatch completion event for UI update — carries spawnKey so
818
+ * the frontend can mark the correct sidebar task as completed. */
819
+ safeDispatchCustomEvent(GraphEvents.ON_AGENT_TRANSITION, {
820
+ sourceAgentId: destination,
821
+ sourceAgentName: destName,
822
+ destinationAgentId: sourceAgentId,
823
+ destinationAgentName: this.agentContexts.get(sourceAgentId)?.name ??
824
+ sourceAgentId,
564
825
  edgeType: EdgeType.HANDOFF,
565
826
  timestamp: Date.now(),
566
- }, config);
567
- /**
568
- * Invoke the child subgraph with config propagation.
569
- * Config carries callbacks (for SSE streaming), abort signal,
570
- * and configurable data (thread_id, user_id) to the child.
571
- */
572
- const result = await subgraph.invoke(childState, config);
573
- const resultText = MultiAgentGraph.extractHandoffResult(result.messages, destination);
574
- const truncatedResult = MultiAgentGraph.truncateHandoffResult(resultText, maxResultChars);
575
- console.debug(`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" DONE ` +
576
- `(result: ${resultText.length} chars` +
577
- `${truncatedResult.length < resultText.length ? `, truncated to ${truncatedResult.length}` : ''})`);
578
- return truncatedResult;
827
+ isCompletion: true,
828
+ durationMs,
829
+ resultLength: truncated.length,
830
+ spawnKey,
831
+ spawnPath: childSpawnPath,
832
+ parentSpawnPath: parentSpawnPath || null,
833
+ spawnDepth: childDepth,
834
+ }, config).catch(() => {
835
+ /* best-effort event dispatch */
836
+ });
837
+ return truncated;
579
838
  }
580
839
  catch (err) {
581
- const errorMessage = err instanceof Error ? err.message : String(err);
582
- console.error(`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" ERROR:`, errorMessage);
583
- return `[Handoff to "${destination}" failed: ${errorMessage}]`;
840
+ const durationMs = Date.now() - spawnedAt;
841
+ const errMsg = err instanceof Error ? err.message : String(err);
842
+ // EPIPE from console.debug is non-fatal
843
+ if (errMsg.includes('EPIPE')) {
844
+ mwarn(`[MultiAgentGraph] Child "${destination}" hit EPIPE (non-fatal) spawnKey=${spawnKey}`);
845
+ safeDispatchCustomEvent(GraphEvents.ON_AGENT_TRANSITION, {
846
+ sourceAgentId: destination,
847
+ sourceAgentName: destName,
848
+ destinationAgentId: sourceAgentId,
849
+ destinationAgentName: this.agentContexts.get(sourceAgentId)?.name ??
850
+ sourceAgentId,
851
+ edgeType: EdgeType.HANDOFF,
852
+ timestamp: Date.now(),
853
+ isCompletion: true,
854
+ durationMs,
855
+ spawnKey,
856
+ spawnPath: childSpawnPath,
857
+ parentSpawnPath: parentSpawnPath || null,
858
+ spawnDepth: childDepth,
859
+ }, config).catch(() => {
860
+ /* best-effort */
861
+ });
862
+ return `Agent "${destName}" completed but output was lost due to stream closure.`;
863
+ }
864
+ console.error(`[MultiAgentGraph] Handoff FAILED "${sourceAgentId}" -> "${destination}" ` +
865
+ `spawnKey=${spawnKey} (${durationMs}ms): ${errMsg}`);
866
+ safeDispatchCustomEvent(GraphEvents.ON_AGENT_TRANSITION, {
867
+ sourceAgentId: destination,
868
+ sourceAgentName: destName,
869
+ destinationAgentId: sourceAgentId,
870
+ destinationAgentName: this.agentContexts.get(sourceAgentId)?.name ??
871
+ sourceAgentId,
872
+ edgeType: EdgeType.HANDOFF,
873
+ timestamp: Date.now(),
874
+ isCompletion: true,
875
+ durationMs,
876
+ spawnKey,
877
+ spawnPath: childSpawnPath,
878
+ parentSpawnPath: parentSpawnPath || null,
879
+ spawnDepth: childDepth,
880
+ error: errMsg,
881
+ }, config).catch(() => {
882
+ /* best-effort */
883
+ });
884
+ return `Agent "${destName}" failed after ${durationMs}ms: ${errMsg}`;
584
885
  }
585
886
  }, {
586
887
  name: toolName,
587
- schema: hasPromptInput
588
- ? {
589
- type: 'object',
590
- properties: {
591
- [promptKey]: {
592
- type: 'string',
593
- description: promptInputDescription,
594
- },
888
+ schema: {
889
+ type: 'object',
890
+ properties: {
891
+ [promptKey]: {
892
+ type: 'string',
893
+ description: promptInputDescription,
595
894
  },
596
- required: [],
597
- }
598
- : { type: 'object', properties: {}, required: [] },
895
+ },
896
+ required: [promptKey],
897
+ },
599
898
  description: toolDescription,
600
899
  }));
601
900
  }
@@ -637,7 +936,7 @@ class MultiAgentGraph extends StandardGraph {
637
936
  /**
638
937
  * Truncate handoff result using head/tail strategy (60/40 split).
639
938
  * Preserves the beginning (key findings) and end (conclusions).
640
- * Matches the TaskTool.truncateResult pattern from Ranger.
939
+ * Matches the TaskTool.truncateResult pattern used by host orchestrators.
641
940
  * @param result - The full result text
642
941
  * @param maxChars - Maximum allowed characters
643
942
  */
@@ -673,8 +972,140 @@ class MultiAgentGraph extends StandardGraph {
673
972
  * Create a complete agent subgraph (similar to createReactAgent)
674
973
  */
675
974
  createAgentSubgraph(agentId) {
676
- /** This is essentially the same as `createAgentNode` from `StandardGraph` */
677
- return this.createAgentNode(agentId);
975
+ /**
976
+ * Scoped subgraph build for handoff targets.
977
+ *
978
+ * If the handoff target has outgoing sequence/transfer edges (e.g. a
979
+ * "researcher" agent with its own sequence `[researcher → prod_assistant]`),
980
+ * we compile a mini-StateGraph containing the agent and all agents reachable
981
+ * from it via non-handoff edges. This way, when the parent hands off to
982
+ * researcher via `subgraph.invoke()`, the nested sequence runs to completion
983
+ * before the result is returned to the parent.
984
+ *
985
+ * Fast path: if no downstream agents are reachable, fall back to the
986
+ * previous single-node behavior (`createAgentNode`).
987
+ *
988
+ * See docs/multi-agent-nesting-architecture.md §6.
989
+ */
990
+ const reachable = this.computeReachableViaNonHandoff(agentId);
991
+ if (reachable.size === 1) {
992
+ return this.createAgentNode(agentId);
993
+ }
994
+ mlog(`[MultiAgentGraph] Scoped subgraph for "${agentId}": ${reachable.size} nodes [${Array.from(reachable).join(', ')}]`);
995
+ return this.buildScopedSubgraph(agentId, reachable);
996
+ }
997
+ /**
998
+ * BFS from `rootAgentId` across sequence + transfer edges (NOT handoff edges).
999
+ * Returns the set of agents reachable in this agent's "local workflow".
1000
+ */
1001
+ computeReachableViaNonHandoff(rootAgentId) {
1002
+ const reachable = new Set([rootAgentId]);
1003
+ const queue = [rootAgentId];
1004
+ const localEdges = [...this.sequenceEdges, ...this.transferEdges];
1005
+ while (queue.length > 0) {
1006
+ const current = queue.shift();
1007
+ for (const edge of localEdges) {
1008
+ const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
1009
+ if (!sources.includes(current))
1010
+ continue;
1011
+ const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
1012
+ for (const dest of dests) {
1013
+ if (!reachable.has(dest) && this.agentContexts.has(dest)) {
1014
+ reachable.add(dest);
1015
+ queue.push(dest);
1016
+ }
1017
+ }
1018
+ }
1019
+ }
1020
+ return reachable;
1021
+ }
1022
+ /**
1023
+ * Build a compiled scoped StateGraph containing `agentIds` as nodes, rooted
1024
+ * at `rootAgentId`. Linear sequence edges where both endpoints are in scope
1025
+ * are wired directly; nodes with no outgoing in-scope edges route to END.
1026
+ *
1027
+ * Each node is wrapped around the per-agent `createAgentNode` compiled
1028
+ * workflow (agent + tools loop) to preserve isolated tool context.
1029
+ */
1030
+ buildScopedSubgraph(rootAgentId, agentIds) {
1031
+ const StateAnnotation = Annotation.Root({
1032
+ messages: Annotation({
1033
+ reducer: messagesStateReducer,
1034
+ default: () => [],
1035
+ }),
1036
+ });
1037
+ const builder = new StateGraph(StateAnnotation);
1038
+ // Precompile each scoped agent's inner workflow and wrap as a node.
1039
+ //
1040
+ // Two different isolation strategies depending on position:
1041
+ //
1042
+ // • ROOT node (the handoff target itself): receives the parent
1043
+ // orchestrator's handoff frame. Use `prepareHandoffMessages` — drops
1044
+ // orphaned tool_use, compacts paired tool calls, guarantees trailing
1045
+ // HumanMessage for Bedrock/VertexAI compatibility. The root needs
1046
+ // orchestrator context because it's responding to the handoff.
1047
+ //
1048
+ // • DOWNSTREAM nodes (sequence targets of the root): run as FULLY
1049
+ // ISOLATED child sessions. They receive only:
1050
+ // [original user request, synthetic HumanMessage describing what
1051
+ // the upstream agent produced and asking them to act]
1052
+ // No raw tool_use / tool_result blocks from the upstream agent —
1053
+ // prevents schema confusion when a downstream agent sees noisy
1054
+ // upstream context and produces malformed tool_use JSON.
1055
+ //
1056
+ // Each wrapper returns only the DELTA (new messages produced by the
1057
+ // inner invoke), not the prepared input — otherwise messagesStateReducer
1058
+ // would double-append the synthetic instruction into the scoped state.
1059
+ for (const aid of agentIds) {
1060
+ const inner = this.createAgentNode(aid);
1061
+ const isRoot = aid === rootAgentId;
1062
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1063
+ builder.addNode(aid, async (state, config) => {
1064
+ const prepared = isRoot
1065
+ ? MultiAgentGraph.prepareHandoffMessages(state.messages)
1066
+ : MultiAgentGraph.prepareIsolatedChildMessages(state.messages);
1067
+ mlog(`[MultiAgentGraph] scoped node "${aid}" entering (isRoot=${isRoot}, stateMessages=${state.messages.length}, prepared=${prepared.length})`);
1068
+ const result = await inner.invoke({ ...state, messages: prepared }, config);
1069
+ // Return only the messages the inner node appended beyond its input,
1070
+ // so messagesStateReducer doesn't duplicate the synthetic wrapper
1071
+ // prompt into the scoped state.
1072
+ const delta = result.messages.length > prepared.length
1073
+ ? result.messages.slice(prepared.length)
1074
+ : result.messages;
1075
+ return { messages: delta };
1076
+ });
1077
+ }
1078
+ // START → root
1079
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1080
+ // @ts-ignore — LangGraph string typing is too strict for dynamic agent ids
1081
+ builder.addEdge(START, rootAgentId);
1082
+ // Wire sequence edges in scope (linear chain support)
1083
+ const hasOutgoing = new Set();
1084
+ for (const edge of this.sequenceEdges) {
1085
+ const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
1086
+ const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
1087
+ for (const source of sources) {
1088
+ if (!agentIds.has(source))
1089
+ continue;
1090
+ for (const dest of dests) {
1091
+ if (!agentIds.has(dest))
1092
+ continue;
1093
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1094
+ // @ts-ignore
1095
+ builder.addEdge(source, dest);
1096
+ hasOutgoing.add(source);
1097
+ }
1098
+ }
1099
+ }
1100
+ // Leaves (no outgoing in-scope edges) route to END
1101
+ for (const aid of agentIds) {
1102
+ if (!hasOutgoing.has(aid)) {
1103
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1104
+ // @ts-ignore
1105
+ builder.addEdge(aid, END);
1106
+ }
1107
+ }
1108
+ return builder.compile(this.compileOptions);
678
1109
  }
679
1110
  /**
680
1111
  * Detects if the current agent is receiving a handoff and processes the messages accordingly.
@@ -689,108 +1120,20 @@ class MultiAgentGraph extends StandardGraph {
689
1120
  * @returns Object with filtered messages, extracted instructions, source agent, and parallel siblings
690
1121
  */
691
1122
  /**
692
- * Prepare messages for a handoff child agent.
693
- *
694
- * Handles two problems:
695
- * 1. **Orphaned tool_use**: The parent's AI message contains a `tool_use` block
696
- * for the handoff tool itself, with no matching `tool_result`. Providers
697
- * (Bedrock/Anthropic) reject this.
698
- * 2. **Paired tool_use/tool_result in history**: The child may not have the same
699
- * tools as the parent. Bedrock requires `toolConfig` when tool_use/tool_result
700
- * blocks exist in the message history. Compacting these into text summaries
701
- * avoids the requirement and reduces context bloat.
702
- *
703
- * Strategy:
704
- * - Remove orphaned tool_use blocks (no matching tool_result)
705
- * - Compact paired tool_use/tool_result interactions into text summaries
1123
+ * Prepare messages for a handoff child agent. See
1124
+ * {@link prepareHandoffMessagesUtil} for the full implementation and
1125
+ * semantics this static method is a thin delegate preserved for
1126
+ * backward compatibility with existing call sites and unit tests.
706
1127
  */
707
1128
  static prepareHandoffMessages(messages) {
708
- if (messages.length === 0)
709
- return messages;
710
- /** Collect all tool_result IDs so we know which tool_use blocks are paired */
711
- const pairedToolCallIds = new Set();
712
- for (const msg of messages) {
713
- if (msg.getType() === 'tool') {
714
- const tm = msg;
715
- if (tm.tool_call_id) {
716
- pairedToolCallIds.add(tm.tool_call_id);
717
- }
718
- }
719
- }
720
- /**
721
- * Pass 1: Remove orphaned tool_use blocks (no matching tool_result).
722
- * Also skip ToolMessages since we'll compact paired ones in pass 2.
723
- */
724
- const cleaned = [];
725
- for (const msg of messages) {
726
- /** Skip all ToolMessages — paired ones will be compacted in pass 2 */
727
- if (msg.getType() === 'tool') {
728
- continue;
729
- }
730
- if (msg.getType() !== 'ai') {
731
- cleaned.push(msg);
732
- continue;
733
- }
734
- const aiMsg = msg;
735
- const toolCalls = aiMsg.tool_calls ?? [];
736
- if (toolCalls.length === 0) {
737
- cleaned.push(msg);
738
- continue;
739
- }
740
- /** Extract text content from the AI message */
741
- const textContent = typeof aiMsg.content === 'string'
742
- ? aiMsg.content
743
- : Array.isArray(aiMsg.content)
744
- ? aiMsg.content
745
- .filter((b) => b.type === 'text' && 'text' in b)
746
- .map((b) => b.text ?? '')
747
- .join('\n')
748
- : '';
749
- /** Build text summaries of paired tool calls */
750
- const toolSummaries = [];
751
- for (const tc of toolCalls) {
752
- if (tc.id != null && pairedToolCallIds.has(tc.id)) {
753
- /** Find the matching ToolMessage result */
754
- const toolResult = messages.find((m) => m.getType() === 'tool' && m.tool_call_id === tc.id);
755
- const resultContent = toolResult
756
- ? typeof toolResult.content === 'string'
757
- ? toolResult.content.slice(0, 500)
758
- : '[complex result]'
759
- : '[no result]';
760
- toolSummaries.push(`[Tool "${tc.name}": ${resultContent}]`);
761
- }
762
- // Orphaned tool_use blocks (no matching result) are silently dropped
763
- }
764
- /** Reconstruct as plain text AI message (no tool_calls) */
765
- const parts = [textContent, ...toolSummaries].filter(Boolean);
766
- if (parts.length > 0) {
767
- cleaned.push(new AIMessage({
768
- content: parts.join('\n\n'),
769
- id: aiMsg.id,
770
- }));
771
- }
772
- }
773
- /**
774
- * Ensure messages end with a HumanMessage.
775
- * After stripping tool artifacts, the last message may be an AIMessage
776
- * (orchestrator's reasoning before the handoff). Some providers (Bedrock,
777
- * Google/VertexAI) reject conversations ending with an assistant message.
778
- * Convert the trailing AIMessage to a HumanMessage to preserve any useful
779
- * context (e.g., compacted tool summaries) while satisfying the API requirement.
780
- */
781
- if (cleaned.length > 0 && cleaned[cleaned.length - 1].getType() === 'ai') {
782
- const lastAI = cleaned[cleaned.length - 1];
783
- const content = typeof lastAI.content === 'string'
784
- ? lastAI.content
785
- : '';
786
- if (content.trim()) {
787
- cleaned[cleaned.length - 1] = new HumanMessage(`[Context from orchestrator]: ${content}`);
788
- }
789
- else {
790
- cleaned.pop();
791
- }
792
- }
793
- return cleaned;
1129
+ return prepareHandoffMessages(messages);
1130
+ }
1131
+ /**
1132
+ * Build an isolated message context for a downstream scoped-subgraph
1133
+ * node. See {@link prepareIsolatedChildMessagesUtil} for details.
1134
+ */
1135
+ static prepareIsolatedChildMessages(messages) {
1136
+ return prepareIsolatedChildMessages(messages);
794
1137
  }
795
1138
  processTransferReception(messages, agentId) {
796
1139
  if (messages.length === 0)
@@ -822,7 +1165,8 @@ class MultiAgentGraph extends StandardGraph {
822
1165
  }
823
1166
  else if (isConditionalTransfer) {
824
1167
  const transferDest = candidateMsg.additional_kwargs.handoff_destination;
825
- destinationAgent = typeof transferDest === 'string' ? transferDest : null;
1168
+ destinationAgent =
1169
+ typeof transferDest === 'string' ? transferDest : null;
826
1170
  }
827
1171
  /** Check if this transfer targets our agent */
828
1172
  if (destinationAgent === agentId) {
@@ -1063,8 +1407,35 @@ class MultiAgentGraph extends StandardGraph {
1063
1407
  for (const startNode of this.startingNodes) {
1064
1408
  handoffOnlyDestinations.delete(startNode);
1065
1409
  }
1410
+ /**
1411
+ * Nested-sequence expansion: for each handoff-only target, its downstream
1412
+ * sequence/transfer agents MUST also become handoff-only — they exist only
1413
+ * inside the target's scoped subgraph, not at top level. Without this,
1414
+ * those downstream nodes would be added as top-level orphans and LangGraph
1415
+ * would fail compilation (UNREACHABLE_NODE).
1416
+ *
1417
+ * See docs/multi-agent-nesting-architecture.md §6.
1418
+ */
1419
+ const nestedHandoffOnly = new Set();
1420
+ for (const target of handoffOnlyDestinations) {
1421
+ const reachable = this.computeReachableViaNonHandoff(target);
1422
+ for (const agent of reachable) {
1423
+ if (agent === target)
1424
+ continue;
1425
+ // Skip if this agent is legitimately a top-level starting node
1426
+ if (this.startingNodes.has(agent))
1427
+ continue;
1428
+ nestedHandoffOnly.add(agent);
1429
+ }
1430
+ }
1431
+ for (const agent of nestedHandoffOnly) {
1432
+ handoffOnlyDestinations.add(agent);
1433
+ }
1434
+ if (nestedHandoffOnly.size > 0) {
1435
+ mlog(`[MultiAgentGraph] Nested handoff-only (scoped subgraph downstream): [${Array.from(nestedHandoffOnly).join(', ')}]`);
1436
+ }
1066
1437
  if (handoffOnlyDestinations.size > 0) {
1067
- console.debug(`[MultiAgentGraph] Handoff-only children (subgraph only, no top-level node): [${Array.from(handoffOnlyDestinations).join(', ')}]`);
1438
+ mlog(`[MultiAgentGraph] Handoff-only children (subgraph only, no top-level node): [${Array.from(handoffOnlyDestinations).join(', ')}]`);
1068
1439
  }
1069
1440
  // Add agents as nodes — skip handoff-only children (they exist as subgraphs only)
1070
1441
  for (const [agentId] of this.agentContexts) {
@@ -1113,7 +1484,7 @@ class MultiAgentGraph extends StandardGraph {
1113
1484
  }
1114
1485
  /** Wrapper function that handles agentMessages channel, handoff reception, and conditional routing */
1115
1486
  const agentWrapper = async (state, config) => {
1116
- console.debug(`[MultiAgentGraph] Agent "${agentId}" wrapper ENTRY (messages: ${state.messages.length}, needsCommandRouting: ${needsCommandRouting})`);
1487
+ mlog(`[MultiAgentGraph] Agent "${agentId}" wrapper ENTRY (messages: ${state.messages.length}, needsCommandRouting: ${needsCommandRouting})`);
1117
1488
  let result;
1118
1489
  /**
1119
1490
  * Check if this agent is receiving a transfer.
@@ -1124,7 +1495,7 @@ class MultiAgentGraph extends StandardGraph {
1124
1495
  const transferContext = this.processTransferReception(state.messages, agentId);
1125
1496
  if (transferContext !== null) {
1126
1497
  const { filteredMessages, instructions, sourceAgentName, parallelSiblings, } = transferContext;
1127
- console.debug(`[MultiAgentGraph] Agent "${agentId}" receiving transfer from "${sourceAgentName}" (instructions: ${instructions != null}, parallelSiblings: ${parallelSiblings.length})`);
1498
+ mlog(`[MultiAgentGraph] Agent "${agentId}" receiving transfer from "${sourceAgentName}" (instructions: ${instructions != null}, parallelSiblings: ${parallelSiblings.length})`);
1128
1499
  /**
1129
1500
  * Set handoff context on the receiving agent.
1130
1501
  * Uses pre-computed graph position for depth and parallel info.
@@ -1240,7 +1611,7 @@ class MultiAgentGraph extends StandardGraph {
1240
1611
  }
1241
1612
  /** Track the last agent that produced output for continuation support */
1242
1613
  this.lastActiveAgentId = agentId;
1243
- console.debug(`[MultiAgentGraph] Agent "${agentId}" wrapper EXIT (result messages: ${result.messages.length})`);
1614
+ mlog(`[MultiAgentGraph] Agent "${agentId}" wrapper EXIT (result messages: ${result.messages.length})`);
1244
1615
  /** If agent has both transfer and sequence edges, use Command for exclusive routing */
1245
1616
  if (needsCommandRouting) {
1246
1617
  /** Check if a transfer occurred */
@@ -1251,7 +1622,7 @@ class MultiAgentGraph extends StandardGraph {
1251
1622
  lastMessage.name.startsWith(Constants.LC_TRANSFER_TO_)) {
1252
1623
  /** Transfer occurred - extract destination and navigate there exclusively */
1253
1624
  const transferDest = lastMessage.name.replace(Constants.LC_TRANSFER_TO_, '');
1254
- console.debug(`[MultiAgentGraph] Command routing: "${agentId}" -> transfer to "${transferDest}" (sequence edges skipped: [${Array.from(sequenceDestinations).join(', ')}])`);
1625
+ mlog(`[MultiAgentGraph] Command routing: "${agentId}" -> transfer to "${transferDest}" (sequence edges skipped: [${Array.from(sequenceDestinations).join(', ')}])`);
1255
1626
  /** Validate destination agent exists */
1256
1627
  if (!this.agentContexts.has(transferDest)) {
1257
1628
  const availableAgents = Array.from(this.agentContexts.keys()).join(', ');
@@ -1279,7 +1650,7 @@ class MultiAgentGraph extends StandardGraph {
1279
1650
  }
1280
1651
  const receiverBudget = receiverContext.maxContextTokens;
1281
1652
  if (currentSize > receiverBudget * 0.7) {
1282
- console.warn(`[MultiAgentGraph] Pre-handoff compaction: context (${currentSize} tokens) exceeds ` +
1653
+ mwarn(`[MultiAgentGraph] Pre-handoff compaction: context (${currentSize} tokens) exceeds ` +
1283
1654
  `70% of receiver "${transferDest}" budget (${receiverBudget} tokens)`);
1284
1655
  /** Generate handoff briefing */
1285
1656
  const senderName = senderContext.name ?? agentId;
@@ -1344,7 +1715,7 @@ class MultiAgentGraph extends StandardGraph {
1344
1715
  }
1345
1716
  else {
1346
1717
  /** No transfer - proceed with sequence edges */
1347
- console.debug(`[MultiAgentGraph] Command routing: "${agentId}" -> no transfer, following sequence edges: [${Array.from(sequenceDestinations).join(', ')}]`);
1718
+ mlog(`[MultiAgentGraph] Command routing: "${agentId}" -> no transfer, following sequence edges: [${Array.from(sequenceDestinations).join(', ')}]`);
1348
1719
  const directDests = Array.from(sequenceDestinations);
1349
1720
  for (const dest of directDests) {
1350
1721
  await safeDispatchCustomEvent(GraphEvents.ON_AGENT_TRANSITION, {
@@ -1371,7 +1742,30 @@ class MultiAgentGraph extends StandardGraph {
1371
1742
  }
1372
1743
  }
1373
1744
  }
1374
- /** No special routing needed - return state normally */
1745
+ /**
1746
+ * No Command routing needed — dispatch ON_AGENT_TRANSITION for all
1747
+ * destinations so callbacks.js can register child agents for event
1748
+ * isolation BEFORE they start streaming.
1749
+ */
1750
+ const allDests = new Set([
1751
+ ...transferDestinations,
1752
+ ...sequenceDestinations,
1753
+ ]);
1754
+ if (allDests.size > 0) {
1755
+ const edgeType = hasTransferEdges
1756
+ ? EdgeType.TRANSFER
1757
+ : EdgeType.SEQUENCE;
1758
+ for (const dest of allDests) {
1759
+ await safeDispatchCustomEvent(GraphEvents.ON_AGENT_TRANSITION, {
1760
+ sourceAgentId: agentId,
1761
+ sourceAgentName: this.agentContexts.get(agentId)?.name ?? agentId,
1762
+ destinationAgentId: dest,
1763
+ destinationAgentName: this.agentContexts.get(dest)?.name ?? dest,
1764
+ edgeType,
1765
+ timestamp: Date.now(),
1766
+ }, config);
1767
+ }
1768
+ }
1375
1769
  return result;
1376
1770
  };
1377
1771
  /** Wrapped agent as a node with its possible destinations */
@@ -1393,16 +1787,13 @@ class MultiAgentGraph extends StandardGraph {
1393
1787
  this.agentContexts.has(this.resumeFromAgentId);
1394
1788
  if (validResumeAgent) {
1395
1789
  const resumeAgentId = this.resumeFromAgentId;
1396
- console.debug(`[MultiAgentGraph] Multi-turn resumption: routing START → "${resumeAgentId}" (skipping default starting nodes: [${Array.from(this.startingNodes).join(', ')}])`);
1790
+ mlog(`[MultiAgentGraph] Multi-turn resumption: routing START → "${resumeAgentId}" (skipping default starting nodes: [${Array.from(this.startingNodes).join(', ')}])`);
1397
1791
  /**
1398
1792
  * Build route map containing both the resume agent and default starting
1399
1793
  * nodes. This is required by LangGraph — all possible destinations must
1400
1794
  * be declared even if the router always picks one.
1401
1795
  */
1402
- const allPossibleStarts = new Set([
1403
- ...this.startingNodes,
1404
- resumeAgentId,
1405
- ]);
1796
+ const allPossibleStarts = new Set([...this.startingNodes, resumeAgentId]);
1406
1797
  const routeMap = {};
1407
1798
  for (const nodeId of allPossibleStarts) {
1408
1799
  routeMap[nodeId] = nodeId;
@@ -1411,7 +1802,7 @@ class MultiAgentGraph extends StandardGraph {
1411
1802
  }
1412
1803
  else {
1413
1804
  if (this.resumeFromAgentId != null) {
1414
- console.warn(`[MultiAgentGraph] resumeFromAgentId "${this.resumeFromAgentId}" not found in graph — falling back to default starting nodes`);
1805
+ mwarn(`[MultiAgentGraph] resumeFromAgentId "${this.resumeFromAgentId}" not found in graph — falling back to default starting nodes`);
1415
1806
  }
1416
1807
  for (const startNode of this.startingNodes) {
1417
1808
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -1475,8 +1866,19 @@ class MultiAgentGraph extends StandardGraph {
1475
1866
  if (gatedEdges.has(edge)) {
1476
1867
  continue;
1477
1868
  }
1478
- const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
1479
- for (const destination of destinations) {
1869
+ /**
1870
+ * Skip sequence edges where either endpoint lives only inside a scoped
1871
+ * handoff subgraph. Those edges are wired inside `buildScopedSubgraph`,
1872
+ * not at the top level — adding them here would reference non-existent
1873
+ * top-level nodes and fail compilation.
1874
+ */
1875
+ const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
1876
+ const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
1877
+ const anyEndpointHandoffOnly = [...sources, ...dests].some((n) => handoffOnlyDestinations.has(n));
1878
+ if (anyEndpointHandoffOnly) {
1879
+ continue;
1880
+ }
1881
+ for (const destination of dests) {
1480
1882
  if (!edgesByDestination.has(destination)) {
1481
1883
  edgesByDestination.set(destination, []);
1482
1884
  }
@@ -1573,7 +1975,8 @@ class MultiAgentGraph extends StandardGraph {
1573
1975
  return eSources.includes(source);
1574
1976
  });
1575
1977
  /** Skip adding edge if source uses Command routing (has both types) */
1576
- if (sourceTransferEdges.length > 0 && sourceSequenceEdges.length > 0) {
1978
+ if (sourceTransferEdges.length > 0 &&
1979
+ sourceSequenceEdges.length > 0) {
1577
1980
  continue;
1578
1981
  }
1579
1982
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment