@illuma-ai/agents 1.1.20 → 1.1.22

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 (246) hide show
  1. package/dist/cjs/graphs/Graph.cjs +12 -1
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/graphs/MultiAgentGraph.cjs +85 -1
  4. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  5. package/dist/cjs/llm/bedrock/index.cjs +14 -0
  6. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  7. package/dist/cjs/run.cjs +20 -9
  8. package/dist/cjs/run.cjs.map +1 -1
  9. package/dist/esm/graphs/Graph.mjs +12 -1
  10. package/dist/esm/graphs/Graph.mjs.map +1 -1
  11. package/dist/esm/graphs/MultiAgentGraph.mjs +85 -1
  12. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  13. package/dist/esm/llm/bedrock/index.mjs +14 -0
  14. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  15. package/dist/esm/run.mjs +20 -9
  16. package/dist/esm/run.mjs.map +1 -1
  17. package/dist/types/graphs/MultiAgentGraph.d.ts +17 -0
  18. package/package.json +1 -1
  19. package/src/graphs/Graph.ts +12 -1
  20. package/src/graphs/MultiAgentGraph.ts +105 -1
  21. package/src/graphs/__tests__/multi-agent-delegate.test.ts +191 -0
  22. package/src/llm/bedrock/index.ts +17 -0
  23. package/src/run.ts +20 -11
  24. package/src/scripts/test-bedrock-handoff-autonomous.ts +231 -0
  25. package/src/agents/AgentContext.js +0 -782
  26. package/src/agents/AgentContext.test.js +0 -421
  27. package/src/agents/__tests__/AgentContext.test.js +0 -678
  28. package/src/agents/__tests__/resolveStructuredOutputMode.test.js +0 -117
  29. package/src/common/enum.js +0 -192
  30. package/src/common/index.js +0 -3
  31. package/src/events.js +0 -166
  32. package/src/graphs/Graph.js +0 -1857
  33. package/src/graphs/MultiAgentGraph.js +0 -1092
  34. package/src/graphs/__tests__/structured-output.integration.test.js +0 -624
  35. package/src/graphs/__tests__/structured-output.test.js +0 -144
  36. package/src/graphs/contextManagement.e2e.test.js +0 -718
  37. package/src/graphs/contextManagement.test.js +0 -485
  38. package/src/graphs/handoffValidation.test.js +0 -276
  39. package/src/graphs/index.js +0 -3
  40. package/src/index.js +0 -28
  41. package/src/instrumentation.js +0 -21
  42. package/src/llm/anthropic/index.js +0 -319
  43. package/src/llm/anthropic/types.js +0 -46
  44. package/src/llm/anthropic/utils/message_inputs.js +0 -627
  45. package/src/llm/anthropic/utils/message_outputs.js +0 -290
  46. package/src/llm/anthropic/utils/output_parsers.js +0 -89
  47. package/src/llm/anthropic/utils/tools.js +0 -25
  48. package/src/llm/bedrock/__tests__/bedrock-caching.test.js +0 -392
  49. package/src/llm/bedrock/index.js +0 -303
  50. package/src/llm/bedrock/types.js +0 -2
  51. package/src/llm/bedrock/utils/index.js +0 -6
  52. package/src/llm/bedrock/utils/message_inputs.js +0 -463
  53. package/src/llm/bedrock/utils/message_outputs.js +0 -269
  54. package/src/llm/fake.js +0 -92
  55. package/src/llm/google/index.js +0 -215
  56. package/src/llm/google/types.js +0 -12
  57. package/src/llm/google/utils/common.js +0 -670
  58. package/src/llm/google/utils/tools.js +0 -111
  59. package/src/llm/google/utils/zod_to_genai_parameters.js +0 -47
  60. package/src/llm/openai/index.js +0 -1033
  61. package/src/llm/openai/types.js +0 -2
  62. package/src/llm/openai/utils/index.js +0 -756
  63. package/src/llm/openai/utils/isReasoningModel.test.js +0 -79
  64. package/src/llm/openrouter/index.js +0 -261
  65. package/src/llm/openrouter/reasoning.test.js +0 -181
  66. package/src/llm/providers.js +0 -36
  67. package/src/llm/text.js +0 -65
  68. package/src/llm/vertexai/index.js +0 -402
  69. package/src/messages/__tests__/tools.test.js +0 -392
  70. package/src/messages/cache.js +0 -404
  71. package/src/messages/cache.test.js +0 -1167
  72. package/src/messages/content.js +0 -48
  73. package/src/messages/content.test.js +0 -314
  74. package/src/messages/core.js +0 -359
  75. package/src/messages/ensureThinkingBlock.test.js +0 -997
  76. package/src/messages/format.js +0 -973
  77. package/src/messages/formatAgentMessages.test.js +0 -2278
  78. package/src/messages/formatAgentMessages.tools.test.js +0 -362
  79. package/src/messages/formatMessage.test.js +0 -608
  80. package/src/messages/ids.js +0 -18
  81. package/src/messages/index.js +0 -9
  82. package/src/messages/labelContentByAgent.test.js +0 -725
  83. package/src/messages/prune.js +0 -438
  84. package/src/messages/reducer.js +0 -60
  85. package/src/messages/shiftIndexTokenCountMap.test.js +0 -63
  86. package/src/messages/summarize.js +0 -146
  87. package/src/messages/summarize.test.js +0 -332
  88. package/src/messages/tools.js +0 -90
  89. package/src/mockStream.js +0 -81
  90. package/src/prompts/collab.js +0 -7
  91. package/src/prompts/index.js +0 -3
  92. package/src/prompts/taskmanager.js +0 -58
  93. package/src/run.js +0 -427
  94. package/src/schemas/index.js +0 -3
  95. package/src/schemas/schema-preparation.test.js +0 -370
  96. package/src/schemas/validate.js +0 -314
  97. package/src/schemas/validate.test.js +0 -264
  98. package/src/scripts/abort.js +0 -127
  99. package/src/scripts/ant_web_search.js +0 -130
  100. package/src/scripts/ant_web_search_edge_case.js +0 -133
  101. package/src/scripts/ant_web_search_error_edge_case.js +0 -119
  102. package/src/scripts/args.js +0 -41
  103. package/src/scripts/bedrock-cache-debug.js +0 -186
  104. package/src/scripts/bedrock-content-aggregation-test.js +0 -195
  105. package/src/scripts/bedrock-merge-test.js +0 -80
  106. package/src/scripts/bedrock-parallel-tools-test.js +0 -150
  107. package/src/scripts/caching.js +0 -106
  108. package/src/scripts/cli.js +0 -152
  109. package/src/scripts/cli2.js +0 -119
  110. package/src/scripts/cli3.js +0 -163
  111. package/src/scripts/cli4.js +0 -165
  112. package/src/scripts/cli5.js +0 -165
  113. package/src/scripts/code_exec.js +0 -171
  114. package/src/scripts/code_exec_files.js +0 -180
  115. package/src/scripts/code_exec_multi_session.js +0 -185
  116. package/src/scripts/code_exec_ptc.js +0 -265
  117. package/src/scripts/code_exec_session.js +0 -217
  118. package/src/scripts/code_exec_simple.js +0 -120
  119. package/src/scripts/content.js +0 -111
  120. package/src/scripts/empty_input.js +0 -125
  121. package/src/scripts/handoff-test.js +0 -96
  122. package/src/scripts/image.js +0 -138
  123. package/src/scripts/memory.js +0 -83
  124. package/src/scripts/multi-agent-chain.js +0 -271
  125. package/src/scripts/multi-agent-conditional.js +0 -185
  126. package/src/scripts/multi-agent-document-review-chain.js +0 -171
  127. package/src/scripts/multi-agent-hybrid-flow.js +0 -264
  128. package/src/scripts/multi-agent-parallel-start.js +0 -214
  129. package/src/scripts/multi-agent-parallel.js +0 -346
  130. package/src/scripts/multi-agent-sequence.js +0 -184
  131. package/src/scripts/multi-agent-supervisor.js +0 -324
  132. package/src/scripts/multi-agent-test.js +0 -147
  133. package/src/scripts/parallel-asymmetric-tools-test.js +0 -202
  134. package/src/scripts/parallel-full-metadata-test.js +0 -176
  135. package/src/scripts/parallel-tools-test.js +0 -256
  136. package/src/scripts/programmatic_exec.js +0 -277
  137. package/src/scripts/programmatic_exec_agent.js +0 -168
  138. package/src/scripts/search.js +0 -118
  139. package/src/scripts/sequential-full-metadata-test.js +0 -143
  140. package/src/scripts/simple.js +0 -174
  141. package/src/scripts/single-agent-metadata-test.js +0 -152
  142. package/src/scripts/stream.js +0 -113
  143. package/src/scripts/test-custom-prompt-key.js +0 -132
  144. package/src/scripts/test-handoff-input.js +0 -143
  145. package/src/scripts/test-handoff-preamble.js +0 -227
  146. package/src/scripts/test-handoff-steering.js +0 -353
  147. package/src/scripts/test-multi-agent-list-handoff.js +0 -318
  148. package/src/scripts/test-parallel-agent-labeling.js +0 -253
  149. package/src/scripts/test-parallel-handoffs.js +0 -229
  150. package/src/scripts/test-thinking-handoff-bedrock.js +0 -132
  151. package/src/scripts/test-thinking-handoff.js +0 -132
  152. package/src/scripts/test-thinking-to-thinking-handoff-bedrock.js +0 -140
  153. package/src/scripts/test-tool-before-handoff-role-order.js +0 -223
  154. package/src/scripts/test-tools-before-handoff.js +0 -187
  155. package/src/scripts/test_code_api.js +0 -263
  156. package/src/scripts/thinking-bedrock.js +0 -128
  157. package/src/scripts/thinking-vertexai.js +0 -130
  158. package/src/scripts/thinking.js +0 -134
  159. package/src/scripts/tool_search.js +0 -114
  160. package/src/scripts/tools.js +0 -125
  161. package/src/specs/agent-handoffs-bedrock.integration.test.js +0 -280
  162. package/src/specs/agent-handoffs.test.js +0 -924
  163. package/src/specs/anthropic.simple.test.js +0 -287
  164. package/src/specs/azure.simple.test.js +0 -381
  165. package/src/specs/cache.simple.test.js +0 -282
  166. package/src/specs/custom-event-await.test.js +0 -148
  167. package/src/specs/deepseek.simple.test.js +0 -189
  168. package/src/specs/emergency-prune.test.js +0 -308
  169. package/src/specs/moonshot.simple.test.js +0 -237
  170. package/src/specs/observability.integration.test.js +0 -1337
  171. package/src/specs/openai.simple.test.js +0 -233
  172. package/src/specs/openrouter.simple.test.js +0 -202
  173. package/src/specs/prune.test.js +0 -733
  174. package/src/specs/reasoning.test.js +0 -144
  175. package/src/specs/spec.utils.js +0 -4
  176. package/src/specs/thinking-handoff.test.js +0 -486
  177. package/src/specs/thinking-prune.test.js +0 -600
  178. package/src/specs/token-distribution-edge-case.test.js +0 -246
  179. package/src/specs/token-memoization.test.js +0 -32
  180. package/src/specs/tokens.test.js +0 -49
  181. package/src/specs/tool-error.test.js +0 -139
  182. package/src/splitStream.js +0 -204
  183. package/src/splitStream.test.js +0 -504
  184. package/src/stream.js +0 -650
  185. package/src/stream.test.js +0 -225
  186. package/src/test/mockTools.js +0 -340
  187. package/src/tools/BrowserTools.js +0 -245
  188. package/src/tools/Calculator.js +0 -38
  189. package/src/tools/Calculator.test.js +0 -225
  190. package/src/tools/CodeExecutor.js +0 -233
  191. package/src/tools/ProgrammaticToolCalling.js +0 -602
  192. package/src/tools/StreamingToolCallBuffer.js +0 -179
  193. package/src/tools/ToolNode.js +0 -930
  194. package/src/tools/ToolSearch.js +0 -904
  195. package/src/tools/__tests__/BrowserTools.test.js +0 -306
  196. package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.js +0 -276
  197. package/src/tools/__tests__/ProgrammaticToolCalling.test.js +0 -807
  198. package/src/tools/__tests__/StreamingToolCallBuffer.test.js +0 -175
  199. package/src/tools/__tests__/ToolApproval.test.js +0 -675
  200. package/src/tools/__tests__/ToolNode.recovery.test.js +0 -200
  201. package/src/tools/__tests__/ToolNode.session.test.js +0 -319
  202. package/src/tools/__tests__/ToolSearch.integration.test.js +0 -125
  203. package/src/tools/__tests__/ToolSearch.test.js +0 -812
  204. package/src/tools/__tests__/handlers.test.js +0 -799
  205. package/src/tools/__tests__/truncation-recovery.integration.test.js +0 -362
  206. package/src/tools/handlers.js +0 -306
  207. package/src/tools/schema.js +0 -25
  208. package/src/tools/search/anthropic.js +0 -34
  209. package/src/tools/search/content.js +0 -116
  210. package/src/tools/search/content.test.js +0 -133
  211. package/src/tools/search/firecrawl.js +0 -173
  212. package/src/tools/search/format.js +0 -198
  213. package/src/tools/search/highlights.js +0 -241
  214. package/src/tools/search/index.js +0 -3
  215. package/src/tools/search/jina-reranker.test.js +0 -106
  216. package/src/tools/search/rerankers.js +0 -165
  217. package/src/tools/search/schema.js +0 -102
  218. package/src/tools/search/search.js +0 -561
  219. package/src/tools/search/serper-scraper.js +0 -126
  220. package/src/tools/search/test.js +0 -129
  221. package/src/tools/search/tool.js +0 -453
  222. package/src/tools/search/types.js +0 -2
  223. package/src/tools/search/utils.js +0 -59
  224. package/src/types/graph.js +0 -24
  225. package/src/types/graph.test.js +0 -192
  226. package/src/types/index.js +0 -7
  227. package/src/types/llm.js +0 -2
  228. package/src/types/messages.js +0 -2
  229. package/src/types/run.js +0 -2
  230. package/src/types/stream.js +0 -2
  231. package/src/types/tools.js +0 -2
  232. package/src/utils/contextAnalytics.js +0 -79
  233. package/src/utils/contextAnalytics.test.js +0 -166
  234. package/src/utils/events.js +0 -26
  235. package/src/utils/graph.js +0 -11
  236. package/src/utils/handlers.js +0 -65
  237. package/src/utils/index.js +0 -10
  238. package/src/utils/llm.js +0 -21
  239. package/src/utils/llmConfig.js +0 -205
  240. package/src/utils/logging.js +0 -37
  241. package/src/utils/misc.js +0 -51
  242. package/src/utils/run.js +0 -69
  243. package/src/utils/schema.js +0 -21
  244. package/src/utils/title.js +0 -119
  245. package/src/utils/tokens.js +0 -92
  246. package/src/utils/toonFormat.js +0 -379
