@librechat/agents 3.1.88 → 3.1.90

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 (96) hide show
  1. package/dist/cjs/graphs/Graph.cjs +25 -1
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/hooks/executeHooks.cjs +14 -7
  4. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  5. package/dist/cjs/llm/anthropic/index.cjs +8 -2
  6. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  7. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +34 -0
  8. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  9. package/dist/cjs/main.cjs +9 -0
  10. package/dist/cjs/main.cjs.map +1 -1
  11. package/dist/cjs/stream.cjs +115 -8
  12. package/dist/cjs/stream.cjs.map +1 -1
  13. package/dist/cjs/tools/BashExecutor.cjs +10 -9
  14. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  15. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +12 -8
  16. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  17. package/dist/cjs/tools/CodeExecutor.cjs +35 -11
  18. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  19. package/dist/cjs/tools/CodeSessionFileSummary.cjs +63 -0
  20. package/dist/cjs/tools/CodeSessionFileSummary.cjs.map +1 -0
  21. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +16 -12
  22. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  23. package/dist/cjs/tools/ToolNode.cjs +32 -12
  24. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  25. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +319 -29
  26. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  27. package/dist/cjs/tools/toolOutputReferences.cjs +8 -0
  28. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  29. package/dist/cjs/utils/events.cjs +3 -1
  30. package/dist/cjs/utils/events.cjs.map +1 -1
  31. package/dist/esm/graphs/Graph.mjs +25 -1
  32. package/dist/esm/graphs/Graph.mjs.map +1 -1
  33. package/dist/esm/hooks/executeHooks.mjs +14 -7
  34. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  35. package/dist/esm/llm/anthropic/index.mjs +9 -3
  36. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  37. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +33 -1
  38. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  39. package/dist/esm/main.mjs +2 -1
  40. package/dist/esm/main.mjs.map +1 -1
  41. package/dist/esm/stream.mjs +115 -8
  42. package/dist/esm/stream.mjs.map +1 -1
  43. package/dist/esm/tools/BashExecutor.mjs +11 -10
  44. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  45. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +13 -9
  46. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  47. package/dist/esm/tools/CodeExecutor.mjs +29 -12
  48. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  49. package/dist/esm/tools/CodeSessionFileSummary.mjs +60 -0
  50. package/dist/esm/tools/CodeSessionFileSummary.mjs.map +1 -0
  51. package/dist/esm/tools/ProgrammaticToolCalling.mjs +17 -13
  52. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  53. package/dist/esm/tools/ToolNode.mjs +32 -12
  54. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  55. package/dist/esm/tools/subagent/SubagentExecutor.mjs +320 -31
  56. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  57. package/dist/esm/tools/toolOutputReferences.mjs +8 -1
  58. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  59. package/dist/esm/utils/events.mjs +3 -1
  60. package/dist/esm/utils/events.mjs.map +1 -1
  61. package/dist/types/graphs/Graph.d.ts +8 -0
  62. package/dist/types/llm/anthropic/index.d.ts +3 -1
  63. package/dist/types/llm/anthropic/utils/message_inputs.d.ts +4 -0
  64. package/dist/types/tools/BashExecutor.d.ts +3 -3
  65. package/dist/types/tools/CodeExecutor.d.ts +10 -3
  66. package/dist/types/tools/CodeSessionFileSummary.d.ts +3 -0
  67. package/dist/types/tools/ProgrammaticToolCalling.d.ts +4 -4
  68. package/dist/types/tools/subagent/SubagentExecutor.d.ts +8 -5
  69. package/dist/types/types/tools.d.ts +11 -3
  70. package/dist/types/utils/events.d.ts +1 -1
  71. package/package.json +1 -1
  72. package/src/__tests__/stream.eagerEventExecution.test.ts +1073 -221
  73. package/src/graphs/Graph.ts +27 -5
  74. package/src/hooks/__tests__/executeHooks.test.ts +38 -0
  75. package/src/hooks/executeHooks.ts +27 -7
  76. package/src/llm/anthropic/index.ts +27 -3
  77. package/src/llm/anthropic/llm.spec.ts +60 -1
  78. package/src/llm/anthropic/utils/message_inputs.ts +46 -0
  79. package/src/specs/subagent.test.ts +87 -1
  80. package/src/stream.ts +163 -12
  81. package/src/tools/BashExecutor.ts +21 -10
  82. package/src/tools/BashProgrammaticToolCalling.ts +21 -9
  83. package/src/tools/CodeExecutor.ts +55 -12
  84. package/src/tools/CodeSessionFileSummary.ts +80 -0
  85. package/src/tools/ProgrammaticToolCalling.ts +25 -12
  86. package/src/tools/ToolNode.ts +142 -116
  87. package/src/tools/__tests__/BashExecutor.test.ts +9 -0
  88. package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +43 -0
  89. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +100 -16
  90. package/src/tools/__tests__/SubagentExecutor.test.ts +540 -6
  91. package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +278 -14
  92. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +52 -0
  93. package/src/tools/__tests__/subagentHooks.test.ts +237 -0
  94. package/src/tools/subagent/SubagentExecutor.ts +514 -36
  95. package/src/types/tools.ts +11 -3
  96. package/src/utils/events.ts +4 -2
@@ -11,7 +11,7 @@ import {
11
11
  } from '@/common';
12
12
  import { HandlerRegistry } from '@/events';
13
13
  import * as events from '@/utils/events';
14
- import { ChatModelStreamHandler } from '@/stream';
14
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
15
15
  import {
16
16
  STREAMED_TOOL_CALL_SEAL_METADATA_KEY,
17
17
  STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY,
@@ -71,7 +71,8 @@ function createGraph(overrides: Partial<StandardGraph> = {}): StandardGraph {
71
71
  (details as t.StepDetails).type === StepTypes.TOOL_CALLS &&
72
72
  Array.isArray((details as t.ToolCallsDetails).tool_calls)
73
73
  ) {
74
- for (const toolCall of (details as t.ToolCallsDetails).tool_calls ?? []) {
74
+ for (const toolCall of (details as t.ToolCallsDetails).tool_calls ??
75
+ []) {
75
76
  if (toolCall.id != null && toolCall.id !== '') {
76
77
  graph.toolCallStepIds.set(toolCall.id, id);
77
78
  }
@@ -110,8 +111,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
110
111
  it('prestarts a complete event-driven tool call from the stream', async () => {
111
112
  const graph = createGraph();
112
113
  const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
113
- jest.spyOn(events, 'safeDispatchCustomEvent').mockImplementation(
114
- async (event, data): Promise<void> => {
114
+ jest
115
+ .spyOn(events, 'safeDispatchCustomEvent')
116
+ .mockImplementation(async (event, data): Promise<void> => {
115
117
  if (event !== GraphEvents.ON_TOOL_EXECUTE) {
116
118
  return;
117
119
  }
@@ -124,8 +126,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
124
126
  content: 'sunny',
125
127
  },
126
128
  ]);
127
- }
128
- );
129
+ });
129
130
 
130
131
  await new ChatModelStreamHandler().handle(
131
132
  GraphEvents.CHAT_MODEL_STREAM,
@@ -162,11 +163,65 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
162
163
  expect(graph.toolCallStepIds.has('call_weather')).toBe(true);
163
164
  });
164
165
 
166
+ it('prestarts when subagent callback forwarding can execute tools without a handler registry', async () => {
167
+ const graph = createGraph({
168
+ handlerRegistry: undefined,
169
+ eventToolExecutionAvailable: true,
170
+ });
171
+ const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
172
+ jest
173
+ .spyOn(events, 'safeDispatchCustomEvent')
174
+ .mockImplementation(async (event, data): Promise<void> => {
175
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
176
+ return;
177
+ }
178
+ const batch = data as t.ToolExecuteBatchRequest;
179
+ toolExecuteCalls.push(batch);
180
+ batch.resolve([
181
+ {
182
+ toolCallId: 'call_weather',
183
+ status: 'success',
184
+ content: 'sunny',
185
+ },
186
+ ]);
187
+ });
188
+
189
+ await new ChatModelStreamHandler().handle(
190
+ GraphEvents.CHAT_MODEL_STREAM,
191
+ {
192
+ chunk: {
193
+ content: '',
194
+ tool_calls: [
195
+ {
196
+ id: 'call_weather',
197
+ name: 'weather',
198
+ args: { city: 'NYC' },
199
+ },
200
+ ],
201
+ response_metadata: finalToolCallResponseMetadata,
202
+ } as unknown as t.StreamChunk,
203
+ },
204
+ { langgraph_node: 'agent' },
205
+ graph
206
+ );
207
+
208
+ expect(toolExecuteCalls).toHaveLength(1);
209
+ expect(toolExecuteCalls[0].toolCalls[0]).toMatchObject({
210
+ id: 'call_weather',
211
+ name: 'weather',
212
+ args: { city: 'NYC' },
213
+ stepId: expect.stringMatching(/^step_/),
214
+ turn: 0,
215
+ });
216
+ expect(graph.eagerEventToolExecutions.has('call_weather')).toBe(true);
217
+ });
218
+
165
219
  it('does not prestart parseable tool calls before a final tool-call signal', async () => {
166
220
  const graph = createGraph();
167
221
  const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
168
- jest.spyOn(events, 'safeDispatchCustomEvent').mockImplementation(
169
- async (event, data): Promise<void> => {
222
+ jest
223
+ .spyOn(events, 'safeDispatchCustomEvent')
224
+ .mockImplementation(async (event, data): Promise<void> => {
170
225
  if (event !== GraphEvents.ON_TOOL_EXECUTE) {
171
226
  return;
172
227
  }
@@ -179,8 +234,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
179
234
  content: 'sunny',
180
235
  },
181
236
  ]);
182
- }
183
- );
237
+ });
184
238
 
