@illuma-ai/agents 1.1.28 → 1.3.1

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.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 +89 -45
  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 +117 -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/errors.cjs +113 -0
  69. package/dist/cjs/utils/errors.cjs.map +1 -0
  70. package/dist/cjs/utils/events.cjs +36 -7
  71. package/dist/cjs/utils/events.cjs.map +1 -1
  72. package/dist/cjs/utils/finishReasons.cjs +44 -0
  73. package/dist/cjs/utils/finishReasons.cjs.map +1 -0
  74. package/dist/cjs/utils/llm.cjs.map +1 -1
  75. package/dist/cjs/utils/logging.cjs +34 -0
  76. package/dist/cjs/utils/logging.cjs.map +1 -0
  77. package/dist/cjs/utils/toolCallNormalization.cjs +250 -0
  78. package/dist/cjs/utils/toolCallNormalization.cjs.map +1 -0
  79. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  80. package/dist/esm/common/spawnPath.mjs +95 -0
  81. package/dist/esm/common/spawnPath.mjs.map +1 -0
  82. package/dist/esm/graphs/Graph.mjs +89 -45
  83. package/dist/esm/graphs/Graph.mjs.map +1 -1
  84. package/dist/esm/graphs/HandoffRegistry.mjs +47 -8
  85. package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -1
  86. package/dist/esm/graphs/MultiAgentGraph.mjs +493 -267
  87. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  88. package/dist/esm/graphs/phases/flushLoop.mjs +209 -0
  89. package/dist/esm/graphs/phases/flushLoop.mjs.map +1 -0
  90. package/dist/esm/graphs/phases/memoryFlushPhase.mjs +99 -0
  91. package/dist/esm/graphs/phases/memoryFlushPhase.mjs.map +1 -0
  92. package/dist/esm/llm/bedrock/index.mjs +4 -3
  93. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  94. package/dist/esm/main.mjs +21 -0
  95. package/dist/esm/main.mjs.map +1 -1
  96. package/dist/esm/memory/citations.mjs +64 -0
  97. package/dist/esm/memory/citations.mjs.map +1 -0
  98. package/dist/esm/memory/compositeBackend.mjs +58 -0
  99. package/dist/esm/memory/compositeBackend.mjs.map +1 -0
  100. package/dist/esm/memory/constants.mjs +198 -0
  101. package/dist/esm/memory/constants.mjs.map +1 -0
  102. package/dist/esm/memory/embeddings.mjs +148 -0
  103. package/dist/esm/memory/embeddings.mjs.map +1 -0
  104. package/dist/esm/memory/factory.mjs +93 -0
  105. package/dist/esm/memory/factory.mjs.map +1 -0
  106. package/dist/esm/memory/migrate.mjs +78 -0
  107. package/dist/esm/memory/migrate.mjs.map +1 -0
  108. package/dist/esm/memory/mmr.mjs +130 -0
  109. package/dist/esm/memory/mmr.mjs.map +1 -0
  110. package/dist/esm/memory/paths.mjs +207 -0
  111. package/dist/esm/memory/paths.mjs.map +1 -0
  112. package/dist/esm/memory/pgvectorStore.mjs +223 -0
  113. package/dist/esm/memory/pgvectorStore.mjs.map +1 -0
  114. package/dist/esm/memory/recallTracking.mjs +94 -0
  115. package/dist/esm/memory/recallTracking.mjs.map +1 -0
  116. package/dist/esm/memory/schema.sql +51 -0
  117. package/dist/esm/memory/temporalDecay.mjs +110 -0
  118. package/dist/esm/memory/temporalDecay.mjs.map +1 -0
  119. package/dist/esm/nodes/ApprovalGateNode.mjs +1 -1
  120. package/dist/esm/nodes/ApprovalGateNode.mjs.map +1 -1
  121. package/dist/esm/prompts/memoryFlushPrompt.mjs +44 -0
  122. package/dist/esm/prompts/memoryFlushPrompt.mjs.map +1 -0
  123. package/dist/esm/run.mjs +16 -3
  124. package/dist/esm/run.mjs.map +1 -1
  125. package/dist/esm/tools/AskUser.mjs +6 -1
  126. package/dist/esm/tools/AskUser.mjs.map +1 -1
  127. package/dist/esm/tools/BrowserTools.mjs +1 -1
  128. package/dist/esm/tools/BrowserTools.mjs.map +1 -1
  129. package/dist/esm/tools/ToolNode.mjs +128 -11
  130. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  131. package/dist/esm/tools/approval/constants.mjs +2 -2
  132. package/dist/esm/tools/approval/constants.mjs.map +1 -1
  133. package/dist/esm/tools/memory/index.mjs +46 -0
  134. package/dist/esm/tools/memory/index.mjs.map +1 -0
  135. package/dist/esm/tools/memory/memoryAppendTool.mjs +67 -0
  136. package/dist/esm/tools/memory/memoryAppendTool.mjs.map +1 -0
  137. package/dist/esm/tools/memory/memoryGetTool.mjs +47 -0
  138. package/dist/esm/tools/memory/memoryGetTool.mjs.map +1 -0
  139. package/dist/esm/tools/memory/memorySearchTool.mjs +63 -0
  140. package/dist/esm/tools/memory/memorySearchTool.mjs.map +1 -0
  141. package/dist/esm/tools/memory/shared.mjs +98 -0
  142. package/dist/esm/tools/memory/shared.mjs.map +1 -0
  143. package/dist/esm/types/graph.mjs.map +1 -1
  144. package/dist/esm/utils/childAgentContext.mjs +237 -0
  145. package/dist/esm/utils/childAgentContext.mjs.map +1 -0
  146. package/dist/esm/utils/errors.mjs +109 -0
  147. package/dist/esm/utils/errors.mjs.map +1 -0
  148. package/dist/esm/utils/events.mjs +36 -8
  149. package/dist/esm/utils/events.mjs.map +1 -1
  150. package/dist/esm/utils/finishReasons.mjs +41 -0
  151. package/dist/esm/utils/finishReasons.mjs.map +1 -0
  152. package/dist/esm/utils/llm.mjs.map +1 -1
  153. package/dist/esm/utils/logging.mjs +31 -0
  154. package/dist/esm/utils/logging.mjs.map +1 -0
  155. package/dist/esm/utils/toolCallNormalization.mjs +247 -0
  156. package/dist/esm/utils/toolCallNormalization.mjs.map +1 -0
  157. package/dist/types/common/index.d.ts +1 -0
  158. package/dist/types/common/spawnPath.d.ts +59 -0
  159. package/dist/types/graphs/HandoffRegistry.d.ts +24 -7
  160. package/dist/types/graphs/MultiAgentGraph.d.ts +43 -23
  161. package/dist/types/graphs/phases/flushLoop.d.ts +106 -0
  162. package/dist/types/graphs/phases/memoryFlushPhase.d.ts +100 -0
  163. package/dist/types/index.d.ts +7 -0
  164. package/dist/types/memory/__tests__/mockBackend.d.ts +40 -0
  165. package/dist/types/memory/citations.d.ts +39 -0
  166. package/dist/types/memory/compositeBackend.d.ts +30 -0
  167. package/dist/types/memory/constants.d.ts +121 -0
  168. package/dist/types/memory/embeddings.d.ts +15 -0
  169. package/dist/types/memory/factory.d.ts +23 -0
  170. package/dist/types/memory/index.d.ts +21 -0
  171. package/dist/types/memory/migrate.d.ts +14 -0
  172. package/dist/types/memory/mmr.d.ts +50 -0
  173. package/dist/types/memory/paths.d.ts +107 -0
  174. package/dist/types/memory/pgvectorStore.d.ts +56 -0
  175. package/dist/types/memory/recallTracking.d.ts +30 -0
  176. package/dist/types/memory/temporalDecay.d.ts +53 -0
  177. package/dist/types/memory/types.d.ts +182 -0
  178. package/dist/types/prompts/memoryFlushPrompt.d.ts +54 -0
  179. package/dist/types/run.d.ts +1 -0
  180. package/dist/types/tools/AskUser.d.ts +1 -1
  181. package/dist/types/tools/BrowserTools.d.ts +2 -2
  182. package/dist/types/tools/approval/constants.d.ts +2 -2
  183. package/dist/types/tools/memory/index.d.ts +39 -0
  184. package/dist/types/tools/memory/memoryAppendTool.d.ts +27 -0
  185. package/dist/types/tools/memory/memoryGetTool.d.ts +22 -0
  186. package/dist/types/tools/memory/memorySearchTool.d.ts +22 -0
  187. package/dist/types/tools/memory/shared.d.ts +106 -0
  188. package/dist/types/types/graph.d.ts +10 -3
  189. package/dist/types/utils/childAgentContext.d.ts +99 -0
  190. package/dist/types/utils/errors.d.ts +37 -0
  191. package/dist/types/utils/events.d.ts +21 -0
  192. package/dist/types/utils/finishReasons.d.ts +32 -0
  193. package/dist/types/utils/index.d.ts +1 -0
  194. package/dist/types/utils/logging.d.ts +2 -0
  195. package/dist/types/utils/toolCallNormalization.d.ts +44 -0
  196. package/package.json +6 -4
  197. package/src/agents/AgentContext.ts +12 -4
  198. package/src/common/__tests__/enum.test.ts +4 -2
  199. package/src/common/__tests__/spawnPath.test.ts +110 -0
  200. package/src/common/index.ts +1 -0
  201. package/src/common/spawnPath.ts +101 -0
  202. package/src/graphs/Graph.ts +95 -61
  203. package/src/graphs/HandoffRegistry.ts +48 -17
  204. package/src/graphs/MultiAgentGraph.ts +588 -327
  205. package/src/graphs/__tests__/HandoffRegistry.test.ts +4 -1
  206. package/src/graphs/__tests__/multi-agent-delegate.test.ts +61 -16
  207. package/src/graphs/__tests__/multi-agent-edges.test.ts +4 -2
  208. package/src/graphs/__tests__/multi-agent-nested-subgraph.test.ts +221 -0
  209. package/src/graphs/__tests__/structured-output.integration.test.ts +212 -118
  210. package/src/graphs/contextManagement.e2e.test.ts +1 -1
  211. package/src/graphs/phases/__tests__/flushLoop.test.ts +264 -0
  212. package/src/graphs/phases/__tests__/memoryFlushPhase.test.ts +37 -0
  213. package/src/graphs/phases/__tests__/runMemoryFlush.test.ts +150 -0
  214. package/src/graphs/phases/flushLoop.ts +303 -0
  215. package/src/graphs/phases/memoryFlushPhase.ts +209 -0
  216. package/src/index.ts +30 -1
  217. package/src/llm/bedrock/index.ts +4 -5
  218. package/src/memory/__tests__/citations.test.ts +61 -0
  219. package/src/memory/__tests__/compositeBackend.test.ts +79 -0
  220. package/src/memory/__tests__/isolation.test.ts +206 -0
  221. package/src/memory/__tests__/mmr.test.ts +148 -0
  222. package/src/memory/__tests__/mockBackend.ts +161 -0
  223. package/src/memory/__tests__/paths.test.ts +168 -0
  224. package/src/memory/__tests__/recallTracking.test.ts +96 -0
  225. package/src/memory/__tests__/temporalDecay.test.ts +151 -0
  226. package/src/memory/citations.ts +80 -0
  227. package/src/memory/compositeBackend.ts +99 -0
  228. package/src/memory/constants.ts +229 -0
  229. package/src/memory/embeddings.ts +188 -0
  230. package/src/memory/factory.ts +111 -0
  231. package/src/memory/index.ts +46 -0
  232. package/src/memory/migrate.ts +116 -0
  233. package/src/memory/mmr.ts +161 -0
  234. package/src/memory/paths.ts +258 -0
  235. package/src/memory/pgvectorStore.ts +324 -0
  236. package/src/memory/recallTracking.ts +127 -0
  237. package/src/memory/schema.sql +51 -0
  238. package/src/memory/temporalDecay.ts +134 -0
  239. package/src/memory/types.ts +185 -0
  240. package/src/nodes/ApprovalGateNode.ts +4 -10
  241. package/src/nodes/__tests__/ApprovalGateNode.test.ts +11 -20
  242. package/src/prompts/memoryFlushPrompt.ts +78 -0
  243. package/src/run.ts +17 -6
  244. package/src/scripts/test-bedrock-handoff-autonomous.ts +56 -20
  245. package/src/specs/agent-handoffs-bedrock.integration.test.ts +8 -5
  246. package/src/specs/agent-handoffs.test.ts +8 -2
  247. package/src/tools/AskUser.ts +7 -2
  248. package/src/tools/BrowserTools.ts +3 -5
  249. package/src/tools/ToolNode.ts +150 -13
  250. package/src/tools/__tests__/ToolApproval.test.ts +22 -9
  251. package/src/tools/approval/__tests__/constants.test.ts +1 -1
  252. package/src/tools/approval/constants.ts +2 -2
  253. package/src/tools/memory/__tests__/memoryTools.test.ts +205 -0
  254. package/src/tools/memory/index.ts +96 -0
  255. package/src/tools/memory/memoryAppendTool.ts +101 -0
  256. package/src/tools/memory/memoryGetTool.ts +53 -0
  257. package/src/tools/memory/memorySearchTool.ts +80 -0
  258. package/src/tools/memory/shared.ts +169 -0
  259. package/src/tools/search/search.test.ts +6 -1
  260. package/src/types/graph.ts +10 -3
  261. package/src/utils/__tests__/childAgentContext.test.ts +217 -0
  262. package/src/utils/__tests__/errors.test.ts +136 -0
  263. package/src/utils/__tests__/finishReasons.test.ts +55 -0
  264. package/src/utils/__tests__/toolCallNormalization.test.ts +181 -0
  265. package/src/utils/childAgentContext.ts +259 -0
  266. package/src/utils/errors.ts +115 -0
  267. package/src/utils/events.ts +37 -7
  268. package/src/utils/finishReasons.ts +40 -0
  269. package/src/utils/index.ts +1 -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