@@ -206,3 +206,194 @@ describe('handoff tool naming', () => {
206
206
  expect(transferName).toBe('lc_transfer_to_test');
207
207
  });
208
208
  });
209
+
210
+ // ---------------------------------------------------------------------------
211
+ // prepareHandoffMessages
212
+ // ---------------------------------------------------------------------------
213
+ describe('prepareHandoffMessages', () => {
214
+ it('returns empty array for empty input', () => {
215
+ expect(MultiAgentGraph.prepareHandoffMessages([])).toEqual([]);
216
+ });
217
+
218
+ it('passes through plain HumanMessage and text-only AIMessage unchanged', () => {
219
+ const messages: BaseMessage[] = [
220
+ new HumanMessage('Hello'),
221
+ new AIMessage('Hi there'),
222
+ ];
223
+ const result = MultiAgentGraph.prepareHandoffMessages(messages);
224
+ expect(result).toHaveLength(2);
225
+ expect(result[0].getType()).toBe('human');
226
+ expect(result[0].content).toBe('Hello');
227
+ expect(result[1].getType()).toBe('ai');
228
+ expect(result[1].content).toBe('Hi there');
229
+ });
230
+
231
+ it('strips orphaned tool_use blocks (no matching tool_result)', () => {
232
+ const messages: BaseMessage[] = [
233
+ new HumanMessage('Do something'),
234
+ new AIMessage({
235
+ content: 'Let me call a tool',
236
+ tool_calls: [
237
+ { name: 'lc_handoff_to_researcher', args: { instructions: 'research this' }, id: 'tc1', type: 'tool_call' },
238
+ ],
239
+ }),
240
+ // No ToolMessage for tc1 — it's orphaned
241
+ ];
242
+ const result = MultiAgentGraph.prepareHandoffMessages(messages);
243
+ expect(result).toHaveLength(2);
244
+ expect(result[0].content).toBe('Do something');
245
+ // AI message should be converted to text-only (no tool_calls)
246
+ expect(result[1].getType()).toBe('ai');
247
+ expect(result[1].content).toContain('Let me call a tool');
248
+ // Should NOT contain tool_calls
249
+ const aiMsg = result[1] as AIMessage;
250
+ expect(aiMsg.tool_calls ?? []).toHaveLength(0);
251
+ });
252
+
253
+ it('compacts paired tool_use/tool_result into text summaries', () => {
254
+ const messages: BaseMessage[] = [
255
+ new HumanMessage('Search for data'),
256
+ new AIMessage({
257
+ content: 'I will search for you',
258
+ tool_calls: [
259
+ { name: 'web_search', args: { query: 'test' }, id: 'tc1', type: 'tool_call' },
260
+ ],
261
+ }),
262
+ new ToolMessage({ content: 'Found 3 results about testing', tool_call_id: 'tc1', name: 'web_search' }),
263
+ new AIMessage('Based on the search, here is what I found.'),
264
+ ];
265
+ const result = MultiAgentGraph.prepareHandoffMessages(messages);
266
+
267
+ // Should have: HumanMessage, compacted AIMessage (with tool summary), final AIMessage
268
+ expect(result).toHaveLength(3);
269
+
270
+ // ToolMessage should be removed
271
+ expect(result.every((m) => m.getType() !== 'tool')).toBe(true);
272
+
273
+ // Compacted AI message should contain tool summary
274
+ const compactedAI = result[1];
275
+ expect(compactedAI.content).toContain('I will search for you');
276
+ expect(compactedAI.content).toContain('web_search');
277
+ expect(compactedAI.content).toContain('Found 3 results');
278
+ });
279
+
280
+ it('removes ALL ToolMessages from output', () => {
281
+ const messages: BaseMessage[] = [
282
+ new HumanMessage('task'),
283
+ new AIMessage({
284
+ content: '',
285
+ tool_calls: [
286
+ { name: 'tool_a', args: {}, id: 'tc1', type: 'tool_call' },
287
+ { name: 'tool_b', args: {}, id: 'tc2', type: 'tool_call' },
288
+ ],
289
+ }),
290
+ new ToolMessage({ content: 'result a', tool_call_id: 'tc1', name: 'tool_a' }),
291
+ new ToolMessage({ content: 'result b', tool_call_id: 'tc2', name: 'tool_b' }),
292
+ new AIMessage('Final answer.'),
293
+ ];
294
+ const result = MultiAgentGraph.prepareHandoffMessages(messages);
295
+
296
+ // Zero ToolMessages in output
297
+ const toolMsgs = result.filter((m) => m.getType() === 'tool');
298
+ expect(toolMsgs).toHaveLength(0);
299
+ });
300
+
301
+ it('handles multi-hop scenario: multiple tool interactions compacted', () => {
302
+ // Simulates: Orchestrator called researcher (handoff), got result,
303
+ // then called writer (handoff) — the writer should get clean messages
304
+ const messages: BaseMessage[] = [
305
+ new HumanMessage('Research and write a report'),
306
+ // Orchestrator's first tool call (handoff to researcher)
307
+ new AIMessage({
308
+ content: 'I will delegate to the researcher first.',
309
+ tool_calls: [
310
+ { name: 'lc_handoff_to_researcher', args: { instructions: 'research AI trends' }, id: 'h1', type: 'tool_call' },
311
+ ],
312
+ }),
313
+ new ToolMessage({ content: 'Research findings: AI adoption is growing rapidly...', tool_call_id: 'h1', name: 'lc_handoff_to_researcher' }),
314
+ // Orchestrator's response after getting research back
315
+ new AIMessage({
316
+ content: 'Now I will delegate to the writer.',
317
+ tool_calls: [
318
+ { name: 'lc_handoff_to_writer', args: { instructions: 'write report based on findings' }, id: 'h2', type: 'tool_call' },
319
+ ],
320
+ }),
321
+ // h2 is orphaned — writer hasn't returned yet (this is the current handoff)
322
+ ];
323
+
324
+ const result = MultiAgentGraph.prepareHandoffMessages(messages);
325
+
326
+ // No ToolMessages
327
+ expect(result.filter((m) => m.getType() === 'tool')).toHaveLength(0);
328
+
329
+ // No tool_calls on any AI message
330
+ for (const msg of result) {
331
+ if (msg.getType() === 'ai') {
332
+ expect((msg as AIMessage).tool_calls ?? []).toHaveLength(0);
333
+ }
334
+ }
335
+
336
+ // Should have: HumanMessage + 2 compacted/cleaned AI messages
337
+ expect(result).toHaveLength(3);
338
+
339
+ // First AI message should have researcher tool summary (paired)
340
+ expect(result[1].content).toContain('lc_handoff_to_researcher');
341
+ expect(result[1].content).toContain('AI adoption');
342
+
343
+ // Second AI message should be text-only (orphaned tool_use stripped)
344
+ expect(result[2].content).toContain('Now I will delegate to the writer');
345
+ });
346
+
347
+ it('preserves message order', () => {
348
+ const messages: BaseMessage[] = [
349
+ new HumanMessage('Step 1'),
350
+ new AIMessage('Response 1'),
351
+ new HumanMessage('Step 2'),
352
+ new AIMessage('Response 2'),
353
+ ];
354
+ const result = MultiAgentGraph.prepareHandoffMessages(messages);
355
+ expect(result).toHaveLength(4);
356
+ expect(result[0].content).toBe('Step 1');
357
+ expect(result[1].content).toBe('Response 1');
358
+ expect(result[2].content).toBe('Step 2');
359
+ expect(result[3].content).toBe('Response 2');
360
+ });
361
+
362
+ it('drops AI messages that have only tool_calls and no text content', () => {
363
+ const messages: BaseMessage[] = [
364
+ new HumanMessage('Do it'),
365
+ new AIMessage({
366
+ content: '',
367
+ tool_calls: [
368
+ { name: 'orphan_tool', args: {}, id: 'orp1', type: 'tool_call' },
369
+ ],
370
+ }),
371
+ ];
372
+ const result = MultiAgentGraph.prepareHandoffMessages(messages);
373
+ // AI message with empty content + orphaned tool = nothing to keep
374
+ expect(result).toHaveLength(1);
375
+ expect(result[0].content).toBe('Do it');
376
+ });
377
+
378
+ it('handles AI messages with array content (text blocks)', () => {
379
+ const messages: BaseMessage[] = [
380
+ new HumanMessage('task'),
381
+ new AIMessage({
382
+ content: [
383
+ { type: 'text', text: 'Part one.' },
384
+ { type: 'text', text: 'Part two.' },
385
+ ],
386
+ tool_calls: [
387
+ { name: 'some_tool', args: {}, id: 'tc1', type: 'tool_call' },
388
+ ],
389
+ }),
390
+ new ToolMessage({ content: 'tool output', tool_call_id: 'tc1', name: 'some_tool' }),
391
+ ];
392
+ const result = MultiAgentGraph.prepareHandoffMessages(messages);
393
+ expect(result).toHaveLength(2);
394
+ const aiContent = result[1].content as string;
395
+ expect(aiContent).toContain('Part one.');
396
+ expect(aiContent).toContain('Part two.');
397
+ expect(aiContent).toContain('some_tool');
398
+ });
399
+ });
@@ -167,6 +167,23 @@ export class IllumaBedrockConverse extends ChatBedrockConverse {
167
167
  const params = super.invocationParams(options);
168
168
 
169
169
  // Add cachePoint to tools array if promptCache is enabled and tools exist
170
+ /**
171
+ * Bedrock requires all toolSpec.description fields to be non-empty strings.
172
+ * Some tools (e.g., MCP-sourced or dynamically created) may have empty or
173
+ * missing descriptions. Patch them here to avoid Bedrock validation errors.
174
+ */
175
+ if (
176
+ params.toolConfig?.tools &&
177
+ Array.isArray(params.toolConfig.tools)
178
+ ) {
179
+ for (const t of params.toolConfig.tools) {
180
+ const spec = (t as { toolSpec?: { description?: string } }).toolSpec;
181
+ if (spec && (!spec.description || spec.description === '')) {
182
+ spec.description = spec.description || `Tool: ${(spec as { name?: string }).name ?? 'unknown'}`;
183
+ }
184
+ }
185
+ }
186
+
170
187
  // Only Claude models support cachePoint - check model name
171
188
  const modelId = this.model.toLowerCase();
172
189
  const isClaudeModel =
package/src/run.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/run.ts
2
2
  import './instrumentation';
3
3
  import { ObservabilityCallbackHandler } from '@illuma-ai/observability-langchain';
4
- import { Command } from '@langchain/langgraph';
4
+ import { Command, isGraphInterrupt } from '@langchain/langgraph';
5
5
  import { PromptTemplate } from '@langchain/core/prompts';
6
6
  import { RunnableLambda } from '@langchain/core/runnables';
7
7
  import { AzureChatOpenAI, ChatOpenAI } from '@langchain/openai';
@@ -349,20 +349,29 @@ export class Run<_T extends t.BaseGraphState> {
349
349
  ignoreCustomEvent: true,
350
350
  });