185
239
  const handler = new ChatModelStreamHandler();
186
240
  const metadata = { langgraph_node: 'agent' };
@@ -248,8 +302,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
248
302
  ) as unknown as StandardGraph['getAgentContext'],
249
303
  });
250
304
  const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
251
- jest.spyOn(events, 'safeDispatchCustomEvent').mockImplementation(
252
- async (event, data): Promise<void> => {
305
+ jest
306
+ .spyOn(events, 'safeDispatchCustomEvent')
307
+ .mockImplementation(async (event, data): Promise<void> => {
253
308
  if (event !== GraphEvents.ON_TOOL_EXECUTE) {
254
309
  return;
255
310
  }
@@ -262,8 +317,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
262
317
  content: `${request.name} result`,
263
318
  }))
264
319
  );
265
- }
266
- );
320
+ });
267
321
 
268
322
  await new ChatModelStreamHandler().handle(
269
323
  GraphEvents.CHAT_MODEL_STREAM,
@@ -308,7 +362,8 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
308
362
  }),
309
363
  ]);
310
364
  const weatherExecution = graph.eagerEventToolExecutions.get('call_weather');
311
- const calendarExecution = graph.eagerEventToolExecutions.get('call_calendar');
365
+ const calendarExecution =
366
+ graph.eagerEventToolExecutions.get('call_calendar');
312
367
  expect(weatherExecution).toMatchObject({
313
368
  toolCallId: 'call_weather',
314
369
  toolName: 'weather',
@@ -325,8 +380,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
325
380
  it('assigns same-tool eager turns in model emission order', async () => {
326
381
  const graph = createGraph();
327
382
  const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
328
- jest.spyOn(events, 'safeDispatchCustomEvent').mockImplementation(
329
- async (event, data): Promise<void> => {
383
+ jest
384
+ .spyOn(events, 'safeDispatchCustomEvent')
385
+ .mockImplementation(async (event, data): Promise<void> => {
330
386
  if (event !== GraphEvents.ON_TOOL_EXECUTE) {
331
387
  return;
332
388
  }
@@ -339,8 +395,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
339
395
  content: `${request.args.city} weather`,
340
396
  }))
341
397
  );
342
- }
343
- );
398
+ });
344
399
 
345
400
  await new ChatModelStreamHandler().handle(
346
401
  GraphEvents.CHAT_MODEL_STREAM,
@@ -371,10 +426,12 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
371
426
  0, 1,
372
427
  ]);
373
428
  expect(graph.eagerEventToolUsageCount.get('weather')).toBe(2);
374
- expect(graph.eagerEventToolExecutions.get('call_weather_1')?.request.turn)
375
- .toBe(0);
376
- expect(graph.eagerEventToolExecutions.get('call_weather_2')?.request.turn)
377
- .toBe(1);
429
+ expect(
430
+ graph.eagerEventToolExecutions.get('call_weather_1')?.request.turn
431
+ ).toBe(0);
432
+ expect(
433
+ graph.eagerEventToolExecutions.get('call_weather_2')?.request.turn
434
+ ).toBe(1);
378
435
  });
379
436
 
380
437
  it('scopes eager turn reservation by agent', async () => {
@@ -391,19 +448,21 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
391
448
  const graph = createGraph({
392
449
  getEagerEventToolUsageCount: jest.fn(getUsageCount),
393
450
  getAgentContext: jest.fn(
394
- (metadata?: Record<string, unknown>): AgentContext => ({
395
- provider: Providers.ANTHROPIC,
396
- reasoningKey: 'reasoning',
397
- toolDefinitions: [{ name: 'weather' }],
398
- graphTools: [],
399
- agentId:
400
- metadata?.langgraph_node === 'agent_2' ? 'agent_2' : 'agent_1',
401
- }) as unknown as AgentContext
451
+ (metadata?: Record<string, unknown>): AgentContext =>
452
+ ({
453
+ provider: Providers.ANTHROPIC,
454
+ reasoningKey: 'reasoning',
455
+ toolDefinitions: [{ name: 'weather' }],
456
+ graphTools: [],
457
+ agentId:
458
+ metadata?.langgraph_node === 'agent_2' ? 'agent_2' : 'agent_1',
459
+ }) as unknown as AgentContext
402
460
  ),
403
461
  });
404
462
  const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
405
- jest.spyOn(events, 'safeDispatchCustomEvent').mockImplementation(
406
- async (event, data): Promise<void> => {
463
+ jest
464
+ .spyOn(events, 'safeDispatchCustomEvent')
465
+ .mockImplementation(async (event, data): Promise<void> => {
407
466
  if (event !== GraphEvents.ON_TOOL_EXECUTE) {
408
467
  return;
409
468
  }
@@ -416,8 +475,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
416
475
  content: 'ok',
417
476
  }))
418
477
  );
419
- }
420
- );
478
+ });
421
479
 
422
480
  const handler = new ChatModelStreamHandler();
423
481
  await handler.handle(
@@ -463,25 +521,27 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
463
521
  ]);
464
522
  expect(usageByAgent.get('agent_1')?.get('weather')).toBe(1);
465
523
  expect(usageByAgent.get('agent_2')?.get('weather')).toBe(1);
466
- expect(graph.eagerEventToolExecutions.get('call_agent_1_weather')?.request.turn)
467
- .toBe(0);
468
- expect(graph.eagerEventToolExecutions.get('call_agent_2_weather')?.request.turn)
469
- .toBe(0);
524
+ expect(
525
+ graph.eagerEventToolExecutions.get('call_agent_1_weather')?.request.turn
526
+ ).toBe(0);
527
+ expect(
528
+ graph.eagerEventToolExecutions.get('call_agent_2_weather')?.request.turn
529
+ ).toBe(0);
470
530
  });
471
531
 
472
532
  it('skips eager for the whole batch if any call is not request-plannable', async () => {
473
533
  const graph = createGraph();
474
534
  const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
475
- jest.spyOn(events, 'safeDispatchCustomEvent').mockImplementation(
476
- async (event, data): Promise<void> => {
535
+ jest
536
+ .spyOn(events, 'safeDispatchCustomEvent')
537
+ .mockImplementation(async (event, data): Promise<void> => {
477
538
  if (event !== GraphEvents.ON_TOOL_EXECUTE) {
478
539
  return;
479
540
  }
480
541
  const batch = data as t.ToolExecuteBatchRequest;
481
542
  toolExecuteCalls.push(batch);
482
543
  batch.resolve([]);
483
- }
484
- );
544
+ });
485
545
 
486
546
  await new ChatModelStreamHandler().handle(
487
547
  GraphEvents.CHAT_MODEL_STREAM,
@@ -515,8 +575,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
515
575
  it('records complete chunk-only tool calls after creating a tool step', async () => {
516
576
  const graph = createGraph();
517
577
  const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
518
- jest.spyOn(events, 'safeDispatchCustomEvent').mockImplementation(
519
- async (event, data): Promise<void> => {
578
+ jest
579
+ .spyOn(events, 'safeDispatchCustomEvent')
580
+ .mockImplementation(async (event, data): Promise<void> => {
520
581
  if (event !== GraphEvents.ON_TOOL_EXECUTE) {
521
582
  return;
522
583
  }
@@ -529,8 +590,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
529
590
  content: 'sunny',
530
591
  },
531
592
  ]);
532
- }
533
- );
593
+ });
534
594
 
535
595
  await new ChatModelStreamHandler().handle(
536
596
  GraphEvents.CHAT_MODEL_STREAM,
@@ -554,8 +614,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
554
614
  expect(toolExecuteCalls).toHaveLength(0);
555
615
  expect(graph.toolCallStepIds.has('call_weather')).toBe(true);
556
616
  expect(
557
- graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))
558
- ?.argsText
617
+ graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))?.argsText
559
618
  ).toBe('{"city":"NYC"}');
560
619
  });
561
620
 
@@ -572,8 +631,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
572
631
  ) as unknown as StandardGraph['getAgentContext'],
573
632
  });
574
633
  const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
575
- jest.spyOn(events, 'safeDispatchCustomEvent').mockImplementation(
576
- async (event, data): Promise<void> => {
634
+ jest
635
+ .spyOn(events, 'safeDispatchCustomEvent')
636
+ .mockImplementation(async (event, data): Promise<void> => {
577
637
  if (event !== GraphEvents.ON_TOOL_EXECUTE) {
578
638
  return;
579
639
  }
@@ -586,8 +646,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
586
646
  content: 'sunny',
587
647
  },
588
648
  ]);
589
- }
590
- );
649
+ });
591
650
 
592
651
  const handler = new ChatModelStreamHandler();
593
652
  const metadata = { langgraph_node: 'agent' };
@@ -666,9 +725,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
666
725
  stepId: expect.stringMatching(/^step_/),
667
726
  turn: 0,
668
727
  });
669
- expect(graph.eagerEventToolCallChunks.has(chunkStateKey('step-key', 0))).toBe(
670
- false
671
- );
728
+ expect(
729
+ graph.eagerEventToolCallChunks.has(chunkStateKey('step-key', 0))
730
+ ).toBe(false);
672
731
  });
673
732
 
