@librechat/agents 3.2.33 → 3.2.35

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 (133) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +47 -10
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +13 -0
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +121 -3
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/llm/bedrock/index.cjs +21 -2
  8. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  9. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs +38 -2
  10. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs.map +1 -1
  11. package/dist/cjs/llm/google/utils/common.cjs +6 -0
  12. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  13. package/dist/cjs/llm/invoke.cjs +49 -8
  14. package/dist/cjs/llm/invoke.cjs.map +1 -1
  15. package/dist/cjs/llm/openai/index.cjs +48 -1
  16. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  17. package/dist/cjs/llm/vertexai/index.cjs +19 -0
  18. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  19. package/dist/cjs/main.cjs +2 -0
  20. package/dist/cjs/messages/content.cjs +12 -14
  21. package/dist/cjs/messages/content.cjs.map +1 -1
  22. package/dist/cjs/messages/prune.cjs +31 -13
  23. package/dist/cjs/messages/prune.cjs.map +1 -1
  24. package/dist/cjs/run.cjs +7 -2
  25. package/dist/cjs/run.cjs.map +1 -1
  26. package/dist/cjs/stream.cjs +20 -2
  27. package/dist/cjs/stream.cjs.map +1 -1
  28. package/dist/cjs/summarization/node.cjs +12 -1
  29. package/dist/cjs/summarization/node.cjs.map +1 -1
  30. package/dist/cjs/tools/ToolNode.cjs +41 -4
  31. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  32. package/dist/cjs/tools/streamedToolCallSeals.cjs +30 -1
  33. package/dist/cjs/tools/streamedToolCallSeals.cjs.map +1 -1
  34. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +138 -2
  35. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  36. package/dist/cjs/utils/tokens.cjs +30 -0
  37. package/dist/cjs/utils/tokens.cjs.map +1 -1
  38. package/dist/esm/agents/AgentContext.mjs +47 -10
  39. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  40. package/dist/esm/common/enum.mjs +13 -0
  41. package/dist/esm/common/enum.mjs.map +1 -1
  42. package/dist/esm/graphs/Graph.mjs +122 -4
  43. package/dist/esm/graphs/Graph.mjs.map +1 -1
  44. package/dist/esm/llm/bedrock/index.mjs +22 -3
  45. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  46. package/dist/esm/llm/bedrock/utils/message_outputs.mjs +38 -3
  47. package/dist/esm/llm/bedrock/utils/message_outputs.mjs.map +1 -1
  48. package/dist/esm/llm/google/utils/common.mjs +6 -0
  49. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  50. package/dist/esm/llm/invoke.mjs +49 -8
  51. package/dist/esm/llm/invoke.mjs.map +1 -1
  52. package/dist/esm/llm/openai/index.mjs +48 -1
  53. package/dist/esm/llm/openai/index.mjs.map +1 -1
  54. package/dist/esm/llm/vertexai/index.mjs +19 -0
  55. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  56. package/dist/esm/main.mjs +3 -3
  57. package/dist/esm/messages/content.mjs +12 -15
  58. package/dist/esm/messages/content.mjs.map +1 -1
  59. package/dist/esm/messages/prune.mjs +31 -13
  60. package/dist/esm/messages/prune.mjs.map +1 -1
  61. package/dist/esm/run.mjs +7 -2
  62. package/dist/esm/run.mjs.map +1 -1
  63. package/dist/esm/stream.mjs +21 -3
  64. package/dist/esm/stream.mjs.map +1 -1
  65. package/dist/esm/summarization/node.mjs +12 -1
  66. package/dist/esm/summarization/node.mjs.map +1 -1
  67. package/dist/esm/tools/ToolNode.mjs +41 -4
  68. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  69. package/dist/esm/tools/streamedToolCallSeals.mjs +25 -2
  70. package/dist/esm/tools/streamedToolCallSeals.mjs.map +1 -1
  71. package/dist/esm/tools/subagent/SubagentExecutor.mjs +138 -2
  72. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  73. package/dist/esm/utils/tokens.mjs +30 -1
  74. package/dist/esm/utils/tokens.mjs.map +1 -1
  75. package/dist/types/agents/AgentContext.d.ts +7 -3
  76. package/dist/types/common/enum.d.ts +13 -0
  77. package/dist/types/graphs/Graph.d.ts +8 -1
  78. package/dist/types/llm/bedrock/utils/index.d.ts +1 -1
  79. package/dist/types/llm/bedrock/utils/message_outputs.d.ts +9 -0
  80. package/dist/types/llm/invoke.d.ts +1 -1
  81. package/dist/types/llm/vertexai/index.d.ts +10 -0
  82. package/dist/types/messages/content.d.ts +5 -0
  83. package/dist/types/messages/prune.d.ts +4 -0
  84. package/dist/types/run.d.ts +1 -0
  85. package/dist/types/tools/ToolNode.d.ts +8 -0
  86. package/dist/types/tools/streamedToolCallSeals.d.ts +5 -1
  87. package/dist/types/tools/subagent/SubagentExecutor.d.ts +11 -1
  88. package/dist/types/types/graph.d.ts +89 -3
  89. package/dist/types/types/run.d.ts +13 -0
  90. package/dist/types/types/tools.d.ts +10 -0
  91. package/dist/types/utils/tokens.d.ts +7 -0
  92. package/package.json +1 -1
  93. package/src/__tests__/stream.eagerEventExecution.test.ts +703 -0
  94. package/src/agents/AgentContext.ts +69 -6
  95. package/src/agents/__tests__/AgentContext.test.ts +6 -2
  96. package/src/common/enum.ts +13 -0
  97. package/src/graphs/Graph.ts +196 -0
  98. package/src/llm/bedrock/index.ts +40 -0
  99. package/src/llm/bedrock/streamSealDispatch.test.ts +158 -0
  100. package/src/llm/bedrock/utils/index.ts +1 -0
  101. package/src/llm/bedrock/utils/message_outputs.test.ts +85 -0
  102. package/src/llm/bedrock/utils/message_outputs.ts +43 -0
  103. package/src/llm/google/utils/common.test.ts +64 -0
  104. package/src/llm/google/utils/common.ts +18 -0
  105. package/src/llm/invoke.test.ts +79 -1
  106. package/src/llm/invoke.ts +58 -4
  107. package/src/llm/openai/index.ts +95 -1
  108. package/src/llm/openai/sequentialToolCallSeals.test.ts +199 -0
  109. package/src/llm/vertexai/index.ts +31 -0
  110. package/src/llm/vertexai/sealStreamedToolCalls.test.ts +88 -0
  111. package/src/llm/vertexai/streamSealDispatch.test.ts +148 -0
  112. package/src/messages/content.ts +24 -32
  113. package/src/messages/prune.ts +39 -2
  114. package/src/run.ts +5 -0
  115. package/src/scripts/subagent-usage-sink.ts +176 -0
  116. package/src/specs/context-accuracy.live.test.ts +409 -0
  117. package/src/specs/context-usage-event.test.ts +117 -0
  118. package/src/specs/context-usage.live.test.ts +297 -0
  119. package/src/specs/prune.test.ts +51 -1
  120. package/src/specs/subagent.test.ts +124 -1
  121. package/src/stream.ts +40 -6
  122. package/src/summarization/__tests__/node.test.ts +60 -1
  123. package/src/summarization/node.ts +20 -1
  124. package/src/tools/ToolNode.ts +85 -3
  125. package/src/tools/__tests__/SubagentExecutor.test.ts +443 -1
  126. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +368 -0
  127. package/src/tools/streamedToolCallSeals.ts +37 -9
  128. package/src/tools/subagent/SubagentExecutor.ts +221 -3
  129. package/src/types/graph.ts +94 -1
  130. package/src/types/run.ts +13 -0
  131. package/src/types/tools.ts +10 -0
  132. package/src/utils/__tests__/apportion.test.ts +32 -0
  133. package/src/utils/tokens.ts +33 -0