351
351
 
352
- for await (const event of stream) {
353
- const { data, metadata, ...info } = event;
352
+ try {
353
+ for await (const event of stream) {
354
+ const { data, metadata, ...info } = event;
354
355
 
355
- const eventName: t.EventName = info.event;
356
+ const eventName: t.EventName = info.event;
356
357
 
357
- /** Skip custom events as they're handled by our callback */
358
- if (eventName === GraphEvents.ON_CUSTOM_EVENT) {
359
- continue;
360
- }
358
+ /** Skip custom events as they're handled by our callback */
359
+ if (eventName === GraphEvents.ON_CUSTOM_EVENT) {
360
+ continue;
361
+ }
361
362
 
362
- const handler = this.handlerRegistry?.getHandler(eventName);
363
- if (handler) {
364
- await handler.handle(eventName, data, metadata, this.Graph);
363
+ const handler = this.handlerRegistry?.getHandler(eventName);
364
+ if (handler) {
365
+ await handler.handle(eventName, data, metadata, this.Graph);
366
+ }
367
+ }
368
+ } catch (e) {
369
+ // GraphInterrupt is expected when interrupt() fires (HITL approval flow).
370
+ // Exit gracefully so the host can check hasInterrupts() and resume.
371
+ if (isGraphInterrupt(e)) {
372
+ return undefined;
365
373
  }
374
+ throw e;
366
375
  }
367
376
 
368
377
  /**
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Integration test: Autonomous Handoff with Bedrock
3
+ *
4
+ * Tests the full autonomous handoff pattern where:
5
+ * 1. Orchestrator receives user request
6
+ * 2. Orchestrator delegates to specialist via handoff (subgraph.invoke)
7
+ * 3. Specialist completes work and returns result to orchestrator
8
+ * 4. Orchestrator synthesizes and responds
9
+ *
10
+ * This validates:
11
+ * - Handoff-only children don't cause UNREACHABLE_NODE errors
12
+ * - stripOrphanedToolUse prevents Bedrock tool_use/tool_result mismatch
13
+ * - Empty tool descriptions are sanitized for Bedrock
14
+ * - Orchestrator receives child result and produces final response
15
+ *
16
+ * Requires: BEDROCK_AWS_ACCESS_KEY_ID, BEDROCK_AWS_SECRET_ACCESS_KEY, BEDROCK_AWS_REGION
17
+ */
18
+ import { config } from 'dotenv';
19
+ config();
20
+ import { resolve } from 'path';
21
+ config({ path: resolve(process.cwd(), '..', 'ranger', '.env'), override: false });
22
+
23
+ // Disable observability for this test (requires @illuma-ai/observability-node)
24
+ delete process.env.ILLUMA_SECRET_KEY;
25
+ delete process.env.ILLUMA_PUBLIC_KEY;
26
+ delete process.env.ILLUMA_BASE_URL;
27
+
28
+ import { HumanMessage } from '@langchain/core/messages';
29
+ import { Run } from '@/run';
30
+ import { Providers, GraphEvents } from '@/common';
31
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
32
+ import { ToolEndHandler, ModelEndHandler } from '@/events';
33
+ import type * as t from '@/types';
34
+ import type { RunnableConfig } from '@langchain/core/runnables';
35
+
36
+ const bedrockRegion =
37
+ process.env.BEDROCK_AWS_REGION ?? process.env.BEDROCK_AWS_DEFAULT_REGION ?? 'us-east-1';
38
+
39
+ const bedrockOptions = {
40
+ model: 'us.anthropic.claude-3-5-haiku-20241022-v1:0',
41
+ region: bedrockRegion,
42
+ credentials: {
43
+ accessKeyId: process.env.BEDROCK_AWS_ACCESS_KEY_ID!,
44
+ secretAccessKey: process.env.BEDROCK_AWS_SECRET_ACCESS_KEY!,
45
+ },
46
+ };
47
+
48
+ function createBedrockAgent(
49
+ agentId: string,
50
+ name: string,
51
+ description: string,
52
+ instructions: string
53
+ ): t.AgentInputs {
54
+ return {
55
+ agentId,
56
+ name,
57
+ description,
58
+ provider: Providers.BEDROCK,
59
+ clientOptions: bedrockOptions,
60
+ instructions,
61
+ maxContextTokens: 16000,
62
+ };
63
+ }
64
+
65
+ async function testAutonomousHandoff() {
66
+ console.log('=== Autonomous Handoff with Bedrock ===\n');
67
+
68
+ const agents: t.AgentInputs[] = [
69
+ createBedrockAgent(
70
+ 'orchestrator',
71
+ 'Orchestrator',
72
+ 'Routes complex requests to specialist agents and synthesizes results',
73
+ `You are an autonomous orchestrator. Break down user requests and delegate to specialists using your handoff tools.
74
+
75
+ IMPORTANT: You MUST use your handoff tools to delegate work. Do NOT answer questions yourself.
76
+ - Use lc_handoff_to_researcher for any information gathering or research tasks
77
+ - Use lc_handoff_to_writer for any content creation tasks
78
+
79
+ After receiving results from a specialist, synthesize and present the final answer to the user.`
80
+ ),
81
+ createBedrockAgent(
82
+ 'researcher',
83
+ 'Researcher',
84
+ 'Finds and analyzes information on any topic',
85
+ `You are a research specialist. When you receive a request:
86
+ 1. Analyze what information is needed
87
+ 2. Provide a structured, factual response with key findings
88
+ 3. Be concise but thorough — aim for 3-5 key points
89
+
90
+ Do NOT delegate or transfer. Just do the research and respond.`
91
+ ),
92
+ createBedrockAgent(
93
+ 'writer',
94
+ 'Writer',
95
+ 'Creates professional written content',
96
+ `You are a professional writer. When you receive a request:
97
+ 1. Create well-structured, clear content
98
+ 2. Use appropriate tone and format
99
+ 3. Be concise
100
+
101
+ Do NOT delegate or transfer. Just write the content and respond.`
102
+ ),
103
+ ];
104
+
105
+ const edges: t.GraphEdge[] = [
106
+ { from: 'orchestrator', to: 'researcher', edgeType: 'handoff' },
107
+ { from: 'orchestrator', to: 'writer', edgeType: 'handoff' },
108
+ ];
109
+
110
+ // Set up content aggregator and handlers
111
+ const { contentParts, aggregateContent } = createContentAggregator();
112
+
113
+ const customHandlers = {
114
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
115
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
116
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
117
+ [GraphEvents.ON_RUN_STEP]: {
118
+ handle: (event: string, data: t.StreamEventData): void => {
119
+ aggregateContent({ event: event as GraphEvents, data: data as t.RunStep });
120
+ },
121
+ },
122
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
123
+ handle: (event: string, data: t.StreamEventData): void => {
124
+ aggregateContent({
125
+ event: event as GraphEvents,
126
+ data: data as unknown as { result: t.ToolEndEvent },
127
+ });
128
+ },
129
+ },
130
+ [GraphEvents.ON_MESSAGE_DELTA]: {
131
+ handle: (event: string, data: t.StreamEventData): void => {
132
+ aggregateContent({ event: event as GraphEvents, data: data as t.MessageDeltaEvent });
133
+ },
134
+ },
135
+ };
136
+
137
+ const run = await Run.create({
138
+ runId: `bedrock-autonomous-handoff-${Date.now()}`,
139
+ graphConfig: {
140
+ type: 'multi-agent',
141
+ agents,
142
+ edges,
143
+ },
144
+ customHandlers,
145
+ returnContent: true,
146
+ });
147
+
148
+ const streamConfig: Partial<RunnableConfig> & { version: 'v1' | 'v2'; streamMode: string } = {
149
+ configurable: { thread_id: 'bedrock-autonomous-test' },
150
+ streamMode: 'values',
151
+ version: 'v2',
152
+ };
153
+
154
+ // ── Test 1: Single handoff (Orchestrator → Researcher → back) ──
155
+ console.log('\n--- Test 1: Single Handoff (Research) ---');
156
+ console.log('Query: "What are the top 3 trends in enterprise AI adoption in 2025?"');
157
+
158
+ const messages1 = [
159
+ new HumanMessage('What are the top 3 trends in enterprise AI adoption in 2025?'),
160
+ ];
161
+
162
+ try {
163
+ await run.processStream({ messages: messages1 }, streamConfig);
164
+
165
+ const finalMessages = run.getRunMessages();
166
+ console.log(`\n✓ Graph completed. Total messages: ${finalMessages?.length ?? 0}`);
167
+
168
+ // Check that a handoff happened
169
+ const lastMsg = finalMessages?.[finalMessages.length - 1];
170
+ if (lastMsg) {
171
+ const content = typeof lastMsg.content === 'string'
172
+ ? lastMsg.content
173
+ : JSON.stringify(lastMsg.content);
174
+ console.log(`\n✓ Final response (first 500 chars):\n${content.slice(0, 500)}`);
175
+ }
176
+
177
+ // Verify last active agent tracking
178
+ const lastActiveAgentId = run.getLastActiveAgentId?.();
179
+ console.log(`\n✓ lastActiveAgentId: ${lastActiveAgentId}`);
180
+
181
+ console.log('\n✅ Test 1 PASSED — Single handoff completed successfully');
182
+ } catch (err) {
183
+ console.error('\n❌ Test 1 FAILED:', (err as Error).message);
184
+ console.error((err as Error).stack);
185
+ }
186
+
187
+ // ── Test 2: Multi-hop (Orchestrator → Researcher → back → Writer → back) ──
188
+ console.log('\n\n--- Test 2: Multi-Hop Handoff (Research → Write) ---');
189
+ console.log('Query: "Research AI agent frameworks and write a brief summary email"');
190
+
191
+ const run2 = await Run.create({
192
+ runId: `bedrock-autonomous-handoff-2-${Date.now()}`,
193
+ graphConfig: {
194
+ type: 'multi-agent',
195
+ agents,
196
+ edges,
197
+ },
198
+ returnContent: true,
199
+ });
200
+
201
+ const messages2 = [
202
+ new HumanMessage('Research the latest AI agent frameworks and write a brief 3-sentence summary.'),
203
+ ];
204
+
205
+ try {
206
+ await run2.processStream({ messages: messages2 }, streamConfig);
207
+
208
+ const finalMessages2 = run2.getRunMessages();
209
+ console.log(`\n✓ Graph completed. Total messages: ${finalMessages2?.length ?? 0}`);
210
+
211
+ const lastMsg2 = finalMessages2?.[finalMessages2.length - 1];
212
+ if (lastMsg2) {
213
+ const content = typeof lastMsg2.content === 'string'
214
+ ? lastMsg2.content
215
+ : JSON.stringify(lastMsg2.content);
216
+ console.log(`\n✓ Final response (first 500 chars):\n${content.slice(0, 500)}`);
217
+ }
218
+
219
+ const lastActiveAgentId2 = run2.getLastActiveAgentId?.();
220
+ console.log(`\n✓ lastActiveAgentId: ${lastActiveAgentId2}`);
221
+
222
+ console.log('\n✅ Test 2 PASSED — Multi-hop handoff completed successfully');
223
+ } catch (err) {
224
+ console.error('\n❌ Test 2 FAILED:', (err as Error).message);
225
+ console.error((err as Error).stack);
226
+ }
227
+
228
+ console.log('\n\n=== All Tests Complete ===');
229
+ }
230
+
231
+ testAutonomousHandoff().catch(console.error);