674
733
  it('keeps OpenAI Chat Completions streamed chunks on the final tool_calls path', async () => {
@@ -684,8 +743,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
684
743
  ) as unknown as StandardGraph['getAgentContext'],
685
744
  });
686
745
  const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
687
- jest.spyOn(events, 'safeDispatchCustomEvent').mockImplementation(
688
- async (event, data): Promise<void> => {
746
+ jest
747
+ .spyOn(events, 'safeDispatchCustomEvent')
748
+ .mockImplementation(async (event, data): Promise<void> => {
689
749
  if (event !== GraphEvents.ON_TOOL_EXECUTE) {
690
750
  return;
691
751
  }
@@ -698,8 +758,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
698
758
  content: `ok ${call.name}`,
699
759
  }))
700
760
  );
701
- }
702
- );
761
+ });
703
762
 
704
763
  const handler = new ChatModelStreamHandler();
705
764
  const metadata = { langgraph_node: 'agent' };
@@ -786,8 +845,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
786
845
  it('prestarts final tool calls even when the final chunk also has tool-call chunks', async () => {
787
846
  const graph = createGraph();
788
847
  const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
789
- jest.spyOn(events, 'safeDispatchCustomEvent').mockImplementation(
790
- async (event, data): Promise<void> => {
848
+ jest
849
+ .spyOn(events, 'safeDispatchCustomEvent')
850
+ .mockImplementation(async (event, data): Promise<void> => {
791
851
  if (event !== GraphEvents.ON_TOOL_EXECUTE) {
792
852
  return;
793
853
  }
@@ -800,8 +860,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
800
860
  content: 'sunny',
801
861
  },
802
862
  ]);
803
- }
804
- );
863
+ });
805
864
 
806
865
  await new ChatModelStreamHandler().handle(
807
866
  GraphEvents.CHAT_MODEL_STREAM,
@@ -843,8 +902,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
843
902
  it('waits for final tool calls before prestarting streamed chunk calls', async () => {
844
903
  const graph = createGraph();
845
904
  const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
846
- jest.spyOn(events, 'safeDispatchCustomEvent').mockImplementation(
847
- async (event, data): Promise<void> => {
905
+ jest
906
+ .spyOn(events, 'safeDispatchCustomEvent')
907
+ .mockImplementation(async (event, data): Promise<void> => {
848
908
  if (event !== GraphEvents.ON_TOOL_EXECUTE) {
849
909
  return;
850
910
  }
@@ -857,8 +917,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
857
917
  content: 'sunny',
858
918
  },
859
919
  ]);
860
- }
861
- );
920
+ });
862
921
 
863
922
  const handler = new ChatModelStreamHandler();
864
923
 
@@ -1033,8 +1092,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1033
1092
  it('preserves repeated adjacent argument deltas', async () => {
1034
1093
  const graph = createGraph();
1035
1094
  const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
1036
- jest.spyOn(events, 'safeDispatchCustomEvent').mockImplementation(
1037
- async (event, data): Promise<void> => {
1095
+ jest
1096
+ .spyOn(events, 'safeDispatchCustomEvent')
1097
+ .mockImplementation(async (event, data): Promise<void> => {
1038
1098
  if (event !== GraphEvents.ON_TOOL_EXECUTE) {
1039
1099
  return;
1040
1100
  }
@@ -1047,8 +1107,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1047
1107
  content: 'ok',
1048
1108
  },
1049
1109
  ]);
1050
- }
1051
- );
1110
+ });
1052
1111
 
1053
1112
  const handler = new ChatModelStreamHandler();
1054
1113
  const metadata = { langgraph_node: 'agent' };
@@ -1127,8 +1186,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1127
1186
 
1128
1187
  expect(toolExecuteCalls).toHaveLength(0);
1129
1188
  expect(
1130
- graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))
1131
- ?.argsText
1189
+ graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))?.argsText
1132
1190
  ).toBe('{"word":"book"}');
1133
1191
 
1134
1192
  await handler.handle(
@@ -1202,8 +1260,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1202
1260
  );
1203
1261
 
1204
1262
  expect(
1205
- graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))
1206
- ?.argsText
1263
+ graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))?.argsText
1207
1264
  ).toBe('oo');
1208
1265
  });
1209
1266
 
@@ -1249,16 +1306,16 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1249
1306
  );
1250
1307
 
1251
1308
  expect(
1252
- graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))
1253
- ?.argsText
1309
+ graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))?.argsText
1254
1310
  ).toBe('{"titl');
1255
1311
  });
1256
1312
 
1257
1313
  it('does not prestart from cumulative streamed args before final tool calls', async () => {
1258
1314
  const graph = createGraph();
1259
1315
  const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
1260
- jest.spyOn(events, 'safeDispatchCustomEvent').mockImplementation(
1261
- async (event, data): Promise<void> => {
1316
+ jest
1317
+ .spyOn(events, 'safeDispatchCustomEvent')
1318
+ .mockImplementation(async (event, data): Promise<void> => {
1262
1319
  if (event !== GraphEvents.ON_TOOL_EXECUTE) {
1263
1320
  return;
1264
1321
  }
@@ -1271,8 +1328,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1271
1328
  content: 'sunny',
1272
1329
  },
1273
1330
  ]);
1274
- }
1275
- );
1331
+ });
1276
1332
 
1277
1333
  const handler = new ChatModelStreamHandler();
1278
1334
  const metadata = { langgraph_node: 'agent' };
@@ -1353,8 +1409,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1353
1409
 
1354
1410
  expect(toolExecuteCalls).toHaveLength(0);
1355
1411
  expect(
1356
- graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))
1357
- ?.argsText
1412
+ graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))?.argsText
1358
1413
  ).toBe('{"city":"NYC","unit":"C"}');
1359
1414
 
1360
1415
  await handler.handle(
@@ -1445,8 +1500,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1445
1500
  );
1446
1501
 
1447
1502
  expect(
1448
- graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))
1449
- ?.argsText
1503
+ graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))?.argsText
1450
1504
  ).toBe('{"title":"alpha","city":"NYC"}');
1451
1505
  });
1452
1506
 
@@ -1504,8 +1558,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1504
1558
  );
1505
1559
 
1506
1560
  expect(
1507
- graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))
1508
- ?.argsText
1561
+ graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))?.argsText
1509
1562
  ).toBe('{"word":"book"}');
1510
1563
  });
1511
1564
 
@@ -1590,8 +1643,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1590
1643
  );
1591
1644
 
1592
1645
  expect(
1593
- graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))
1594
- ?.argsText
1646
+ graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))?.argsText
1595
1647
  ).toBe('{"city":"NYC"}');
1596
1648
  });
1597
1649
 
@@ -1599,8 +1651,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1599
1651
  const graphA = createGraph();
1600
1652
  const graphB = createGraph();
1601
1653
  const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
1602
- jest.spyOn(events, 'safeDispatchCustomEvent').mockImplementation(
1603
- async (event, data): Promise<void> => {
1654
+ jest
1655
+ .spyOn(events, 'safeDispatchCustomEvent')
1656
+ .mockImplementation(async (event, data): Promise<void> => {
1604
1657
  if (event !== GraphEvents.ON_TOOL_EXECUTE) {
1605
1658
  return;
1606
1659
  }
@@ -1613,8 +1666,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1613
1666
  content: `${request.id} result`,
1614
1667
  }))
1615
1668
  );
1616
- }
1617
- );
1669
+ });
1618
1670
 
1619
1671
  const handler = new ChatModelStreamHandler();
1620
1672
  const sharedChunk = {
@@ -1660,8 +1712,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1660
1712
  ) as unknown as StandardGraph['getAgentContext'],
1661
1713
  });
1662
1714
  const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
1663
- jest.spyOn(events, 'safeDispatchCustomEvent').mockImplementation(
1664
- async (event, data): Promise<void> => {
1715
+ jest
1716
+ .spyOn(events, 'safeDispatchCustomEvent')
1717
+ .mockImplementation(async (event, data): Promise<void> => {
1665
1718
  if (event !== GraphEvents.ON_TOOL_EXECUTE) {
1666
1719
  return;
1667
1720
  }
@@ -1674,8 +1727,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1674
1727
  content: `ok ${call.name}`,
1675
1728
  }))
1676
1729
  );
1677
- }
1678
- );
1730
+ });
1679
1731
 
1680
1732
  const handler = new ChatModelStreamHandler();
1681
1733
  const metadata = { langgraph_node: 'agent' };
@@ -1732,9 +1784,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1732
1784
  ]);
1733
1785
  expect(graph.eagerEventToolExecutions.has('call_weather')).toBe(true);
1734
1786
  expect(graph.eagerEventToolExecutions.has('call_stock')).toBe(false);
1735
- expect(graph.eagerEventToolCallChunks.has(chunkStateKey('step-key', 0))).toBe(
1736
- false
1737
- );
1787
+ expect(
1788
+ graph.eagerEventToolCallChunks.has(chunkStateKey('step-key', 0))
1789
+ ).toBe(false);
1738
1790
  expect(
1739
1791
  graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 1))?.argsText
1740
1792
  ).toBe('{"ticker":"C');
@@ -1793,11 +1845,11 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1793
1845
  expect(graph.eagerEventToolCallChunks.size).toBe(0);
1794
1846
  });
1795
1847
 