@@ -6,6 +6,9 @@ import {
6
6
  STREAMED_TOOL_CALL_SEAL_METADATA_KEY,
7
7
  STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY,
8
8
  OPENAI_RESPONSES_STREAMED_TOOL_CALL_ADAPTER,
9
+ BEDROCK_CONVERSE_STREAMED_TOOL_CALL_ADAPTER,
10
+ GOOGLE_STREAMED_TOOL_CALL_ADAPTER,
11
+ OPENAI_CHAT_SEQUENTIAL_STREAMED_TOOL_CALL_ADAPTER,
9
12
  } from '@/tools/streamedToolCallSeals';
10
13
  import {
11
14
  Constants,
@@ -139,6 +142,18 @@ const openAIResponsesToolCallMetadata = {
139
142
  [STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY]:
140
143
  OPENAI_RESPONSES_STREAMED_TOOL_CALL_ADAPTER,
141
144
  };
145
+ const bedrockConverseToolCallMetadata = {
146
+ [STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY]:
147
+ BEDROCK_CONVERSE_STREAMED_TOOL_CALL_ADAPTER,
148
+ };
149
+ const googleOnArrivalToolCallMetadata = {
150
+ [STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY]: GOOGLE_STREAMED_TOOL_CALL_ADAPTER,
151
+ [STREAMED_TOOL_CALL_SEAL_METADATA_KEY]: { kind: 'all' },
152
+ };
153
+ const openAIChatSequentialToolCallMetadata = {
154
+ [STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY]:
155
+ OPENAI_CHAT_SEQUENTIAL_STREAMED_TOOL_CALL_ADAPTER,
156
+ };
142
157
 
143
158
  describe('ChatModelStreamHandler eager event tool execution', () => {
144
159
  afterEach(() => {
@@ -4023,4 +4038,692 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
4023
4038
  });
4024
4039
  expect(graph.eagerEventToolUsageCount.get('weather')).toBe(2);
4025
4040
  });
4041
+
4042
+ it('prestarts Bedrock Converse streamed tool calls on content block stop seals', async () => {
4043
+ const graph = createGraph({
4044
+ getAgentContext: jest.fn(
4045
+ (): Partial<AgentContext> => ({
4046
+ provider: Providers.BEDROCK,
4047
+ reasoningKey: 'reasoning_content',
4048
+ toolDefinitions: [{ name: 'weather' }],
4049
+ graphTools: [],
4050
+ agentId: 'agent_1',
4051
+ })
4052
+ ) as unknown as StandardGraph['getAgentContext'],
4053
+ });
4054
+ const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
4055
+ jest
4056
+ .spyOn(events, 'safeDispatchCustomEvent')
4057
+ .mockImplementation(async (event, data): Promise<void> => {
4058
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
4059
+ return;
4060
+ }
4061
+ const batch = data as t.ToolExecuteBatchRequest;
4062
+ toolExecuteCalls.push(batch);
4063
+ batch.resolve([
4064
+ {
4065
+ toolCallId: 'call_weather',
4066
+ status: 'success',
4067
+ content: 'sunny',
4068
+ },
4069
+ ]);
4070
+ });
4071
+
4072
+ const handler = new ChatModelStreamHandler();
4073
+ const metadata = { langgraph_node: 'agent' };
4074
+
4075
+ await handler.handle(
4076
+ GraphEvents.CHAT_MODEL_STREAM,
4077
+ {
4078
+ chunk: {
4079
+ content: '',
4080
+ tool_call_chunks: [
4081
+ {
4082
+ id: 'call_weather',
4083
+ name: 'weather',
4084
+ index: 1,
4085
+ },
4086
+ ],
4087
+ response_metadata: bedrockConverseToolCallMetadata,
4088
+ } as unknown as t.StreamChunk,
4089
+ },
4090
+ metadata,
4091
+ graph
4092
+ );
4093
+ await handler.handle(
4094
+ GraphEvents.CHAT_MODEL_STREAM,
4095
+ {
4096
+ chunk: {
4097
+ content: '',
4098
+ tool_call_chunks: [
4099
+ {
4100
+ args: '{"city":"N',
4101
+ index: 1,
4102
+ },
4103
+ ],
4104
+ response_metadata: bedrockConverseToolCallMetadata,
4105
+ } as unknown as t.StreamChunk,
4106
+ },
4107
+ metadata,
4108
+ graph
4109
+ );
4110
+ await handler.handle(
4111
+ GraphEvents.CHAT_MODEL_STREAM,
4112
+ {
4113
+ chunk: {
4114
+ content: '',
4115
+ tool_call_chunks: [
4116
+ {
4117
+ args: 'YC"}',
4118
+ index: 1,
4119
+ },
4120
+ ],
4121
+ response_metadata: bedrockConverseToolCallMetadata,
4122
+ } as unknown as t.StreamChunk,
4123
+ },
4124
+ metadata,
4125
+ graph
4126
+ );
4127
+
4128
+ expect(toolExecuteCalls).toHaveLength(0);
4129
+
4130
+ await handler.handle(
4131
+ GraphEvents.CHAT_MODEL_STREAM,
4132
+ {
4133
+ chunk: {
4134
+ content: '',
4135
+ tool_call_chunks: [
4136
+ {
4137
+ args: '',
4138
+ index: 1,
4139
+ },
4140
+ ],
4141
+ response_metadata: {
4142
+ ...bedrockConverseToolCallMetadata,
4143
+ [STREAMED_TOOL_CALL_SEAL_METADATA_KEY]: {
4144
+ kind: 'single',
4145
+ index: 1,
4146
+ },
4147
+ },
4148
+ } as unknown as t.StreamChunk,
4149
+ },
4150
+ metadata,
4151
+ graph
4152
+ );
4153
+
4154
+ expect(toolExecuteCalls).toHaveLength(1);
4155
+ expect(toolExecuteCalls[0].toolCalls[0]).toMatchObject({
4156
+ id: 'call_weather',
4157
+ name: 'weather',
4158
+ args: { city: 'NYC' },
4159
+ stepId: expect.stringMatching(/^step_/),
4160
+ turn: 0,
4161
+ });
4162
+ expect(
4163
+ graph.eagerEventToolCallChunks.has(chunkStateKey('step-key', 1))
4164
+ ).toBe(false);
4165
+ });
4166
+
4167
+ it('prestarts each Bedrock tool call at its own stop seal in multi-call turns', async () => {
4168
+ const graph = createGraph({
4169
+ getAgentContext: jest.fn(
4170
+ (): Partial<AgentContext> => ({
4171
+ provider: Providers.BEDROCK,
4172
+ reasoningKey: 'reasoning_content',
4173
+ toolDefinitions: [{ name: 'weather' }, { name: 'stock' }],
4174
+ graphTools: [],
4175
+ agentId: 'agent_1',
4176
+ })
4177
+ ) as unknown as StandardGraph['getAgentContext'],
4178
+ });
4179
+ const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
4180
+ jest
4181
+ .spyOn(events, 'safeDispatchCustomEvent')
4182
+ .mockImplementation(async (event, data): Promise<void> => {
4183
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
4184
+ return;
4185
+ }
4186
+ const batch = data as t.ToolExecuteBatchRequest;
4187
+ toolExecuteCalls.push(batch);
4188
+ batch.resolve(
4189
+ batch.toolCalls.map((call) => ({
4190
+ toolCallId: call.id,
4191
+ status: 'success',
4192
+ content: `ok ${call.name}`,
4193
+ }))
4194
+ );
4195
+ });
4196
+
4197
+ const handler = new ChatModelStreamHandler();
4198
+ const metadata = { langgraph_node: 'agent' };
4199
+ const sendChunk = async (chunk: Record<string, unknown>): Promise<void> =>
4200
+ handler.handle(
4201
+ GraphEvents.CHAT_MODEL_STREAM,
4202
+ { chunk: chunk as unknown as t.StreamChunk },
4203
+ metadata,
4204
+ graph
4205
+ );
4206
+
4207
+ await sendChunk({
4208
+ content: '',
4209
+ tool_call_chunks: [{ id: 'call_weather', name: 'weather', index: 1 }],
4210
+ response_metadata: bedrockConverseToolCallMetadata,
4211
+ });
4212
+ await sendChunk({
4213
+ content: '',
4214
+ tool_call_chunks: [{ args: '{"city":"NYC"}', index: 1 }],
4215
+ response_metadata: bedrockConverseToolCallMetadata,
4216
+ });
4217
+ await sendChunk({
4218
+ content: '',
4219
+ tool_call_chunks: [{ args: '', index: 1 }],
4220
+ response_metadata: {
4221
+ ...bedrockConverseToolCallMetadata,
4222
+ [STREAMED_TOOL_CALL_SEAL_METADATA_KEY]: { kind: 'single', index: 1 },
4223
+ },
4224
+ });
4225
+
4226
+ expect(toolExecuteCalls).toHaveLength(1);
4227
+ expect(toolExecuteCalls[0].toolCalls[0]).toMatchObject({
4228
+ id: 'call_weather',
4229
+ args: { city: 'NYC' },
4230
+ });
4231
+
4232
+ await sendChunk({
4233
+ content: '',
4234
+ tool_call_chunks: [{ id: 'call_stock', name: 'stock', index: 2 }],
4235
+ response_metadata: bedrockConverseToolCallMetadata,
4236
+ });
4237
+ await sendChunk({
4238
+ content: '',
4239
+ tool_call_chunks: [{ args: '{"ticker":"CH"}', index: 2 }],
4240
+ response_metadata: bedrockConverseToolCallMetadata,
4241
+ });
4242
+
4243
+ expect(toolExecuteCalls).toHaveLength(1);
4244
+
4245
+ await sendChunk({
4246
+ content: '',
4247
+ tool_call_chunks: [{ args: '', index: 2 }],
4248
+ response_metadata: {
4249
+ ...bedrockConverseToolCallMetadata,
4250
+ [STREAMED_TOOL_CALL_SEAL_METADATA_KEY]: { kind: 'single', index: 2 },
4251
+ },
4252
+ });
4253
+
4254
+ expect(toolExecuteCalls).toHaveLength(2);
4255
+ expect(toolExecuteCalls[1].toolCalls[0]).toMatchObject({
4256
+ id: 'call_stock',
4257
+ args: { ticker: 'CH' },
4258
+ turn: 0,
4259
+ });
4260
+ });
4261
+
4262
+ it('prestarts Google tool calls sealed on arrival without a final tool-call signal', async () => {
4263
+ const graph = createGraph({
4264
+ getAgentContext: jest.fn(
4265
+ (): Partial<AgentContext> => ({
4266
+ provider: Providers.GOOGLE,
4267
+ reasoningKey: 'reasoning',
4268
+ toolDefinitions: [{ name: 'weather' }, { name: 'stock' }],
4269
+ graphTools: [],
4270
+ agentId: 'agent_1',
4271
+ })
4272
+ ) as unknown as StandardGraph['getAgentContext'],
4273
+ });
4274
+ const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
4275
+ jest
4276
+ .spyOn(events, 'safeDispatchCustomEvent')
4277
+ .mockImplementation(async (event, data): Promise<void> => {
4278
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
4279
+ return;
4280
+ }
4281
+ const batch = data as t.ToolExecuteBatchRequest;
4282
+ toolExecuteCalls.push(batch);
4283
+ batch.resolve(
4284
+ batch.toolCalls.map((call) => ({
4285
+ toolCallId: call.id,
4286
+ status: 'success',
4287
+ content: `ok ${call.name}`,
4288
+ }))
4289
+ );
4290
+ });
4291
+
4292
+ const handler = new ChatModelStreamHandler();
4293
+ const metadata = { langgraph_node: 'agent' };
4294
+
4295
+ await handler.handle(
4296
+ GraphEvents.CHAT_MODEL_STREAM,
4297
+ {
4298
+ chunk: {
4299
+ content: '',
4300
+ tool_calls: [
4301
+ {
4302
+ id: 'call_weather',
4303
+ name: 'weather',
4304
+ args: { city: 'NYC' },
4305
+ },
4306
+ ],
4307
+ tool_call_chunks: [
4308
+ {
4309
+ id: 'call_weather',
4310
+ name: 'weather',
4311
+ args: '{"city":"NYC"}',
4312
+ },
4313
+ ],
4314
+ response_metadata: googleOnArrivalToolCallMetadata,
4315
+ } as unknown as t.StreamChunk,
4316
+ },
4317
+ metadata,
4318
+ graph
4319
+ );
4320
+
4321
+ expect(toolExecuteCalls).toHaveLength(1);
4322
+ expect(toolExecuteCalls[0].toolCalls[0]).toMatchObject({
4323
+ id: 'call_weather',
4324
+ name: 'weather',
4325
+ args: { city: 'NYC' },
4326
+ stepId: expect.stringMatching(/^step_/),
4327
+ turn: 0,
4328
+ });
4329
+
4330
+ // A later chunk with another sealed call prestarts independently.
4331
+ await handler.handle(
4332
+ GraphEvents.CHAT_MODEL_STREAM,
4333
+ {
4334
+ chunk: {
4335
+ content: '',
4336
+ tool_calls: [
4337
+ {
4338
+ id: 'call_stock',
4339
+ name: 'stock',
4340
+ args: { ticker: 'CH' },
4341
+ },
4342
+ ],
4343
+ tool_call_chunks: [
4344
+ {
4345
+ id: 'call_stock',
4346
+ name: 'stock',
4347
+ args: '{"ticker":"CH"}',
4348
+ },
4349
+ ],
4350
+ response_metadata: googleOnArrivalToolCallMetadata,
4351
+ } as unknown as t.StreamChunk,
4352
+ },
4353
+ metadata,
4354
+ graph
4355
+ );
4356
+
4357
+ expect(toolExecuteCalls).toHaveLength(2);
4358
+ expect(toolExecuteCalls[1].toolCalls[0]).toMatchObject({
4359
+ id: 'call_stock',
4360
+ name: 'stock',
4361
+ args: { ticker: 'CH' },
4362
+ turn: 0,
4363
+ });
4364
+ expect(graph.eagerEventToolExecutions.has('call_weather')).toBe(true);
4365
+ expect(graph.eagerEventToolExecutions.has('call_stock')).toBe(true);
4366
+ });
4367
+
4368
+ it('keeps Google tool calls lazy without an on-arrival seal', async () => {
4369
+ const graph = createGraph({
4370
+ getAgentContext: jest.fn(
4371
+ (): Partial<AgentContext> => ({
4372
+ provider: Providers.GOOGLE,
4373
+ reasoningKey: 'reasoning',
4374
+ toolDefinitions: [{ name: 'weather' }],
4375
+ graphTools: [],
4376
+ agentId: 'agent_1',
4377
+ })
4378
+ ) as unknown as StandardGraph['getAgentContext'],
4379
+ });
4380
+ const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
4381
+ jest
4382
+ .spyOn(events, 'safeDispatchCustomEvent')
4383
+ .mockImplementation(async (event, data): Promise<void> => {
4384
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
4385
+ return;
4386
+ }
4387
+ toolExecuteCalls.push(data as t.ToolExecuteBatchRequest);
4388
+ });
4389
+
4390
+ await new ChatModelStreamHandler().handle(
4391
+ GraphEvents.CHAT_MODEL_STREAM,
4392
+ {
4393
+ chunk: {
4394
+ content: '',
4395
+ tool_calls: [
4396
+ {
4397
+ id: 'call_weather',
4398
+ name: 'weather',
4399
+ args: { city: 'NYC' },
4400
+ },
4401
+ ],
4402
+ tool_call_chunks: [
4403
+ {
4404
+ id: 'call_weather',
4405
+ name: 'weather',
4406
+ args: '{"city":"NYC"}',
4407
+ },
4408
+ ],
4409
+ } as unknown as t.StreamChunk,
4410
+ },
4411
+ { langgraph_node: 'agent' },
4412
+ graph
4413
+ );
4414
+
4415
+ expect(toolExecuteCalls).toHaveLength(0);
4416
+ expect(graph.eagerEventToolExecutions.size).toBe(0);
4417
+ });
4418
+
4419
+ it('does not prestart on-arrival sealed calls when batch-sensitive hooks are configured', async () => {
4420
+ const graph = createGraph({
4421
+ hookRegistry: {} as StandardGraph['hookRegistry'],
4422
+ });
4423
+ const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
4424
+
4425
+ await new ChatModelStreamHandler().handle(
4426
+ GraphEvents.CHAT_MODEL_STREAM,
4427
+ {
4428
+ chunk: {
4429
+ content: '',
4430
+ tool_calls: [
4431
+ {
4432
+ id: 'call_weather',
4433
+ name: 'weather',
4434
+ args: { city: 'NYC' },
4435
+ },
4436
+ ],
4437
+ tool_call_chunks: [
4438
+ {
4439
+ id: 'call_weather',
4440
+ name: 'weather',
4441
+ args: '{"city":"NYC"}',
4442
+ },
4443
+ ],
4444
+ response_metadata: googleOnArrivalToolCallMetadata,
4445
+ } as unknown as t.StreamChunk,
4446
+ },
4447
+ { langgraph_node: 'agent' },
4448
+ graph
4449
+ );
4450
+
4451
+ expect(dispatchSpy).not.toHaveBeenCalledWith(
4452
+ GraphEvents.ON_TOOL_EXECUTE,
4453
+ expect.anything(),
4454
+ expect.anything()
4455
+ );
4456
+ expect(graph.eagerEventToolExecutions.size).toBe(0);
4457
+ });
4458
+
4459
+ it('does not prestart on-arrival sealed calls for programmatic tool calling batches', async () => {
4460
+ const graph = createGraph();
4461
+ const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
4462
+
4463
+ await new ChatModelStreamHandler().handle(
4464
+ GraphEvents.CHAT_MODEL_STREAM,
4465
+ {
4466
+ chunk: {
4467
+ content: '',
4468
+ tool_calls: [
4469
+ {
4470
+ id: 'call_weather',
4471
+ name: 'weather',
4472
+ args: { city: 'NYC' },
4473
+ },
4474
+ ],
4475
+ tool_call_chunks: [
4476
+ {
4477
+ id: 'call_weather',
4478
+ name: 'weather',
4479
+ args: '{"city":"NYC"}',
4480
+ },
4481
+ ],
4482
+ response_metadata: googleOnArrivalToolCallMetadata,
4483
+ } as unknown as t.StreamChunk,
4484
+ },
4485
+ {
4486
+ langgraph_node: 'agent',
4487
+ [Constants.PROGRAMMATIC_TOOL_CALLING]: true,
4488
+ },
4489
+ graph
4490
+ );
4491
+
4492
+ expect(dispatchSpy).not.toHaveBeenCalledWith(
4493
+ GraphEvents.ON_TOOL_EXECUTE,
4494
+ expect.anything(),
4495
+ expect.anything()
4496
+ );
4497
+ expect(graph.eagerEventToolExecutions.size).toBe(0);
4498
+ });
4499
+
4500
+ it('does not prestart on-arrival sealed calls when eager execution is disabled', async () => {
4501
+ const graph = createGraph({
4502
+ eagerEventToolExecution: { enabled: false },
4503
+ } as Partial<StandardGraph>);
4504
+ const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
4505
+
4506
+ await new ChatModelStreamHandler().handle(
4507
+ GraphEvents.CHAT_MODEL_STREAM,
4508
+ {
4509
+ chunk: {
4510
+ content: '',
4511
+ tool_calls: [
4512
+ {
4513
+ id: 'call_weather',
4514
+ name: 'weather',
4515
+ args: { city: 'NYC' },
4516
+ },
4517
+ ],
4518
+ tool_call_chunks: [
4519
+ {
4520
+ id: 'call_weather',
4521
+ name: 'weather',
4522
+ args: '{"city":"NYC"}',
4523
+ },
4524
+ ],
4525
+ response_metadata: googleOnArrivalToolCallMetadata,
4526
+ } as unknown as t.StreamChunk,
4527
+ },
4528
+ { langgraph_node: 'agent' },
4529
+ graph
4530
+ );
4531
+
4532
+ expect(dispatchSpy).not.toHaveBeenCalledWith(
4533
+ GraphEvents.ON_TOOL_EXECUTE,
4534
+ expect.anything(),
4535
+ expect.anything()
4536
+ );
4537
+ expect(graph.eagerEventToolExecutions.size).toBe(0);
4538
+ });
4539
+
4540
+ it('prestarts official OpenAI sequential streamed tool calls when a later index begins', async () => {
4541
+ const graph = createGraph({
4542
+ getAgentContext: jest.fn(
4543
+ (): Partial<AgentContext> => ({
4544
+ provider: Providers.OPENAI,
4545
+ reasoningKey: 'reasoning_content',
4546
+ toolDefinitions: [{ name: 'weather' }, { name: 'stock' }],
4547
+ graphTools: [],
4548
+ agentId: 'agent_1',
4549
+ })
4550
+ ) as unknown as StandardGraph['getAgentContext'],
4551
+ });
4552
+ const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
4553
+ jest
4554
+ .spyOn(events, 'safeDispatchCustomEvent')
4555
+ .mockImplementation(async (event, data): Promise<void> => {
4556
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
4557
+ return;
4558
+ }
4559
+ const batch = data as t.ToolExecuteBatchRequest;
4560
+ toolExecuteCalls.push(batch);
4561
+ batch.resolve(
4562
+ batch.toolCalls.map((call) => ({
4563
+ toolCallId: call.id,
4564
+ status: 'success',
4565
+ content: `ok ${call.name}`,
4566
+ }))
4567
+ );
4568
+ });
4569
+
4570
+ const handler = new ChatModelStreamHandler();
4571
+ const metadata = { langgraph_node: 'agent' };
4572
+
4573
+ await handler.handle(
4574
+ GraphEvents.CHAT_MODEL_STREAM,
4575
+ {
4576
+ chunk: {
4577
+ content: '',
4578
+ tool_call_chunks: [
4579
+ {
4580
+ id: 'call_weather',
4581
+ name: 'weather',
4582
+ args: '{"city":"NYC"}',
4583
+ index: 0,
4584
+ },
4585
+ ],
4586
+ response_metadata: openAIChatSequentialToolCallMetadata,
4587
+ } as unknown as t.StreamChunk,
4588
+ },
4589
+ metadata,
4590
+ graph
4591
+ );
4592
+
4593
+ expect(toolExecuteCalls).toHaveLength(0);
4594
+
4595
+ await handler.handle(
4596
+ GraphEvents.CHAT_MODEL_STREAM,
4597
+ {
4598
+ chunk: {
4599
+ content: '',
4600
+ tool_call_chunks: [
4601
+ {
4602
+ id: 'call_stock',
4603
+ name: 'stock',
4604
+ index: 1,
4605
+ },
4606
+ ],
4607
+ response_metadata: openAIChatSequentialToolCallMetadata,
4608
+ } as unknown as t.StreamChunk,
4609
+ },
4610
+ metadata,
4611
+ graph
4612
+ );
4613
+
4614
+ expect(toolExecuteCalls).toHaveLength(1);
4615
+ expect(toolExecuteCalls[0].toolCalls[0]).toMatchObject({
4616
+ id: 'call_weather',
4617
+ name: 'weather',
4618
+ args: { city: 'NYC' },
4619
+ stepId: expect.stringMatching(/^step_/),
4620
+ turn: 0,
4621
+ });
4622
+
4623
+ await handler.handle(
4624
+ GraphEvents.CHAT_MODEL_STREAM,
4625
+ {
4626
+ chunk: {
4627
+ content: '',
4628
+ tool_call_chunks: [
4629
+ {
4630
+ args: '{"ticker":"CH"}',
4631
+ index: 1,
4632
+ },
4633
+ ],
4634
+ response_metadata: openAIChatSequentialToolCallMetadata,
4635
+ } as unknown as t.StreamChunk,
4636
+ },
4637
+ metadata,
4638
+ graph
4639
+ );
4640
+
4641
+ expect(toolExecuteCalls).toHaveLength(1);
4642
+
4643
+ // The final cumulative tool_calls chunk dispatches only the unstarted call.
4644
+ await handler.handle(
4645
+ GraphEvents.CHAT_MODEL_STREAM,
4646
+ {
4647
+ chunk: {
4648
+ content: '',
4649
+ tool_calls: [
4650
+ {
4651
+ id: 'call_weather',
4652
+ name: 'weather',
4653
+ args: { city: 'NYC' },
4654
+ },
4655
+ {
4656
+ id: 'call_stock',
4657
+ name: 'stock',
4658
+ args: { ticker: 'CH' },
4659
+ },
4660
+ ],
4661
+ response_metadata: finalToolCallResponseMetadata,
4662
+ } as unknown as t.StreamChunk,
4663
+ },
4664
+ metadata,
4665
+ graph
4666
+ );
4667
+
4668
+ expect(toolExecuteCalls).toHaveLength(2);
4669
+ expect(toolExecuteCalls[1].toolCalls).toHaveLength(1);
4670
+ expect(toolExecuteCalls[1].toolCalls[0]).toMatchObject({
4671
+ id: 'call_stock',
4672
+ name: 'stock',
4673
+ args: { ticker: 'CH' },
4674
+ turn: 0,
4675
+ });
4676
+ });
4677
+
4678
+ it('does not prestart on-arrival sealed calls when direct tools may stream', async () => {
4679
+ const graph = createGraph({
4680
+ getAgentContext: jest.fn(
4681
+ (): Partial<AgentContext> => ({
4682
+ provider: Providers.GOOGLE,
4683
+ reasoningKey: 'reasoning',
4684
+ toolDefinitions: [{ name: 'weather' }],
4685
+ graphTools: [{ name: 'lookup' }],
4686
+ agentId: 'agent_1',
4687
+ })
4688
+ ) as unknown as StandardGraph['getAgentContext'],
4689
+ });
4690
+ const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
4691
+ jest
4692
+ .spyOn(events, 'safeDispatchCustomEvent')
4693
+ .mockImplementation(async (event, data): Promise<void> => {
4694
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
4695
+ return;
4696
+ }
4697
+ toolExecuteCalls.push(data as t.ToolExecuteBatchRequest);
4698
+ });
4699
+
4700
+ await new ChatModelStreamHandler().handle(
4701
+ GraphEvents.CHAT_MODEL_STREAM,
4702
+ {
4703
+ chunk: {
4704
+ content: '',
4705
+ tool_calls: [
4706
+ {
4707
+ id: 'call_weather',
4708
+ name: 'weather',
4709
+ args: { city: 'NYC' },
4710
+ },
4711
+ ],
4712
+ tool_call_chunks: [
4713
+ {
4714
+ id: 'call_weather',
4715
+ name: 'weather',
4716
+ args: '{"city":"NYC"}',
4717
+ },
4718
+ ],
4719
+ response_metadata: googleOnArrivalToolCallMetadata,
4720
+ } as unknown as t.StreamChunk,
4721
+ },
4722
+ { langgraph_node: 'agent' },
4723
+ graph
4724
+ );
4725
+
4726
+ expect(toolExecuteCalls).toHaveLength(0);
4727
+ expect(graph.eagerEventToolExecutions.size).toBe(0);
4728
+ });
4026
4729
  });