@@ -42,7 +42,10 @@ function createFailingPromise(
42
42
  /** Stub extractResult — returns content string from last AI message */
43
43
  function extractResult(messages: any[], _agentId: string): string {
44
44
  for (let i = messages.length - 1; i >= 0; i--) {
45
- if (messages[i].getType() === 'ai' && typeof messages[i].content === 'string') {
45
+ if (
46
+ messages[i].getType() === 'ai' &&
47
+ typeof messages[i].content === 'string'
48
+ ) {
46
49
  return messages[i].content;
47
50
  }
48
51
  }
@@ -100,7 +100,9 @@ describe('extractHandoffResult', () => {
100
100
  new AIMessage('found the data'),
101
101
  new AIMessage({
102
102
  content: '',
103
- tool_calls: [{ name: 'search', args: {}, id: 'tc1', type: 'tool_call' }],
103
+ tool_calls: [
104
+ { name: 'search', args: {}, id: 'tc1', type: 'tool_call' },
105
+ ],
104
106
  }),
105
107
  new ToolMessage({ content: 'search result', tool_call_id: 'tc1' }),
106
108
  ];
@@ -109,16 +111,18 @@ describe('extractHandoffResult', () => {
109
111
  });
110
112
 
111
113
  it('returns fallback message when no AIMessage has text', () => {
112
- const messages: BaseMessage[] = [
113
- new HumanMessage('task'),
114
- ];
114
+ const messages: BaseMessage[] = [new HumanMessage('task')];
115
115
  const result = MultiAgentGraph.extractHandoffResult(messages, 'researcher');
116
- expect(result).toBe('[Agent "researcher" completed but produced no text output]');
116
+ expect(result).toBe(
117
+ '[Agent "researcher" completed but produced no text output]'
118
+ );
117
119
  });
118
120
 
119
121
  it('returns fallback for empty messages array', () => {
120
122
  const result = MultiAgentGraph.extractHandoffResult([], 'agent');
121
- expect(result).toBe('[Agent "agent" completed but produced no text output]');
123
+ expect(result).toBe(
124
+ '[Agent "agent" completed but produced no text output]'
125
+ );
122
126
  });
123
127
 
124
128
  it('trims whitespace from extracted text', () => {
@@ -160,7 +164,8 @@ describe('truncateHandoffResult', () => {
160
164
  const maxChars = 200;
161
165
  const truncated = MultiAgentGraph.truncateHandoffResult(longText, maxChars);
162
166
 
163
- const notice = '\n\n[... handoff output truncated — middle section omitted to fit parent context ...]\n\n';
167
+ const notice =
168
+ '\n\n[... handoff output truncated — middle section omitted to fit parent context ...]\n\n';
164
169
  const available = maxChars - notice.length;
165
170
  const expectedHead = Math.floor(available * 0.6);
166
171
  const expectedTail = available - expectedHead;
@@ -249,7 +254,12 @@ describe('prepareHandoffMessages', () => {
249
254
  new AIMessage({
250
255
  content: 'Let me call a tool',
251
256
  tool_calls: [
252
- { name: 'lc_handoff_to_researcher', args: { instructions: 'research this' }, id: 'tc1', type: 'tool_call' },
257
+ {
258
+ name: 'lc_handoff_to_researcher',
259
+ args: { instructions: 'research this' },
260
+ id: 'tc1',
261
+ type: 'tool_call',
262
+ },
253
263
  ],
254
264
  }),
255
265
  // No ToolMessage for tc1 — it's orphaned
@@ -268,10 +278,19 @@ describe('prepareHandoffMessages', () => {
268
278
  new AIMessage({
269
279
  content: 'I will search for you',
270
280
  tool_calls: [
271
- { name: 'web_search', args: { query: 'test' }, id: 'tc1', type: 'tool_call' },
281
+ {
282
+ name: 'web_search',
283
+ args: { query: 'test' },
284
+ id: 'tc1',
285
+ type: 'tool_call',
286
+ },
272
287
  ],
273
288
  }),
274
- new ToolMessage({ content: 'Found 3 results about testing', tool_call_id: 'tc1', name: 'web_search' }),
289
+ new ToolMessage({
290
+ content: 'Found 3 results about testing',
291
+ tool_call_id: 'tc1',
292
+ name: 'web_search',
293
+ }),
275
294
  new AIMessage('Based on the search, here is what I found.'),
276
295
  ];
277
296
  const result = MultiAgentGraph.prepareHandoffMessages(messages);
@@ -302,8 +321,16 @@ describe('prepareHandoffMessages', () => {
302
321
  { name: 'tool_b', args: {}, id: 'tc2', type: 'tool_call' },
303
322
  ],
304
323
  }),
305
- new ToolMessage({ content: 'result a', tool_call_id: 'tc1', name: 'tool_a' }),
306
- new ToolMessage({ content: 'result b', tool_call_id: 'tc2', name: 'tool_b' }),
324
+ new ToolMessage({
325
+ content: 'result a',
326
+ tool_call_id: 'tc1',
327
+ name: 'tool_a',
328
+ }),
329
+ new ToolMessage({
330
+ content: 'result b',
331
+ tool_call_id: 'tc2',
332
+ name: 'tool_b',
333
+ }),
307
334
  new AIMessage('Final answer.'),
308
335
  ];
309
336
  const result = MultiAgentGraph.prepareHandoffMessages(messages);
@@ -322,15 +349,29 @@ describe('prepareHandoffMessages', () => {
322
349
  new AIMessage({
323
350
  content: 'I will delegate to the researcher first.',
324
351
  tool_calls: [
325
- { name: 'lc_handoff_to_researcher', args: { instructions: 'research AI trends' }, id: 'h1', type: 'tool_call' },
352
+ {
353
+ name: 'lc_handoff_to_researcher',
354
+ args: { instructions: 'research AI trends' },
355
+ id: 'h1',
356
+ type: 'tool_call',
357
+ },
326
358
  ],
327
359
  }),
328
- new ToolMessage({ content: 'Research findings: AI adoption is growing rapidly...', tool_call_id: 'h1', name: 'lc_handoff_to_researcher' }),
360
+ new ToolMessage({
361
+ content: 'Research findings: AI adoption is growing rapidly...',
362
+ tool_call_id: 'h1',
363
+ name: 'lc_handoff_to_researcher',
364
+ }),
329
365
  // Orchestrator's response after getting research back
330
366
  new AIMessage({
331
367
  content: 'Now I will delegate to the writer.',
332
368
  tool_calls: [
333
- { name: 'lc_handoff_to_writer', args: { instructions: 'write report based on findings' }, id: 'h2', type: 'tool_call' },
369
+ {
370
+ name: 'lc_handoff_to_writer',
371
+ args: { instructions: 'write report based on findings' },
372
+ id: 'h2',
373
+ type: 'tool_call',
374
+ },
334
375
  ],
335
376
  }),
336
377
  // h2 is orphaned — writer hasn't returned yet (this is the current handoff)
@@ -399,7 +440,11 @@ describe('prepareHandoffMessages', () => {
399
440
  { name: 'some_tool', args: {}, id: 'tc1', type: 'tool_call' },
400
441
  ],
401
442
  }),
402
- new ToolMessage({ content: 'tool output', tool_call_id: 'tc1', name: 'some_tool' }),
443
+ new ToolMessage({
444
+ content: 'tool output',
445
+ tool_call_id: 'tc1',
446
+ name: 'some_tool',
447
+ }),
403
448
  ];
404
449
  const result = MultiAgentGraph.prepareHandoffMessages(messages);
405
450
  // Compacted AI is trailing → converted to HumanMessage
@@ -163,7 +163,8 @@ describe('edge categorization', () => {
163
163
  const edges: GraphEdge[] = [
164
164
  { from: 'supervisor', to: 'researcher', edgeType: EdgeType.HANDOFF },
165
165
  ];
166
- const { sequenceEdges, transferEdges, handoffEdges } = categorizeEdges(edges);
166
+ const { sequenceEdges, transferEdges, handoffEdges } =
167
+ categorizeEdges(edges);
167
168
  expect(handoffEdges).toHaveLength(1);
168
169
  expect(transferEdges).toHaveLength(0);
169
170
  expect(sequenceEdges).toHaveLength(0);
@@ -175,7 +176,8 @@ describe('edge categorization', () => {
175
176
  { from: 'supervisor', to: 'writer', edgeType: EdgeType.TRANSFER },
176
177
  { from: 'supervisor', to: 'formatter', edgeType: EdgeType.SEQUENCE },
177
178
  ];
178
- const { sequenceEdges, transferEdges, handoffEdges } = categorizeEdges(edges);
179
+ const { sequenceEdges, transferEdges, handoffEdges } =
180
+ categorizeEdges(edges);
179
181
  expect(handoffEdges).toHaveLength(1);
180
182
  expect(transferEdges).toHaveLength(1);
181
183
  expect(sequenceEdges).toHaveLength(1);
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Unit tests for the nested sequence-in-handoff scoped subgraph logic.
3
+ *
4
+ * When an agent is a handoff target AND has its own downstream
5
+ * sequence/transfer edges (e.g. researcher → prod_assistant), the parent's
6
+ * `subgraph.invoke()` must run the full local workflow before returning.
7
+ *
8
+ * The implementation lives in three private methods on MultiAgentGraph:
9
+ * - computeReachableViaNonHandoff(rootAgentId): BFS over sequence+transfer
10
+ * edges (handoff edges excluded), returns the local scope set
11
+ * - createAgentSubgraph(agentId): fast-path single-node vs scoped subgraph
12
+ * - buildScopedSubgraph(rootAgentId, agentIds): mini StateGraph wiring
13
+ *
14
+ * These tests mirror the pattern in `multi-agent-edges.test.ts`: extract the
15
+ * pure algorithm as a local function matching the private method 1:1 and
16
+ * exercise the edge cases. This keeps the test unit-level (no LLM mocks, no
17
+ * real agent instantiation) while still guarding the architectural fix.
18
+ *
19
+ * See docs/multi-agent-nesting-architecture.md §6.
20
+ */
21
+ import { EdgeType } from '@/common';
22
+ import type { GraphEdge } from '@/types';
23
+
24
+ /**
25
+ * BFS across sequence + transfer edges (NOT handoff edges).
26
+ * Mirrors MultiAgentGraph.computeReachableViaNonHandoff().
27
+ */
28
+ function computeReachableViaNonHandoff(
29
+ rootAgentId: string,
30
+ sequenceEdges: GraphEdge[],
31
+ transferEdges: GraphEdge[],
32
+ knownAgents: Set<string>
33
+ ): Set<string> {
34
+ const reachable = new Set<string>([rootAgentId]);
35
+ const queue: string[] = [rootAgentId];
36
+ const localEdges = [...sequenceEdges, ...transferEdges];
37
+ while (queue.length > 0) {
38
+ const current = queue.shift()!;
39
+ for (const edge of localEdges) {
40
+ const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
41
+ if (!sources.includes(current)) continue;
42
+ const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
43
+ for (const dest of dests) {
44
+ if (!reachable.has(dest) && knownAgents.has(dest)) {
45
+ reachable.add(dest);
46
+ queue.push(dest);
47
+ }
48
+ }
49
+ }
50
+ }
51
+ return reachable;
52
+ }
53
+
54
+ /**
55
+ * Mirrors the fast-path decision in MultiAgentGraph.createAgentSubgraph():
56
+ * if only the root itself is reachable, return "single-node"; otherwise
57
+ * "scoped-subgraph".
58
+ */
59
+ function decideSubgraphShape(
60
+ reachable: Set<string>
61
+ ): 'single-node' | 'scoped-subgraph' {
62
+ return reachable.size === 1 ? 'single-node' : 'scoped-subgraph';
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // computeReachableViaNonHandoff
67
+ // ---------------------------------------------------------------------------
68
+ describe('computeReachableViaNonHandoff', () => {
69
+ it('returns just the root when there are no outgoing edges', () => {
70
+ const known = new Set(['researcher']);
71
+ const reachable = computeReachableViaNonHandoff(
72
+ 'researcher',
73
+ [],
74
+ [],
75
+ known
76
+ );
77
+ expect(Array.from(reachable)).toEqual(['researcher']);
78
+ });
79
+
80
+ it('walks a single downstream sequence edge (researcher → prod_assistant)', () => {
81
+ const known = new Set(['researcher', 'prod_assistant']);
82
+ const sequenceEdges: GraphEdge[] = [
83
+ { from: 'researcher', to: 'prod_assistant', edgeType: EdgeType.SEQUENCE },
84
+ ];
85
+ const reachable = computeReachableViaNonHandoff(
86
+ 'researcher',
87
+ sequenceEdges,
88
+ [],
89
+ known
90
+ );
91
+ expect(reachable.size).toBe(2);
92
+ expect(reachable.has('researcher')).toBe(true);
93
+ expect(reachable.has('prod_assistant')).toBe(true);
94
+ });
95
+
96
+ it('walks transitive sequence chain (a → b → c)', () => {
97
+ const known = new Set(['a', 'b', 'c']);
98
+ const seq: GraphEdge[] = [
99
+ { from: 'a', to: 'b', edgeType: EdgeType.SEQUENCE },
100
+ { from: 'b', to: 'c', edgeType: EdgeType.SEQUENCE },
101
+ ];
102
+ const reachable = computeReachableViaNonHandoff('a', seq, [], known);
103
+ expect(reachable.size).toBe(3);
104
+ });
105
+
106
+ it('skips handoff edges — downstream handoff targets are out of scope', () => {
107
+ const known = new Set(['researcher', 'prod_assistant', 'critic']);
108
+ const sequenceEdges: GraphEdge[] = [
109
+ { from: 'researcher', to: 'prod_assistant', edgeType: EdgeType.SEQUENCE },
110
+ ];
111
+ // critic is NOT reachable via non-handoff walk even though a handoff edge
112
+ // would connect prod_assistant → critic. Handoff edges are excluded by
113
+ // construction: the caller only passes sequence + transfer edges.
114
+ const reachable = computeReachableViaNonHandoff(
115
+ 'researcher',
116
+ sequenceEdges,
117
+ [],
118
+ known
119
+ );
120
+ expect(reachable.has('critic')).toBe(false);
121
+ expect(reachable.size).toBe(2);
122
+ });
123
+
124
+ it('follows transfer edges (Command-based routing)', () => {
125
+ const known = new Set(['a', 'b']);
126
+ const transferEdges: GraphEdge[] = [
127
+ { from: 'a', to: 'b', edgeType: EdgeType.TRANSFER },
128
+ ];
129
+ const reachable = computeReachableViaNonHandoff(
130
+ 'a',
131
+ [],
132
+ transferEdges,
133
+ known
134
+ );
135
+ expect(reachable.size).toBe(2);
136
+ });
137
+
138
+ it('is robust to cycles (a → b → a)', () => {
139
+ const known = new Set(['a', 'b']);
140
+ const seq: GraphEdge[] = [
141
+ { from: 'a', to: 'b', edgeType: EdgeType.SEQUENCE },
142
+ { from: 'b', to: 'a', edgeType: EdgeType.SEQUENCE },
143
+ ];
144
+ const reachable = computeReachableViaNonHandoff('a', seq, [], known);
145
+ expect(reachable.size).toBe(2);
146
+ });
147
+
148
+ it('ignores destinations that are not known agents (orphaned references)', () => {
149
+ const known = new Set(['researcher']);
150
+ const seq: GraphEdge[] = [
151
+ { from: 'researcher', to: 'ghost_agent', edgeType: EdgeType.SEQUENCE },
152
+ ];
153
+ const reachable = computeReachableViaNonHandoff(
154
+ 'researcher',
155
+ seq,
156
+ [],
157
+ known
158
+ );
159
+ expect(Array.from(reachable)).toEqual(['researcher']);
160
+ });
161
+
162
+ it('expands fan-out sequence (single source → multiple destinations)', () => {
163
+ const known = new Set(['root', 'a', 'b']);
164
+ const seq: GraphEdge[] = [
165
+ { from: 'root', to: ['a', 'b'], edgeType: EdgeType.SEQUENCE },
166
+ ];
167
+ const reachable = computeReachableViaNonHandoff('root', seq, [], known);
168
+ expect(reachable.size).toBe(3);
169
+ });
170
+ });
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // createAgentSubgraph fast-path decision
174
+ // ---------------------------------------------------------------------------
175
+ describe('createAgentSubgraph shape decision', () => {
176
+ it('returns single-node when the handoff target has no local workflow', () => {
177
+ const known = new Set(['researcher']);
178
+ const reachable = computeReachableViaNonHandoff(
179
+ 'researcher',
180
+ [],
181
+ [],
182
+ known
183
+ );
184
+ expect(decideSubgraphShape(reachable)).toBe('single-node');
185
+ });
186
+
187
+ it('returns scoped-subgraph when the handoff target has a downstream sequence', () => {
188
+ const known = new Set(['researcher', 'prod_assistant']);
189
+ const seq: GraphEdge[] = [
190
+ { from: 'researcher', to: 'prod_assistant', edgeType: EdgeType.SEQUENCE },
191
+ ];
192
+ const reachable = computeReachableViaNonHandoff(
193
+ 'researcher',
194
+ seq,
195
+ [],
196
+ known
197
+ );
198
+ expect(decideSubgraphShape(reachable)).toBe('scoped-subgraph');
199
+ });
200
+
201
+ it('returns scoped-subgraph for the orchestrator → handoff → researcher → sequence case', () => {
202
+ // Full topology:
203
+ // orchestrator --handoff--> researcher --sequence--> prod_assistant
204
+ // The handoff edge is NOT passed here (only seq + transfer), so the
205
+ // researcher scope correctly contains [researcher, prod_assistant].
206
+ const known = new Set(['orchestrator', 'researcher', 'prod_assistant']);
207
+ const seq: GraphEdge[] = [
208
+ { from: 'researcher', to: 'prod_assistant', edgeType: EdgeType.SEQUENCE },
209
+ ];
210
+ const reachable = computeReachableViaNonHandoff(
211
+ 'researcher',
212
+ seq,
213
+ [],
214
+ known
215
+ );
216
+ expect(decideSubgraphShape(reachable)).toBe('scoped-subgraph');
217
+ expect(reachable.has('orchestrator')).toBe(false);
218
+ expect(reachable.has('researcher')).toBe(true);
219
+ expect(reachable.has('prod_assistant')).toBe(true);
220
+ });
221
+ });