1796
- it('does not use next-index sealing for Moonshot streamed tool chunks', async () => {
1848
+ it('emits a completed event when an eager streamed tool result settles', async () => {
1797
1849
  const graph = createGraph({
1798
1850
  getAgentContext: jest.fn(
1799
1851
  (): Partial<AgentContext> => ({
1800
- provider: Providers.MOONSHOT,
1852
+ provider: Providers.ANTHROPIC,
1801
1853
  reasoningKey: 'reasoning',
1802
1854
  toolDefinitions: [{ name: 'weather' }, { name: 'stock' }],
1803
1855
  graphTools: [],
@@ -1805,14 +1857,18 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1805
1857
  })
1806
1858
  ) as unknown as StandardGraph['getAgentContext'],
1807
1859
  });
1808
- const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
1809
- jest.spyOn(events, 'safeDispatchCustomEvent').mockImplementation(
1810
- async (event, data): Promise<void> => {
1860
+ const completedEvents: Array<{ result: t.ToolEndEvent }> = [];
1861
+ jest
1862
+ .spyOn(events, 'safeDispatchCustomEvent')
1863
+ .mockImplementation(async (event, data): Promise<void> => {
1864
+ if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
1865
+ completedEvents.push(data as { result: t.ToolEndEvent });
1866
+ return;
1867
+ }
1811
1868
  if (event !== GraphEvents.ON_TOOL_EXECUTE) {
1812
1869
  return;
1813
1870
  }
1814
1871
  const batch = data as t.ToolExecuteBatchRequest;
1815
- toolExecuteCalls.push(batch);
1816
1872
  batch.resolve(
1817
1873
  batch.toolCalls.map((call) => ({
1818
1874
  toolCallId: call.id,
@@ -1820,8 +1876,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1820
1876
  content: `ok ${call.name}`,
1821
1877
  }))
1822
1878
  );
1823
- }
1824
- );
1879
+ });
1825
1880
 
1826
1881
  const handler = new ChatModelStreamHandler();
1827
1882
  const metadata = { langgraph_node: 'agent' };
@@ -1844,6 +1899,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1844
1899
  metadata,
1845
1900
  graph
1846
1901
  );
1902
+
1847
1903
  await handler.handle(
1848
1904
  GraphEvents.CHAT_MODEL_STREAM,
1849
1905
  {
@@ -1863,50 +1919,111 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1863
1919
  graph
1864
1920
  );
1865
1921
 
1866
- expect(toolExecuteCalls).toHaveLength(0);
1867
- expect(graph.eagerEventToolExecutions.size).toBe(0);
1868
- expect(graph.eagerEventToolCallChunks.size).toBe(0);
1922
+ await graph.eagerEventToolExecutions.get('call_weather')?.promise;
1923
+
1924
+ expect(completedEvents).toHaveLength(1);
1925
+ expect(completedEvents[0].result).toMatchObject({
1926
+ id: expect.stringMatching(/^step_/),
1927
+ type: 'tool_call',
1928
+ eager: true,
1929
+ tool_call: {
1930
+ id: 'call_weather',
1931
+ name: 'weather',
1932
+ args: '{"city":"NYC"}',
1933
+ output: 'ok weather',
1934
+ progress: 1,
1935
+ },
1936
+ });
1937
+ expect(
1938
+ graph.eagerEventToolExecutions.get('call_weather')?.completionDispatched
1939
+ ).toBe(true);
1940
+ });
1941
+
1942
+ it('does not emit a stale eager completion after the active execution is invalidated', async () => {
1943
+ const graph = createGraph({
1944
+ getAgentContext: jest.fn(
1945
+ (): Partial<AgentContext> => ({
1946
+ provider: Providers.ANTHROPIC,
1947
+ reasoningKey: 'reasoning',
1948
+ toolDefinitions: [{ name: 'weather' }, { name: 'stock' }],
1949
+ graphTools: [],
1950
+ agentId: 'agent_1',
1951
+ })
1952
+ ) as unknown as StandardGraph['getAgentContext'],
1953
+ });
1954
+ const completedEvents: Array<{ result: t.ToolEndEvent }> = [];
1955
+ let pendingBatch: t.ToolExecuteBatchRequest | undefined;
1956
+ jest
1957
+ .spyOn(events, 'safeDispatchCustomEvent')
1958
+ .mockImplementation(async (event, data): Promise<void> => {
1959
+ if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
1960
+ completedEvents.push(data as { result: t.ToolEndEvent });
1961
+ return;
1962
+ }
1963
+ if (event === GraphEvents.ON_TOOL_EXECUTE) {
1964
+ pendingBatch = data as t.ToolExecuteBatchRequest;
1965
+ }
1966
+ });
1967
+
1968
+ const handler = new ChatModelStreamHandler();
1969
+ const metadata = { langgraph_node: 'agent' };
1869
1970
 
1870
1971
  await handler.handle(
1871
1972
  GraphEvents.CHAT_MODEL_STREAM,
1872
1973
  {
1873
1974
  chunk: {
1874
1975
  content: '',
1875
- tool_calls: [
1976
+ tool_call_chunks: [
1876
1977
  {
1877
1978
  id: 'call_weather',
1878
1979
  name: 'weather',
1879
- args: { city: 'NYC revised' },
1980
+ args: '{"city":"NYC"}',
1981
+ index: 0,
1880
1982
  },
1983
+ ],
1984
+ } as unknown as t.StreamChunk,
1985
+ },
1986
+ metadata,
1987
+ graph
1988
+ );
1989
+ await handler.handle(
1990
+ GraphEvents.CHAT_MODEL_STREAM,
1991
+ {
1992
+ chunk: {
1993
+ content: '',
1994
+ tool_call_chunks: [
1881
1995
  {
1882
1996
  id: 'call_stock',
1883
1997
  name: 'stock',
1884
- args: { ticker: 'CH' },
1998
+ args: '{"ticker":"C',
1999
+ index: 1,
1885
2000
  },
1886
2001
  ],
1887
- response_metadata: finalToolCallResponseMetadata,
1888
2002
  } as unknown as t.StreamChunk,
1889
2003
  },
1890
2004
  metadata,
1891
2005
  graph
1892
2006
  );
1893
2007
 
1894
- expect(toolExecuteCalls).toHaveLength(1);
1895
- expect(toolExecuteCalls[0].toolCalls).toEqual([
1896
- expect.objectContaining({
1897
- id: 'call_weather',
1898
- name: 'weather',
1899
- args: { city: 'NYC revised' },
1900
- }),
1901
- expect.objectContaining({
1902
- id: 'call_stock',
1903
- name: 'stock',
1904
- args: { ticker: 'CH' },
1905
- }),
2008
+ const staleRecord = graph.eagerEventToolExecutions.get('call_weather');
2009
+ expect(staleRecord).toBeDefined();
2010
+ expect(pendingBatch?.toolCalls).toHaveLength(1);
2011
+
2012
+ graph.eagerEventToolExecutions.delete('call_weather');
2013
+ pendingBatch?.resolve([
2014
+ {
2015
+ toolCallId: 'call_weather',
2016
+ status: 'success',
2017
+ content: 'stale weather',
2018
+ },
1906
2019
  ]);
2020
+ await staleRecord?.promise;
2021
+
2022
+ expect(completedEvents).toHaveLength(0);
2023
+ expect(staleRecord?.completionDispatched).toBeUndefined();
1907
2024
  });
1908
2025
 
1909
- it('does not seal a streamed tool when the same chunk also carries its own index', async () => {
2026
+ it('does not mark eager completion dispatched when event delivery is swallowed', async () => {
1910
2027
  const graph = createGraph({
1911
2028
  getAgentContext: jest.fn(
1912
2029
  (): Partial<AgentContext> => ({
@@ -1918,23 +2035,23 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1918
2035
  })
1919
2036
  ) as unknown as StandardGraph['getAgentContext'],
1920
2037
  });
1921
- const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
1922
- jest.spyOn(events, 'safeDispatchCustomEvent').mockImplementation(
1923
- async (event, data): Promise<void> => {
1924
- if (event !== GraphEvents.ON_TOOL_EXECUTE) {
1925
- return;
2038
+ jest
2039
+ .spyOn(events, 'safeDispatchCustomEvent')
2040
+ .mockImplementation(async (event, data): Promise<boolean | void> => {
2041
+ if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
2042
+ return false;
1926
2043
  }
1927
- const batch = data as t.ToolExecuteBatchRequest;
1928
- toolExecuteCalls.push(batch);
1929
- batch.resolve(
1930
- batch.toolCalls.map((call) => ({
1931
- toolCallId: call.id,
1932
- status: 'success',
1933
- content: `ok ${call.name}`,
1934
- }))
1935
- );
1936
- }
1937
- );
2044
+ if (event === GraphEvents.ON_TOOL_EXECUTE) {
2045
+ const batch = data as t.ToolExecuteBatchRequest;
2046
+ batch.resolve([
2047
+ {
2048
+ toolCallId: 'call_weather',
2049
+ status: 'success',
2050
+ content: 'weather result',
2051
+ },
2052
+ ]);
2053
+ }
2054
+ });
1938
2055
 
1939
2056
  const handler = new ChatModelStreamHandler();
1940
2057
  const metadata = { langgraph_node: 'agent' };
@@ -1957,17 +2074,12 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1957
2074
  metadata,
1958
2075
  graph
1959
2076
  );
1960
-
1961
2077
  await handler.handle(
1962
2078
  GraphEvents.CHAT_MODEL_STREAM,
1963
2079
  {
1964
2080
  chunk: {
1965
2081
  content: '',
1966
2082
  tool_call_chunks: [
1967
- {
1968
- args: '',
1969
- index: 0,
1970
- },
1971
2083
  {
1972
2084
  id: 'call_stock',
1973
2085
  name: 'stock',
@@ -1981,38 +2093,96 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
1981
2093
  graph
1982
2094
  );
1983
2095
 
1984
- expect(toolExecuteCalls).toHaveLength(0);
2096
+ const record = graph.eagerEventToolExecutions.get('call_weather');
2097
+ await record?.promise;
1985
2098
 
1986
- await handler.handle(
1987
- GraphEvents.CHAT_MODEL_STREAM,
1988
- {
1989
- chunk: {
1990
- content: '',
1991
- tool_call_chunks: [
2099
+ expect(record?.completionDispatched).toBeUndefined();
2100
+ });
2101
+
2102
+ it('does not overwrite a completed tool output with later streamed deltas', () => {
2103
+ const { contentParts, aggregateContent } = createContentAggregator();
2104
+
2105
+ aggregateContent({
2106
+ event: GraphEvents.ON_RUN_STEP,
2107
+ data: {
2108
+ id: 'step_weather',
2109
+ type: StepTypes.TOOL_CALLS,
2110
+ index: 0,
2111
+ stepDetails: {
2112
+ type: StepTypes.TOOL_CALLS,
2113
+ tool_calls: [
1992
2114
  {
1993
- args: 'H"}',
1994
- index: 1,
2115
+ id: 'call_weather',
2116
+ name: 'weather',
2117
+ args: {},
1995
2118
  },
1996
2119
  ],
1997
- } as unknown as t.StreamChunk,
1998
- },
1999
- metadata,
2000
- graph
2001
- );
2120
+ },
2121
+ usage: null,
2122
+ } as t.RunStep,
2123
+ });
2124
+ aggregateContent({
2125
+ event: GraphEvents.ON_RUN_STEP_COMPLETED,
2126
+ data: {
2127
+ result: {
2128
+ id: 'step_weather',
2129
+ index: 0,
2130
+ type: 'tool_call',
2131
+ tool_call: {
2132
+ id: 'call_weather',
2133
+ name: 'weather',
2134
+ args: '{"city":"NYC"}',
2135
+ output: 'sunny',
2136
+ progress: 1,
2137
+ } as t.ProcessedToolCall,
2138
+ },
2139
+ } as { result: t.ToolEndEvent },
2140
+ });
2141
+ aggregateContent({
2142
+ event: GraphEvents.ON_RUN_STEP_DELTA,
2143
+ data: {
2144
+ id: 'step_weather',
2145
+ delta: {
2146
+ type: StepTypes.TOOL_CALLS,
2147
+ tool_calls: [
2148
+ {
2149
+ id: 'call_weather',
2150
+ name: 'weather',
2151
+ args: '{"city":"NYC revised"}',
2152
+ },
2153
+ ],
2154
+ },
2155
+ } as t.RunStepDeltaEvent,
2156
+ });
2002
2157
 
2003
- expect(toolExecuteCalls).toHaveLength(1);
2004
- expect(toolExecuteCalls[0].toolCalls[0]).toMatchObject({
2005
- id: 'call_weather',
2006
- name: 'weather',
2007
- args: { city: 'NYC' },
2158
+ expect(contentParts[0]).toMatchObject({
2159
+ type: ContentTypes.TOOL_CALL,
2160
+ tool_call: {
2161
+ id: 'call_weather',
2162
+ name: 'weather',
2163
+ args: '{"city":"NYC"}',
2164
+ output: 'sunny',
2165
+ progress: 1,
2166
+ },
2008
2167
  });
2009
2168
  });
2010
2169
 
2011
- it('preserves same-tool turns across per-call streamed eager starts', async () => {
2012
- const graph = createGraph();
2170
+ it('does not use next-index sealing for Moonshot streamed tool chunks', async () => {
2171
+ const graph = createGraph({
2172
+ getAgentContext: jest.fn(
2173
+ (): Partial<AgentContext> => ({
2174
+ provider: Providers.MOONSHOT,
2175
+ reasoningKey: 'reasoning',
2176
+ toolDefinitions: [{ name: 'weather' }, { name: 'stock' }],
2177
+ graphTools: [],
2178
+ agentId: 'agent_1',
2179
+ })
2180
+ ) as unknown as StandardGraph['getAgentContext'],
2181
+ });
2013
2182
  const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
2014
- jest.spyOn(events, 'safeDispatchCustomEvent').mockImplementation(
2015
- async (event, data): Promise<void> => {
2183
+ jest
2184
+ .spyOn(events, 'safeDispatchCustomEvent')
2185
+ .mockImplementation(async (event, data): Promise<void> => {
2016
2186
  if (event !== GraphEvents.ON_TOOL_EXECUTE) {
2017
2187
  return;
2018
2188
  }
@@ -2022,11 +2192,10 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
2022
2192
  batch.toolCalls.map((call) => ({
2023
2193
  toolCallId: call.id,
2024
2194
  status: 'success',
2025
- content: `ok ${call.args.city}`,
2195
+ content: `ok ${call.name}`,
2026
2196
  }))
2027
2197
  );
2028
- }
2029
- );
2198
+ });
2030
2199
 
2031
2200
  const handler = new ChatModelStreamHandler();
2032
2201
  const metadata = { langgraph_node: 'agent' };
@@ -2038,7 +2207,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
2038
2207
  content: '',
2039
2208
  tool_call_chunks: [
2040
2209
  {
2041
- id: 'call_weather_1',
2210
+ id: 'call_weather',
2042
2211
  name: 'weather',
2043
2212
  args: '{"city":"NYC"}',
2044
2213
  index: 0,
@@ -2049,7 +2218,6 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
2049
2218
  metadata,
2050
2219
  graph
2051
2220
  );
2052
-
2053
2221
  await handler.handle(
2054
2222
  GraphEvents.CHAT_MODEL_STREAM,
2055
2223
  {
@@ -2057,9 +2225,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
2057
2225
  content: '',
2058
2226
  tool_call_chunks: [
2059
2227
  {
2060
- id: 'call_weather_2',
2061
- name: 'weather',
2062
- args: '{"city":"B',
2228
+ id: 'call_stock',
2229
+ name: 'stock',
2230
+ args: '{"ticker":"C',
2063
2231
  index: 1,
2064
2232
  },
2065
2233
  ],
@@ -2069,9 +2237,215 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
2069
2237
  graph
2070
2238
  );
2071
2239
 
2072
- expect(toolExecuteCalls).toHaveLength(1);
2073
- expect(toolExecuteCalls[0].toolCalls[0]).toMatchObject({
2074
- id: 'call_weather_1',
2240
+ expect(toolExecuteCalls).toHaveLength(0);
2241
+ expect(graph.eagerEventToolExecutions.size).toBe(0);
2242
+ expect(graph.eagerEventToolCallChunks.size).toBe(0);
2243
+
2244
+ await handler.handle(
2245
+ GraphEvents.CHAT_MODEL_STREAM,
2246
+ {
2247
+ chunk: {
2248
+ content: '',
2249
+ tool_calls: [
2250
+ {
2251
+ id: 'call_weather',
2252
+ name: 'weather',
2253
+ args: { city: 'NYC revised' },
2254
+ },
2255
+ {
2256
+ id: 'call_stock',
2257
+ name: 'stock',
2258
+ args: { ticker: 'CH' },
2259
+ },
2260
+ ],
2261
+ response_metadata: finalToolCallResponseMetadata,
2262
+ } as unknown as t.StreamChunk,
2263
+ },
2264
+ metadata,
2265
+ graph
2266
+ );
2267
+
2268
+ expect(toolExecuteCalls).toHaveLength(1);
2269
+ expect(toolExecuteCalls[0].toolCalls).toEqual([
2270
+ expect.objectContaining({
2271
+ id: 'call_weather',
2272
+ name: 'weather',
2273
+ args: { city: 'NYC revised' },
2274
+ }),
2275
+ expect.objectContaining({
2276
+ id: 'call_stock',
2277
+ name: 'stock',
2278
+ args: { ticker: 'CH' },
2279
+ }),
2280
+ ]);
2281
+ });
2282
+
2283
+ it('does not seal a streamed tool when the same chunk also carries its own index', async () => {
2284
+ const graph = createGraph({
2285
+ getAgentContext: jest.fn(
2286
+ (): Partial<AgentContext> => ({
2287
+ provider: Providers.ANTHROPIC,
2288
+ reasoningKey: 'reasoning',
2289
+ toolDefinitions: [{ name: 'weather' }, { name: 'stock' }],
2290
+ graphTools: [],
2291
+ agentId: 'agent_1',
2292
+ })
2293
+ ) as unknown as StandardGraph['getAgentContext'],
2294
+ });
2295
+ const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
2296
+ jest
2297
+ .spyOn(events, 'safeDispatchCustomEvent')
2298
+ .mockImplementation(async (event, data): Promise<void> => {
2299
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
2300
+ return;
2301
+ }
2302
+ const batch = data as t.ToolExecuteBatchRequest;
2303
+ toolExecuteCalls.push(batch);
2304
+ batch.resolve(
2305
+ batch.toolCalls.map((call) => ({
2306
+ toolCallId: call.id,
2307
+ status: 'success',
2308
+ content: `ok ${call.name}`,
2309
+ }))
2310
+ );
2311
+ });
2312
+
2313
+ const handler = new ChatModelStreamHandler();
2314
+ const metadata = { langgraph_node: 'agent' };
2315
+
2316
+ await handler.handle(
2317
+ GraphEvents.CHAT_MODEL_STREAM,
2318
+ {
2319
+ chunk: {
2320
+ content: '',
2321
+ tool_call_chunks: [
2322
+ {
2323
+ id: 'call_weather',
2324
+ name: 'weather',
2325
+ args: '{"city":"NYC"}',
2326
+ index: 0,
2327
+ },
2328
+ ],
2329
+ } as unknown as t.StreamChunk,
2330
+ },
2331
+ metadata,
2332
+ graph
2333
+ );
2334
+
2335
+ await handler.handle(
2336
+ GraphEvents.CHAT_MODEL_STREAM,
2337
+ {
2338
+ chunk: {
2339
+ content: '',
2340
+ tool_call_chunks: [
2341
+ {
2342
+ args: '',
2343
+ index: 0,
2344
+ },
2345
+ {
2346
+ id: 'call_stock',
2347
+ name: 'stock',
2348
+ args: '{"ticker":"C',
2349
+ index: 1,
2350
+ },
2351
+ ],
2352
+ } as unknown as t.StreamChunk,
2353
+ },
2354
+ metadata,
2355
+ graph
2356
+ );
2357
+
2358
+ expect(toolExecuteCalls).toHaveLength(0);
2359
+
2360
+ await handler.handle(
2361
+ GraphEvents.CHAT_MODEL_STREAM,
2362
+ {
2363
+ chunk: {
2364
+ content: '',
2365
+ tool_call_chunks: [
2366
+ {
2367
+ args: 'H"}',
2368
+ index: 1,
2369
+ },
2370
+ ],
2371
+ } as unknown as t.StreamChunk,
2372
+ },
2373
+ metadata,
2374
+ graph
2375
+ );
2376
+
2377
+ expect(toolExecuteCalls).toHaveLength(1);
2378
+ expect(toolExecuteCalls[0].toolCalls[0]).toMatchObject({
2379
+ id: 'call_weather',
2380
+ name: 'weather',
2381
+ args: { city: 'NYC' },
2382
+ });
2383
+ });
2384
+
2385
+ it('preserves same-tool turns across per-call streamed eager starts', async () => {
2386
+ const graph = createGraph();
2387
+ const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
2388
+ jest
2389
+ .spyOn(events, 'safeDispatchCustomEvent')
2390
+ .mockImplementation(async (event, data): Promise<void> => {
2391
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
2392
+ return;
2393
+ }
2394
+ const batch = data as t.ToolExecuteBatchRequest;
2395
+ toolExecuteCalls.push(batch);
2396
+ batch.resolve(
2397
+ batch.toolCalls.map((call) => ({
2398
+ toolCallId: call.id,
2399
+ status: 'success',
2400
+ content: `ok ${call.args.city}`,
2401
+ }))
2402
+ );
2403
+ });
2404
+
2405
+ const handler = new ChatModelStreamHandler();
2406
+ const metadata = { langgraph_node: 'agent' };
2407
+
2408
+ await handler.handle(
2409
+ GraphEvents.CHAT_MODEL_STREAM,
2410
+ {
2411
+ chunk: {
2412
+ content: '',
2413
+ tool_call_chunks: [
2414
+ {
2415
+ id: 'call_weather_1',
2416
+ name: 'weather',
2417
+ args: '{"city":"NYC"}',
2418
+ index: 0,
2419
+ },
2420
+ ],
2421
+ } as unknown as t.StreamChunk,
2422
+ },
2423
+ metadata,
2424
+ graph
2425
+ );
2426
+
2427
+ await handler.handle(
2428
+ GraphEvents.CHAT_MODEL_STREAM,
2429
+ {
2430
+ chunk: {
2431
+ content: '',
2432
+ tool_call_chunks: [
2433
+ {
2434
+ id: 'call_weather_2',
2435
+ name: 'weather',
2436
+ args: '{"city":"B',
2437
+ index: 1,
2438
+ },
2439
+ ],
2440
+ } as unknown as t.StreamChunk,
2441
+ },
2442
+ metadata,
2443
+ graph
2444
+ );
2445
+
2446
+ expect(toolExecuteCalls).toHaveLength(1);
2447
+ expect(toolExecuteCalls[0].toolCalls[0]).toMatchObject({
2448
+ id: 'call_weather_1',
2075
2449
  name: 'weather',
2076
2450
  args: { city: 'NYC' },
2077
2451
  turn: 0,
@@ -2135,8 +2509,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
2135
2509
  ),
2136
2510
  });
2137
2511
  const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
2138
- jest.spyOn(events, 'safeDispatchCustomEvent').mockImplementation(
2139
- async (event, data): Promise<void> => {
2512
+ jest
2513
+ .spyOn(events, 'safeDispatchCustomEvent')
2514
+ .mockImplementation(async (event, data): Promise<void> => {
2140
2515
  if (event !== GraphEvents.ON_TOOL_EXECUTE) {
2141
2516
  return;
2142
2517
  }
@@ -2149,8 +2524,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
2149
2524
  content: `ok ${call.name}`,
2150
2525
  }))
2151
2526
  );
2152
- }
2153
- );
2527
+ });
2154
2528
 
2155
2529
  const handler = new ChatModelStreamHandler();
2156
2530
 
@@ -2255,9 +2629,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
2255
2629
  graph
2256
2630
  );
2257
2631
 
2258
- expect(graph.eagerEventToolCallChunks.has(chunkStateKey('agent_b', 0))).toBe(
2259
- false
2260
- );
2632
+ expect(
2633
+ graph.eagerEventToolCallChunks.has(chunkStateKey('agent_b', 0))
2634
+ ).toBe(false);
2261
2635
  expect(
2262
2636
  graph.eagerEventToolCallChunks.get(chunkStateKey('agent_a', 0))?.argsText
2263
2637
  ).toBe('{"city":"NYC"}');
@@ -2296,7 +2670,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
2296
2670
  });
2297
2671
 
2298
2672
  it('does not prestart when batch-sensitive hooks are configured', async () => {
2299
- const graph = createGraph({ hookRegistry: {} as StandardGraph['hookRegistry'] });
2673
+ const graph = createGraph({
2674
+ hookRegistry: {} as StandardGraph['hookRegistry'],
2675
+ });
2300
2676
  const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
2301
2677
 
2302
2678
  await new ChatModelStreamHandler().handle(
@@ -2412,7 +2788,10 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
2412
2788
  (): Partial<AgentContext> => ({
2413
2789
  provider: Providers.OPENAI,
2414
2790
  reasoningKey: 'reasoning_content',
2415
- toolDefinitions: [{ name: Constants.EXECUTE_CODE }, { name: 'weather' }],
2791
+ toolDefinitions: [
2792
+ { name: Constants.EXECUTE_CODE },
2793
+ { name: 'weather' },
2794
+ ],
2416
2795
  graphTools: [],
2417
2796
  agentId: 'agent_1',
2418
2797
  })
@@ -2468,53 +2847,526 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
2468
2847
  expect(graph.eagerEventToolCallChunks.size).toBe(0);
2469
2848
  });
2470
2849
 
2471
- it('does not prestart event tools in a mixed direct-tool batch', async () => {
2850
+ it('prestarts streamed remote bash tools when the next Anthropic tool call begins', async () => {
2472
2851
  const graph = createGraph({
2473
- toolExecution: {
2474
- engine: 'local',
2475
- } as StandardGraph['toolExecution'],
2476
2852
  getAgentContext: jest.fn(
2477
2853
  (): Partial<AgentContext> => ({
2478
- provider: Providers.OPENAI,
2479
- reasoningKey: 'reasoning_content',
2854
+ provider: Providers.ANTHROPIC,
2855
+ reasoningKey: 'reasoning',
2480
2856
  toolDefinitions: [
2481
- { name: Constants.EXECUTE_CODE },
2482
- { name: 'weather' },
2857
+ { name: Constants.BASH_TOOL },
2858
+ { name: Constants.READ_FILE },
2483
2859
  ],
2484
2860
  graphTools: [],
2485
2861
  agentId: 'agent_1',
2486
2862
  })
2487
2863
  ) as unknown as StandardGraph['getAgentContext'],
2488
2864
  });
2489
- const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
2865
+ const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
2866
+ jest
2867
+ .spyOn(events, 'safeDispatchCustomEvent')
2868
+ .mockImplementation(async (event, data): Promise<void> => {
2869
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
2870
+ return;
2871
+ }
2872
+ const batch = data as t.ToolExecuteBatchRequest;
2873
+ toolExecuteCalls.push(batch);
2874
+ batch.resolve(
2875
+ batch.toolCalls.map((call) => ({
2876
+ toolCallId: call.id,
2877
+ status: 'success',
2878
+ content: `ok ${call.name}`,
2879
+ }))
2880
+ );
2881
+ });
2490
2882
 
2491
- await new ChatModelStreamHandler().handle(
2883
+ const handler = new ChatModelStreamHandler();
2884
+ const metadata = { langgraph_node: 'agent' };
2885
+
2886
+ await handler.handle(
2492
2887
  GraphEvents.CHAT_MODEL_STREAM,
2493
2888
  {
2494
2889
  chunk: {
2495
2890
  content: '',
2496
- tool_calls: [
2497
- {
2498
- id: 'call_code',
2499
- name: Constants.EXECUTE_CODE,
2500
- args: { code: 'print(1)' },
2501
- },
2891
+ tool_call_chunks: [
2502
2892
  {
2503
- id: 'call_weather',
2504
- name: 'weather',
2505
- args: { city: 'NYC' },
2893
+ id: 'toolu_env',
2894
+ name: Constants.BASH_TOOL,
2895
+ args: '{"command":"echo env"}',
2896
+ index: 2,
2506
2897
  },
2507
2898
  ],
2508
- response_metadata: finalToolCallResponseMetadata,
2509
2899
  } as unknown as t.StreamChunk,
2510
2900
  },
2511
- { langgraph_node: 'agent' },
2901
+ metadata,
2512
2902
  graph
2513
2903
  );
2514
2904
 
2515
- expect(dispatchSpy).not.toHaveBeenCalledWith(
2516
- GraphEvents.ON_TOOL_EXECUTE,
2517
- expect.anything(),
2905
+ expect(toolExecuteCalls).toHaveLength(0);
2906
+
2907
+ await handler.handle(
2908
+ GraphEvents.CHAT_MODEL_STREAM,
2909
+ {
2910
+ chunk: {
2911
+ content: '',
2912
+ tool_call_chunks: [
2913
+ {
2914
+ id: 'toolu_net',
2915
+ name: Constants.BASH_TOOL,
2916
+ args: '{"command":"echo net"}',
2917
+ index: 3,
2918
+ },
2919
+ ],
2920
+ } as unknown as t.StreamChunk,
2921
+ },
2922
+ metadata,
2923
+ graph
2924
+ );
2925
+
2926
+ expect(toolExecuteCalls).toHaveLength(1);
2927
+ expect(toolExecuteCalls[0].toolCalls).toEqual([
2928
+ expect.objectContaining({
2929
+ id: 'toolu_env',
2930
+ name: Constants.BASH_TOOL,
2931
+ args: { command: 'echo env' },
2932
+ stepId: expect.stringMatching(/^step_/),
2933
+ turn: 0,
2934
+ }),
2935
+ ]);
2936
+ expect(graph.eagerEventToolExecutions.has('toolu_env')).toBe(true);
2937
+ expect(graph.eagerEventToolExecutions.has('toolu_net')).toBe(false);
2938
+ });
2939
+
2940
+ it('does not prestart streamed remote tools when graph tools may appear later', async () => {
2941
+ const graph = createGraph({
2942
+ getAgentContext: jest.fn(
2943
+ (): Partial<AgentContext> => ({
2944
+ provider: Providers.ANTHROPIC,
2945
+ reasoningKey: 'reasoning',
2946
+ toolDefinitions: [
2947
+ { name: Constants.BASH_TOOL },
2948
+ { name: Constants.READ_FILE },
2949
+ ],
2950
+ graphTools: [
2951
+ { name: 'transfer_to_researcher' } as unknown as t.GenericTool,
2952
+ ],
2953
+ agentId: 'agent_1',
2954
+ })
2955
+ ) as unknown as StandardGraph['getAgentContext'],
2956
+ });
2957
+ const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
2958
+ jest
2959
+ .spyOn(events, 'safeDispatchCustomEvent')
2960
+ .mockImplementation(async (event, data): Promise<void> => {
2961
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
2962
+ return;
2963
+ }
2964
+ const batch = data as t.ToolExecuteBatchRequest;
2965
+ toolExecuteCalls.push(batch);
2966
+ batch.resolve(
2967
+ batch.toolCalls.map((call) => ({
2968
+ toolCallId: call.id,
2969
+ status: 'success',
2970
+ content: `ok ${call.name}`,
2971
+ }))
2972
+ );
2973
+ });
2974
+
2975
+ const handler = new ChatModelStreamHandler();
2976
+ const metadata = { langgraph_node: 'agent' };
2977
+
2978
+ await handler.handle(
2979
+ GraphEvents.CHAT_MODEL_STREAM,
2980
+ {
2981
+ chunk: {
2982
+ content: '',
2983
+ tool_call_chunks: [
2984
+ {
2985
+ id: 'toolu_env',
2986
+ name: Constants.BASH_TOOL,
2987
+ args: '{"command":"echo env"}',
2988
+ index: 2,
2989
+ },
2990
+ ],
2991
+ } as unknown as t.StreamChunk,
2992
+ },
2993
+ metadata,
2994
+ graph
2995
+ );
2996
+
2997
+ await handler.handle(
2998
+ GraphEvents.CHAT_MODEL_STREAM,
2999
+ {
3000
+ chunk: {
3001
+ content: '',
3002
+ tool_call_chunks: [
3003
+ {
3004
+ id: 'toolu_net',
3005
+ name: Constants.BASH_TOOL,
3006
+ args: '{"command":"echo net"}',
3007
+ index: 3,
3008
+ },
3009
+ ],
3010
+ } as unknown as t.StreamChunk,
3011
+ },
3012
+ metadata,
3013
+ graph
3014
+ );
3015
+
3016
+ expect(toolExecuteCalls).toHaveLength(0);
3017
+ expect(graph.eagerEventToolExecutions.size).toBe(0);
3018
+ expect(graph.eagerEventToolCallChunks.size).toBe(0);
3019
+ });
3020
+
3021
+ it('prestarts streamed remote bash tools when code output references are enabled', async () => {
3022
+ const graph = createGraph({
3023
+ toolOutputReferences: { enabled: true },
3024
+ getAgentContext: jest.fn(
3025
+ (): Partial<AgentContext> => ({
3026
+ provider: Providers.ANTHROPIC,
3027
+ reasoningKey: 'reasoning',
3028
+ toolDefinitions: [
3029
+ { name: Constants.BASH_TOOL },
3030
+ { name: Constants.READ_FILE },
3031
+ ],
3032
+ graphTools: [],
3033
+ agentId: 'agent_1',
3034
+ })
3035
+ ) as unknown as StandardGraph['getAgentContext'],
3036
+ });
3037
+ const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
3038
+ jest
3039
+ .spyOn(events, 'safeDispatchCustomEvent')
3040
+ .mockImplementation(async (event, data): Promise<void> => {
3041
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
3042
+ return;
3043
+ }
3044
+ const batch = data as t.ToolExecuteBatchRequest;
3045
+ toolExecuteCalls.push(batch);
3046
+ batch.resolve(
3047
+ batch.toolCalls.map((call) => ({
3048
+ toolCallId: call.id,
3049
+ status: 'success',
3050
+ content: `ok ${call.name}`,
3051
+ }))
3052
+ );
3053
+ });
3054
+
3055
+ const handler = new ChatModelStreamHandler();
3056
+ const metadata = { langgraph_node: 'agent' };
3057
+
3058
+ for (const args of ['{"command":"echo ', 'env && ', 'pwd"}']) {
3059
+ await handler.handle(
3060
+ GraphEvents.CHAT_MODEL_STREAM,
3061
+ {
3062
+ chunk: {
3063
+ content: '',
3064
+ tool_call_chunks: [
3065
+ {
3066
+ id: 'toolu_env',
3067
+ name: Constants.BASH_TOOL,
3068
+ args,
3069
+ index: 2,
3070
+ },
3071
+ ],
3072
+ } as unknown as t.StreamChunk,
3073
+ },
3074
+ metadata,
3075
+ graph
3076
+ );
3077
+ }
3078
+
3079
+ expect(toolExecuteCalls).toHaveLength(0);
3080
+
3081
+ await handler.handle(
3082
+ GraphEvents.CHAT_MODEL_STREAM,
3083
+ {
3084
+ chunk: {
3085
+ content: '',
3086
+ tool_call_chunks: [
3087
+ {
3088
+ id: 'toolu_net',
3089
+ name: Constants.BASH_TOOL,
3090
+ args: '{"command":"echo net"}',
3091
+ index: 3,
3092
+ },
3093
+ ],
3094
+ } as unknown as t.StreamChunk,
3095
+ },
3096
+ metadata,
3097
+ graph
3098
+ );
3099
+
3100
+ expect(toolExecuteCalls).toHaveLength(1);
3101
+ expect(toolExecuteCalls[0].toolCalls).toEqual([
3102
+ expect.objectContaining({
3103
+ id: 'toolu_env',
3104
+ name: Constants.BASH_TOOL,
3105
+ args: { command: 'echo env && pwd' },
3106
+ }),
3107
+ ]);
3108
+ });
3109
+
3110
+ it('does not prestart streamed code tools whose args contain output references', async () => {
3111
+ const graph = createGraph({
3112
+ toolOutputReferences: { enabled: true },
3113
+ getAgentContext: jest.fn(
3114
+ (): Partial<AgentContext> => ({
3115
+ provider: Providers.ANTHROPIC,
3116
+ reasoningKey: 'reasoning',
3117
+ toolDefinitions: [
3118
+ { name: Constants.BASH_TOOL },
3119
+ { name: Constants.READ_FILE },
3120
+ ],
3121
+ graphTools: [],
3122
+ agentId: 'agent_1',
3123
+ })
3124
+ ) as unknown as StandardGraph['getAgentContext'],
3125
+ });
3126
+ const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
3127
+ const handler = new ChatModelStreamHandler();
3128
+ const metadata = { langgraph_node: 'agent' };
3129
+
3130
+ await handler.handle(
3131
+ GraphEvents.CHAT_MODEL_STREAM,
3132
+ {
3133
+ chunk: {
3134
+ content: '',
3135
+ tool_call_chunks: [
3136
+ {
3137
+ id: 'toolu_ref',
3138
+ name: Constants.BASH_TOOL,
3139
+ args: '{"command":"cat <<EOF\\n{{tool0turn0}}\\nEOF"}',
3140
+ index: 2,
3141
+ },
3142
+ ],
3143
+ } as unknown as t.StreamChunk,
3144
+ },
3145
+ metadata,
3146
+ graph
3147
+ );
3148
+ await handler.handle(
3149
+ GraphEvents.CHAT_MODEL_STREAM,
3150
+ {
3151
+ chunk: {
3152
+ content: '',
3153
+ tool_call_chunks: [
3154
+ {
3155
+ id: 'toolu_next',
3156
+ name: Constants.BASH_TOOL,
3157
+ args: '{"command":"echo next"}',
3158
+ index: 3,
3159
+ },
3160
+ ],
3161
+ } as unknown as t.StreamChunk,
3162
+ },
3163
+ metadata,
3164
+ graph
3165
+ );
3166
+
3167
+ expect(dispatchSpy).not.toHaveBeenCalledWith(
3168
+ GraphEvents.ON_TOOL_EXECUTE,
3169
+ expect.anything(),
3170
+ expect.anything()
3171
+ );
3172
+ });
3173
+
3174
+ it('does not prestart streamed tools when the next Anthropic tool call is a graph tool', async () => {
3175
+ const handoffToolName = `${Constants.LC_TRANSFER_TO_}researcher`;
3176
+ const graph = createGraph({
3177
+ getAgentContext: jest.fn(
3178
+ (): Partial<AgentContext> => ({
3179
+ provider: Providers.ANTHROPIC,
3180
+ reasoningKey: 'reasoning',
3181
+ toolDefinitions: [
3182
+ { name: Constants.BASH_TOOL },
3183
+ { name: handoffToolName },
3184
+ ],
3185
+ graphTools: [{ name: handoffToolName } as unknown as t.GenericTool],
3186
+ agentId: 'agent_1',
3187
+ })
3188
+ ) as unknown as StandardGraph['getAgentContext'],
3189
+ });
3190
+ const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
3191
+ const handler = new ChatModelStreamHandler();
3192
+ const metadata = { langgraph_node: 'agent' };
3193
+
3194
+ await handler.handle(
3195
+ GraphEvents.CHAT_MODEL_STREAM,
3196
+ {
3197
+ chunk: {
3198
+ content: '',
3199
+ tool_call_chunks: [
3200
+ {
3201
+ id: 'toolu_env',
3202
+ name: Constants.BASH_TOOL,
3203
+ args: '{"command":"echo env"}',
3204
+ index: 2,
3205
+ },
3206
+ ],
3207
+ } as unknown as t.StreamChunk,
3208
+ },
3209
+ metadata,
3210
+ graph
3211
+ );
3212
+
3213
+ await handler.handle(
3214
+ GraphEvents.CHAT_MODEL_STREAM,
3215
+ {
3216
+ chunk: {
3217
+ content: '',
3218
+ tool_call_chunks: [
3219
+ {
3220
+ id: 'toolu_handoff',
3221
+ name: handoffToolName,
3222
+ args: '{"message":"check this"}',
3223
+ index: 3,
3224
+ },
3225
+ ],
3226
+ } as unknown as t.StreamChunk,
3227
+ },
3228
+ metadata,
3229
+ graph
3230
+ );
3231
+
3232
+ expect(dispatchSpy).not.toHaveBeenCalledWith(
3233
+ GraphEvents.ON_TOOL_EXECUTE,
3234
+ expect.anything(),
3235
+ expect.anything()
3236
+ );
3237
+ expect(graph.eagerEventToolExecutions.size).toBe(0);
3238
+ });
3239
+
3240
+ it('does not prestart streamed tools after a graph tool appeared earlier in the same step', async () => {
3241
+ const handoffToolName = `${Constants.LC_TRANSFER_TO_}researcher`;
3242
+ const graph = createGraph({
3243
+ getAgentContext: jest.fn(
3244
+ (): Partial<AgentContext> => ({
3245
+ provider: Providers.ANTHROPIC,
3246
+ reasoningKey: 'reasoning',
3247
+ toolDefinitions: [
3248
+ { name: Constants.BASH_TOOL },
3249
+ { name: handoffToolName },
3250
+ ],
3251
+ graphTools: [{ name: handoffToolName } as unknown as t.GenericTool],
3252
+ agentId: 'agent_1',
3253
+ })
3254
+ ) as unknown as StandardGraph['getAgentContext'],
3255
+ });
3256
+ const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
3257
+ const handler = new ChatModelStreamHandler();
3258
+ const metadata = { langgraph_node: 'agent' };
3259
+
3260
+ await handler.handle(
3261
+ GraphEvents.CHAT_MODEL_STREAM,
3262
+ {
3263
+ chunk: {
3264
+ content: '',
3265
+ tool_call_chunks: [
3266
+ {
3267
+ id: 'toolu_env',
3268
+ name: Constants.BASH_TOOL,
3269
+ args: '{"command":"echo env"}',
3270
+ index: 2,
3271
+ },
3272
+ ],
3273
+ } as unknown as t.StreamChunk,
3274
+ },
3275
+ metadata,
3276
+ graph
3277
+ );
3278
+ await handler.handle(
3279
+ GraphEvents.CHAT_MODEL_STREAM,
3280
+ {
3281
+ chunk: {
3282
+ content: '',
3283
+ tool_call_chunks: [
3284
+ {
3285
+ id: 'toolu_handoff',
3286
+ name: handoffToolName,
3287
+ args: '{"message":"partial',
3288
+ index: 3,
3289
+ },
3290
+ ],
3291
+ } as unknown as t.StreamChunk,
3292
+ },
3293
+ metadata,
3294
+ graph
3295
+ );
3296
+ await handler.handle(
3297
+ GraphEvents.CHAT_MODEL_STREAM,
3298
+ {
3299
+ chunk: {
3300
+ content: '',
3301
+ tool_call_chunks: [
3302
+ {
3303
+ id: 'toolu_next',
3304
+ name: Constants.BASH_TOOL,
3305
+ args: '{"command":"echo next"}',
3306
+ index: 4,
3307
+ },
3308
+ ],
3309
+ } as unknown as t.StreamChunk,
3310
+ },
3311
+ metadata,
3312
+ graph
3313
+ );
3314
+
3315
+ expect(dispatchSpy).not.toHaveBeenCalledWith(
3316
+ GraphEvents.ON_TOOL_EXECUTE,
3317
+ expect.anything(),
3318
+ expect.anything()
3319
+ );
3320
+ expect(graph.eagerEventToolExecutions.size).toBe(0);
3321
+ });
3322
+
3323
+ it('does not prestart event tools in a mixed direct-tool batch', async () => {
3324
+ const graph = createGraph({
3325
+ toolExecution: {
3326
+ engine: 'local',
3327
+ } as StandardGraph['toolExecution'],
3328
+ getAgentContext: jest.fn(
3329
+ (): Partial<AgentContext> => ({
3330
+ provider: Providers.OPENAI,
3331
+ reasoningKey: 'reasoning_content',
3332
+ toolDefinitions: [
3333
+ { name: Constants.EXECUTE_CODE },
3334
+ { name: 'weather' },
3335
+ ],
3336
+ graphTools: [],
3337
+ agentId: 'agent_1',
3338
+ })
3339
+ ) as unknown as StandardGraph['getAgentContext'],
3340
+ });
3341
+ const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
3342
+
3343
+ await new ChatModelStreamHandler().handle(
3344
+ GraphEvents.CHAT_MODEL_STREAM,
3345
+ {
3346
+ chunk: {
3347
+ content: '',
3348
+ tool_calls: [
3349
+ {
3350
+ id: 'call_code',
3351
+ name: Constants.EXECUTE_CODE,
3352
+ args: { code: 'print(1)' },
3353
+ },
3354
+ {
3355
+ id: 'call_weather',
3356
+ name: 'weather',
3357
+ args: { city: 'NYC' },
3358
+ },
3359
+ ],
3360
+ response_metadata: finalToolCallResponseMetadata,
3361
+ } as unknown as t.StreamChunk,
3362
+ },
3363
+ { langgraph_node: 'agent' },
3364
+ graph
3365
+ );
3366
+
3367
+ expect(dispatchSpy).not.toHaveBeenCalledWith(
3368
+ GraphEvents.ON_TOOL_EXECUTE,
3369
+ expect.anything(),
2518
3370
  expect.anything()
2519
3371
  );
2520
3372
  expect(graph.eagerEventToolExecutions.size).toBe(0);
@@ -2524,8 +3376,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
2524
3376
  const graph = createGraph();
2525
3377
  graph.eagerEventToolUsageCount.set('weather', 1);
2526
3378
  const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
2527
- jest.spyOn(events, 'safeDispatchCustomEvent').mockImplementation(
2528
- async (event, data): Promise<void> => {
3379
+ jest
3380
+ .spyOn(events, 'safeDispatchCustomEvent')
3381
+ .mockImplementation(async (event, data): Promise<void> => {
2529
3382
  if (event !== GraphEvents.ON_TOOL_EXECUTE) {
2530
3383
  return;
2531
3384
  }
@@ -2538,8 +3391,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
2538
3391
  content: 'sunny',
2539
3392
  },
2540
3393
  ]);
2541
- }
2542
- );
3394
+ });
2543
3395
 
2544
3396
  await new ChatModelStreamHandler().handle(
2545
3397
  GraphEvents.CHAT_MODEL_STREAM,