@librechat/agents 3.1.85 → 3.1.87

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 (166) hide show
  1. package/README.md +69 -0
  2. package/dist/cjs/agents/AgentContext.cjs +7 -2
  3. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  4. package/dist/cjs/events.cjs +23 -0
  5. package/dist/cjs/events.cjs.map +1 -1
  6. package/dist/cjs/graphs/Graph.cjs +133 -18
  7. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
  9. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  10. package/dist/cjs/llm/anthropic/index.cjs +251 -53
  11. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  12. package/dist/cjs/llm/init.cjs +1 -5
  13. package/dist/cjs/llm/init.cjs.map +1 -1
  14. package/dist/cjs/llm/openai/index.cjs +113 -24
  15. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  16. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  17. package/dist/cjs/llm/openrouter/index.cjs +3 -1
  18. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  19. package/dist/cjs/main.cjs +18 -5
  20. package/dist/cjs/main.cjs.map +1 -1
  21. package/dist/cjs/openai/index.cjs +253 -0
  22. package/dist/cjs/openai/index.cjs.map +1 -0
  23. package/dist/cjs/responses/index.cjs +448 -0
  24. package/dist/cjs/responses/index.cjs.map +1 -0
  25. package/dist/cjs/run.cjs +108 -7
  26. package/dist/cjs/run.cjs.map +1 -1
  27. package/dist/cjs/session/AgentSession.cjs +1057 -0
  28. package/dist/cjs/session/AgentSession.cjs.map +1 -0
  29. package/dist/cjs/session/JsonlSessionStore.cjs +425 -0
  30. package/dist/cjs/session/JsonlSessionStore.cjs.map +1 -0
  31. package/dist/cjs/session/handlers.cjs +221 -0
  32. package/dist/cjs/session/handlers.cjs.map +1 -0
  33. package/dist/cjs/session/ids.cjs +22 -0
  34. package/dist/cjs/session/ids.cjs.map +1 -0
  35. package/dist/cjs/session/messageSerialization.cjs +179 -0
  36. package/dist/cjs/session/messageSerialization.cjs.map +1 -0
  37. package/dist/cjs/stream.cjs +472 -11
  38. package/dist/cjs/stream.cjs.map +1 -1
  39. package/dist/cjs/summarization/node.cjs +1 -1
  40. package/dist/cjs/summarization/node.cjs.map +1 -1
  41. package/dist/cjs/tools/ToolNode.cjs +177 -59
  42. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  43. package/dist/cjs/tools/eagerEventExecution.cjs +113 -0
  44. package/dist/cjs/tools/eagerEventExecution.cjs.map +1 -0
  45. package/dist/cjs/tools/handlers.cjs +1 -1
  46. package/dist/cjs/tools/handlers.cjs.map +1 -1
  47. package/dist/cjs/tools/streamedToolCallSeals.cjs +42 -0
  48. package/dist/cjs/tools/streamedToolCallSeals.cjs.map +1 -0
  49. package/dist/esm/agents/AgentContext.mjs +7 -2
  50. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  51. package/dist/esm/events.mjs +23 -1
  52. package/dist/esm/events.mjs.map +1 -1
  53. package/dist/esm/graphs/Graph.mjs +133 -18
  54. package/dist/esm/graphs/Graph.mjs.map +1 -1
  55. package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
  56. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  57. package/dist/esm/llm/anthropic/index.mjs +251 -53
  58. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  59. package/dist/esm/llm/init.mjs +1 -5
  60. package/dist/esm/llm/init.mjs.map +1 -1
  61. package/dist/esm/llm/openai/index.mjs +113 -25
  62. package/dist/esm/llm/openai/index.mjs.map +1 -1
  63. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  64. package/dist/esm/llm/openrouter/index.mjs +4 -2
  65. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  66. package/dist/esm/main.mjs +5 -1
  67. package/dist/esm/main.mjs.map +1 -1
  68. package/dist/esm/openai/index.mjs +246 -0
  69. package/dist/esm/openai/index.mjs.map +1 -0
  70. package/dist/esm/responses/index.mjs +440 -0
  71. package/dist/esm/responses/index.mjs.map +1 -0
  72. package/dist/esm/run.mjs +108 -7
  73. package/dist/esm/run.mjs.map +1 -1
  74. package/dist/esm/session/AgentSession.mjs +1054 -0
  75. package/dist/esm/session/AgentSession.mjs.map +1 -0
  76. package/dist/esm/session/JsonlSessionStore.mjs +422 -0
  77. package/dist/esm/session/JsonlSessionStore.mjs.map +1 -0
  78. package/dist/esm/session/handlers.mjs +219 -0
  79. package/dist/esm/session/handlers.mjs.map +1 -0
  80. package/dist/esm/session/ids.mjs +17 -0
  81. package/dist/esm/session/ids.mjs.map +1 -0
  82. package/dist/esm/session/messageSerialization.mjs +173 -0
  83. package/dist/esm/session/messageSerialization.mjs.map +1 -0
  84. package/dist/esm/stream.mjs +473 -12
  85. package/dist/esm/stream.mjs.map +1 -1
  86. package/dist/esm/summarization/node.mjs +1 -1
  87. package/dist/esm/summarization/node.mjs.map +1 -1
  88. package/dist/esm/tools/ToolNode.mjs +177 -59
  89. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  90. package/dist/esm/tools/eagerEventExecution.mjs +107 -0
  91. package/dist/esm/tools/eagerEventExecution.mjs.map +1 -0
  92. package/dist/esm/tools/handlers.mjs +1 -1
  93. package/dist/esm/tools/handlers.mjs.map +1 -1
  94. package/dist/esm/tools/streamedToolCallSeals.mjs +36 -0
  95. package/dist/esm/tools/streamedToolCallSeals.mjs.map +1 -0
  96. package/dist/types/events.d.ts +1 -0
  97. package/dist/types/graphs/Graph.d.ts +24 -9
  98. package/dist/types/index.d.ts +1 -0
  99. package/dist/types/llm/openai/index.d.ts +1 -0
  100. package/dist/types/openai/index.d.ts +75 -0
  101. package/dist/types/responses/index.d.ts +97 -0
  102. package/dist/types/run.d.ts +2 -0
  103. package/dist/types/session/AgentSession.d.ts +32 -0
  104. package/dist/types/session/JsonlSessionStore.d.ts +67 -0
  105. package/dist/types/session/handlers.d.ts +8 -0
  106. package/dist/types/session/ids.d.ts +4 -0
  107. package/dist/types/session/index.d.ts +5 -0
  108. package/dist/types/session/messageSerialization.d.ts +7 -0
  109. package/dist/types/session/types.d.ts +191 -0
  110. package/dist/types/tools/ToolNode.d.ts +12 -1
  111. package/dist/types/tools/eagerEventExecution.d.ts +23 -0
  112. package/dist/types/tools/streamedToolCallSeals.d.ts +13 -0
  113. package/dist/types/types/hitl.d.ts +4 -0
  114. package/dist/types/types/run.d.ts +11 -1
  115. package/dist/types/types/tools.d.ts +36 -0
  116. package/package.json +19 -2
  117. package/src/__tests__/stream.eagerEventExecution.test.ts +2458 -0
  118. package/src/agents/AgentContext.ts +7 -2
  119. package/src/agents/__tests__/AgentContext.test.ts +254 -5
  120. package/src/events.ts +29 -0
  121. package/src/graphs/Graph.ts +224 -50
  122. package/src/graphs/MultiAgentGraph.ts +1 -1
  123. package/src/graphs/__tests__/composition.smoke.test.ts +30 -0
  124. package/src/index.ts +3 -0
  125. package/src/llm/anthropic/index.ts +356 -84
  126. package/src/llm/anthropic/llm.spec.ts +64 -0
  127. package/src/llm/custom-chat-models.smoke.test.ts +175 -4
  128. package/src/llm/openai/contentBlocks.test.ts +35 -0
  129. package/src/llm/openai/deepseek.test.ts +201 -2
  130. package/src/llm/openai/index.ts +171 -26
  131. package/src/llm/openai/utils/index.ts +22 -0
  132. package/src/llm/openrouter/index.ts +4 -2
  133. package/src/openai/__tests__/openai.test.ts +337 -0
  134. package/src/openai/index.ts +404 -0
  135. package/src/responses/__tests__/responses.test.ts +652 -0
  136. package/src/responses/index.ts +677 -0
  137. package/src/run.ts +158 -8
  138. package/src/scripts/compare_pi_vs_ours.ts +592 -173
  139. package/src/scripts/session_live.ts +548 -0
  140. package/src/session/AgentSession.ts +1432 -0
  141. package/src/session/JsonlSessionStore.ts +572 -0
  142. package/src/session/__tests__/JsonlSessionStore.test.ts +1410 -0
  143. package/src/session/__tests__/handlers.test.ts +161 -0
  144. package/src/session/handlers.ts +272 -0
  145. package/src/session/ids.ts +17 -0
  146. package/src/session/index.ts +44 -0
  147. package/src/session/messageSerialization.ts +207 -0
  148. package/src/session/types.ts +275 -0
  149. package/src/specs/custom-event-await.test.ts +89 -0
  150. package/src/specs/summarization.test.ts +1 -1
  151. package/src/stream.ts +755 -48
  152. package/src/summarization/node.ts +1 -1
  153. package/src/tools/ToolNode.ts +299 -126
  154. package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +373 -0
  155. package/src/tools/__tests__/handlers.test.ts +2 -1
  156. package/src/tools/__tests__/hitl.test.ts +206 -110
  157. package/src/tools/eagerEventExecution.ts +153 -0
  158. package/src/tools/handlers.ts +8 -4
  159. package/src/tools/streamedToolCallSeals.ts +57 -0
  160. package/src/types/hitl.ts +4 -0
  161. package/src/types/run.ts +11 -0
  162. package/src/types/tools.ts +36 -0
  163. package/dist/cjs/llm/text.cjs +0 -69
  164. package/dist/cjs/llm/text.cjs.map +0 -1
  165. package/dist/esm/llm/text.mjs +0 -67
  166. package/dist/esm/llm/text.mjs.map +0 -1
package/src/stream.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/stream.ts
2
2
  import type { ChatOpenAIReasoningSummary } from '@langchain/openai';
3
3
  import type { AIMessageChunk } from '@langchain/core/messages';
4
- import type { ToolCall } from '@langchain/core/messages/tool';
4
+ import type { ToolCall, ToolCallChunk } from '@langchain/core/messages/tool';
5
5
  import type { AgentContext } from '@/agents/AgentContext';
6
6
  import type { StandardGraph } from '@/graphs';
7
7
  import type * as t from '@/types';
@@ -11,6 +11,9 @@ import {
11
11
  GraphEvents,
12
12
  StepTypes,
13
13
  Providers,
14
+ Constants,
15
+ CODE_EXECUTION_TOOLS,
16
+ LOCAL_CODING_BUNDLE_NAMES,
14
17
  } from '@/common';
15
18
  import {
16
19
  handleServerToolResult,
@@ -18,6 +21,21 @@ import {
18
21
  handleToolCalls,
19
22
  } from '@/tools/handlers';
20
23
  import { getMessageId } from '@/messages';
24
+ import { safeDispatchCustomEvent } from '@/utils/events';
25
+ import {
26
+ buildToolExecutionRequestPlan,
27
+ coerceRecordArgs,
28
+ normalizeError,
29
+ } from '@/tools/eagerEventExecution';
30
+ import {
31
+ getStreamedToolCallSeal,
32
+ getStreamedToolCallAdapter,
33
+ type StreamedToolCallSeal,
34
+ } from '@/tools/streamedToolCallSeals';
35
+
36
+ const LOCAL_CODING_BUNDLE_NAME_SET: ReadonlySet<string> = new Set(
37
+ LOCAL_CODING_BUNDLE_NAMES
38
+ );
21
39
 
22
40
  /**
23
41
  * Parses content to extract thinking sections enclosed in <think> tags using string operations
@@ -79,6 +97,631 @@ function getNonEmptyValue(possibleValues: string[]): string | undefined {
79
97
  return undefined;
80
98
  }
81
99
 
100
+ function isBatchSensitiveToolExecution(graph: StandardGraph): boolean {
101
+ return (
102
+ graph.hookRegistry != null ||
103
+ graph.humanInTheLoop?.enabled === true ||
104
+ graph.toolOutputReferences?.enabled === true
105
+ );
106
+ }
107
+
108
+ function isDirectGraphTool(
109
+ name: string,
110
+ agentContext: AgentContext | undefined
111
+ ): boolean {
112
+ if (name.startsWith(Constants.LC_TRANSFER_TO_)) {
113
+ return true;
114
+ }
115
+ return (
116
+ (agentContext?.graphTools as t.GenericTool[] | undefined)?.some(
117
+ (tool) => 'name' in tool && tool.name === name
118
+ ) === true
119
+ );
120
+ }
121
+
122
+ function isDirectLocalTool(name: string, graph: StandardGraph): boolean {
123
+ if (graph.toolExecution?.engine !== 'local') {
124
+ return false;
125
+ }
126
+ if (graph.toolExecution.local?.includeCodingTools === false) {
127
+ return CODE_EXECUTION_TOOLS.has(name);
128
+ }
129
+ return LOCAL_CODING_BUNDLE_NAME_SET.has(name);
130
+ }
131
+
132
+ function toCodeEnvFile(file: t.FileRef, execSessionId: string): t.CodeEnvFile {
133
+ const base = {
134
+ id: file.id,
135
+ resource_id: file.resource_id ?? file.id,
136
+ name: file.name,
137
+ storage_session_id: file.storage_session_id ?? execSessionId,
138
+ };
139
+ const kind = file.kind ?? 'user';
140
+ if (kind === 'skill' && file.version != null) {
141
+ return { ...base, kind: 'skill', version: file.version };
142
+ }
143
+ if (kind === 'agent') {
144
+ return { ...base, kind: 'agent' };
145
+ }
146
+ return { ...base, kind: 'user' };
147
+ }
148
+
149
+ function getCodeSessionContext(
150
+ graph: StandardGraph,
151
+ name: string
152
+ ): t.ToolCallRequest['codeSessionContext'] | undefined {
153
+ if (
154
+ !CODE_EXECUTION_TOOLS.has(name) &&
155
+ name !== Constants.SKILL_TOOL &&
156
+ name !== Constants.READ_FILE
157
+ ) {
158
+ return undefined;
159
+ }
160
+
161
+ const codeSession = graph.sessions.get(Constants.EXECUTE_CODE) as
162
+ | t.CodeSessionContext
163
+ | undefined;
164
+ if (codeSession?.session_id == null || codeSession.session_id === '') {
165
+ return undefined;
166
+ }
167
+
168
+ return {
169
+ session_id: codeSession.session_id,
170
+ files: codeSession.files?.map((file) =>
171
+ toCodeEnvFile(file, codeSession.session_id)
172
+ ),
173
+ };
174
+ }
175
+
176
+ function isEagerToolExecutionEnabledForBatch(args: {
177
+ graph: StandardGraph;
178
+ metadata?: Record<string, unknown>;
179
+ agentContext?: AgentContext;
180
+ }): boolean {
181
+ const { graph, metadata, agentContext } = args;
182
+ if (graph.eagerEventToolExecution?.enabled !== true) {
183
+ return false;
184
+ }
185
+ if ((agentContext?.toolDefinitions?.length ?? 0) === 0) {
186
+ return false;
187
+ }
188
+ if (isBatchSensitiveToolExecution(graph)) {
189
+ return false;
190
+ }
191
+ if (
192
+ metadata?.[Constants.PROGRAMMATIC_TOOL_CALLING] === true ||
193
+ metadata?.[Constants.BASH_PROGRAMMATIC_TOOL_CALLING] === true
194
+ ) {
195
+ return false;
196
+ }
197
+ if (graph.handlerRegistry?.getHandler(GraphEvents.ON_TOOL_EXECUTE) == null) {
198
+ return false;
199
+ }
200
+ return true;
201
+ }
202
+
203
+ function hasFinalToolCallSignal(chunk: Partial<AIMessageChunk>): boolean {
204
+ const metadata = chunk.response_metadata as
205
+ | Record<string, unknown>
206
+ | undefined;
207
+ const finishReason =
208
+ metadata?.finish_reason ??
209
+ metadata?.finishReason ??
210
+ metadata?.stop_reason ??
211
+ metadata?.stopReason;
212
+ return finishReason === 'tool_calls' || finishReason === 'tool_use';
213
+ }
214
+
215
+ function canPrestartSequentialStreamedToolChunks(
216
+ agentContext: AgentContext | undefined
217
+ ): boolean {
218
+ return (
219
+ agentContext?.provider === Providers.ANTHROPIC ||
220
+ agentContext?.provider === Providers.MOONSHOT
221
+ );
222
+ }
223
+
224
+ function hasExplicitStreamedToolCallSeals(
225
+ chunk: Partial<AIMessageChunk>
226
+ ): boolean {
227
+ return (
228
+ getStreamedToolCallAdapter(
229
+ chunk.response_metadata as Record<string, unknown> | undefined
230
+ ) != null
231
+ );
232
+ }
233
+
234
+ function hasDirectToolCallInBatch(args: {
235
+ graph: StandardGraph;
236
+ agentContext?: AgentContext;
237
+ toolCalls: ToolCall[];
238
+ }): boolean {
239
+ const { graph, agentContext, toolCalls } = args;
240
+ return toolCalls.some(
241
+ (toolCall) =>
242
+ toolCall.name !== '' &&
243
+ (isDirectGraphTool(toolCall.name, agentContext) ||
244
+ isDirectLocalTool(toolCall.name, graph))
245
+ );
246
+ }
247
+
248
+ function hasPotentialDirectToolInStreamContext(args: {
249
+ graph: StandardGraph;
250
+ agentContext?: AgentContext;
251
+ }): boolean {
252
+ const { graph, agentContext } = args;
253
+ if (graph.toolExecution?.engine === 'local') {
254
+ return true;
255
+ }
256
+ if ((agentContext?.graphTools?.length ?? 0) > 0) {
257
+ return true;
258
+ }
259
+ return (
260
+ agentContext?.toolDefinitions?.some((toolDefinition) =>
261
+ toolDefinition.name.startsWith(Constants.LC_TRANSFER_TO_)
262
+ ) === true
263
+ );
264
+ }
265
+
266
+ type EagerToolExecutionEntry = {
267
+ id: string;
268
+ toolName: string;
269
+ coercedArgs: Record<string, unknown>;
270
+ request: t.ToolCallRequest;
271
+ };
272
+
273
+ function createEagerToolExecutionPlan(args: {
274
+ graph: StandardGraph;
275
+ metadata?: Record<string, unknown>;
276
+ agentContext?: AgentContext;
277
+ toolCalls: ToolCall[];
278
+ skipExisting?: boolean;
279
+ }): EagerToolExecutionEntry[] | undefined {
280
+ const {
281
+ graph,
282
+ metadata,
283
+ agentContext,
284
+ toolCalls,
285
+ skipExisting = false,
286
+ } = args;
287
+ if (
288
+ !isEagerToolExecutionEnabledForBatch({
289
+ graph,
290
+ metadata,
291
+ agentContext,
292
+ })
293
+ ) {
294
+ return undefined;
295
+ }
296
+
297
+ if (hasDirectToolCallInBatch({ graph, agentContext, toolCalls })) {
298
+ return undefined;
299
+ }
300
+
301
+ const candidateToolCalls = skipExisting
302
+ ? toolCalls.filter((toolCall) => {
303
+ if (toolCall.id == null || toolCall.id === '') {
304
+ return true;
305
+ }
306
+ return !graph.eagerEventToolExecutions.has(toolCall.id);
307
+ })
308
+ : toolCalls;
309
+ if (candidateToolCalls.length === 0) {
310
+ return [];
311
+ }
312
+
313
+ // Eager execution must preserve ToolNode batch semantics exactly for every
314
+ // unstarted call. If any candidate cannot be planned, fall back for that
315
+ // candidate set.
316
+ if (
317
+ candidateToolCalls.some(
318
+ (toolCall) =>
319
+ toolCall.id == null ||
320
+ toolCall.id === '' ||
321
+ toolCall.name === '' ||
322
+ (!skipExisting && graph.eagerEventToolExecutions.has(toolCall.id))
323
+ )
324
+ ) {
325
+ return undefined;
326
+ }
327
+
328
+ const plan = buildToolExecutionRequestPlan({
329
+ toolCalls: candidateToolCalls.map((toolCall) => ({
330
+ id: toolCall.id,
331
+ name: toolCall.name,
332
+ args: toolCall.args,
333
+ stepId: graph.toolCallStepIds.get(toolCall.id!) ?? '',
334
+ codeSessionContext: getCodeSessionContext(graph, toolCall.name),
335
+ })),
336
+ usageCount: graph.getEagerEventToolUsageCount(agentContext?.agentId),
337
+ });
338
+ if (plan == null) {
339
+ return undefined;
340
+ }
341
+
342
+ return plan.requests.map(
343
+ (request): EagerToolExecutionEntry => ({
344
+ id: request.id,
345
+ toolName: request.name,
346
+ coercedArgs: request.args,
347
+ request,
348
+ })
349
+ );
350
+ }
351
+
352
+ function startEagerToolExecutions(args: {
353
+ graph: StandardGraph;
354
+ metadata?: Record<string, unknown>;
355
+ agentContext?: AgentContext;
356
+ toolCalls: ToolCall[];
357
+ skipExisting?: boolean;
358
+ }): void {
359
+ const { graph, metadata, agentContext, toolCalls, skipExisting } = args;
360
+ const entries = createEagerToolExecutionPlan({
361
+ graph,
362
+ metadata,
363
+ agentContext,
364
+ toolCalls,
365
+ skipExisting,
366
+ });
367
+ if (entries == null || entries.length === 0) {
368
+ return;
369
+ }
370
+
371
+ const promise: Promise<t.EagerEventToolExecutionOutcome> = new Promise<
372
+ t.ToolExecuteResult[]
373
+ >((resolve, reject) => {
374
+ let dispatchSettled = false;
375
+ let resultSettled = false;
376
+ let settledResults: t.ToolExecuteResult[] | undefined;
377
+ const maybeResolve = (): void => {
378
+ if (dispatchSettled && resultSettled) {
379
+ resolve(settledResults ?? []);
380
+ }
381
+ };
382
+ const batchRequest: t.ToolExecuteBatchRequest = {
383
+ toolCalls: entries.map((entry) => entry.request),
384
+ userId: graph.config?.configurable?.user_id as string | undefined,
385
+ agentId: agentContext?.agentId,
386
+ configurable: graph.config?.configurable as
387
+ | Record<string, unknown>
388
+ | undefined,
389
+ metadata,
390
+ resolve: (results): void => {
391
+ resultSettled = true;
392
+ settledResults = results;
393
+ maybeResolve();
394
+ },
395
+ reject,
396
+ };
397
+
398
+ void safeDispatchCustomEvent(
399
+ GraphEvents.ON_TOOL_EXECUTE,
400
+ batchRequest,
401
+ graph.config
402
+ )
403
+ .then(() => {
404
+ dispatchSettled = true;
405
+ maybeResolve();
406
+ })
407
+ .catch(reject);
408
+ }).then(
409
+ (results): t.EagerEventToolExecutionOutcome => ({ results }),
410
+ (error): t.EagerEventToolExecutionOutcome => ({
411
+ error: normalizeError(error),
412
+ })
413
+ );
414
+
415
+ for (const entry of entries) {
416
+ graph.eagerEventToolExecutions.set(entry.id, {
417
+ toolCallId: entry.id,
418
+ toolName: entry.toolName,
419
+ args: entry.coercedArgs,
420
+ request: entry.request,
421
+ promise,
422
+ });
423
+ }
424
+ }
425
+
426
+ function getEagerToolChunkKey(
427
+ stepKey: string,
428
+ toolCallChunk: ToolCallChunk
429
+ ): string | undefined {
430
+ let chunkKey: string | undefined;
431
+ if (typeof toolCallChunk.index === 'number') {
432
+ chunkKey = String(toolCallChunk.index);
433
+ } else if (toolCallChunk.id != null && toolCallChunk.id !== '') {
434
+ chunkKey = toolCallChunk.id;
435
+ }
436
+ if (chunkKey == null) {
437
+ return undefined;
438
+ }
439
+ return `${stepKey}\u0000${chunkKey}`;
440
+ }
441
+
442
+ function getEagerToolChunkIndex(
443
+ toolCallChunk: ToolCallChunk
444
+ ): number | undefined {
445
+ return typeof toolCallChunk.index === 'number'
446
+ ? toolCallChunk.index
447
+ : undefined;
448
+ }
449
+
450
+ function pruneEagerToolCallChunkStates(args: {
451
+ graph: StandardGraph;
452
+ stepKey: string;
453
+ toolCallIds?: ReadonlySet<string>;
454
+ clearStep?: boolean;
455
+ }): void {
456
+ const { graph, stepKey, toolCallIds, clearStep = false } = args;
457
+ const prefix = `${stepKey}\u0000`;
458
+ for (const [key, state] of graph.eagerEventToolCallChunks) {
459
+ if (!key.startsWith(prefix)) {
460
+ continue;
461
+ }
462
+ if (
463
+ clearStep ||
464
+ (state.id != null && toolCallIds?.has(state.id) === true)
465
+ ) {
466
+ graph.eagerEventToolCallChunks.delete(key);
467
+ }
468
+ }
469
+ }
470
+
471
+ function isEagerToolChunkStateComplete(
472
+ state: t.EagerEventToolCallChunkState
473
+ ): boolean {
474
+ return (
475
+ state.id != null &&
476
+ state.id !== '' &&
477
+ state.name != null &&
478
+ state.name !== '' &&
479
+ coerceRecordArgs(state.argsText) != null
480
+ );
481
+ }
482
+
483
+ function mergeToolCallArgsText(existing: string, incoming: string): string {
484
+ if (incoming === '') {
485
+ return existing;
486
+ }
487
+ if (existing === '') {
488
+ return incoming;
489
+ }
490
+ if (incoming === existing) {
491
+ try {
492
+ JSON.parse(incoming);
493
+ return incoming;
494
+ } catch {
495
+ return `${existing}${incoming}`;
496
+ }
497
+ }
498
+ if (incoming.startsWith(existing)) {
499
+ return incoming;
500
+ }
501
+ if (existing.startsWith(incoming)) {
502
+ return existing;
503
+ }
504
+ try {
505
+ JSON.parse(existing);
506
+ JSON.parse(incoming);
507
+ return incoming;
508
+ } catch {
509
+ // Fall through to delta concatenation.
510
+ }
511
+ for (
512
+ let overlap = Math.min(existing.length, incoming.length);
513
+ overlap >= 8;
514
+ overlap -= 1
515
+ ) {
516
+ if (existing.endsWith(incoming.slice(0, overlap))) {
517
+ return `${existing}${incoming.slice(overlap)}`;
518
+ }
519
+ }
520
+ return `${existing}${incoming}`;
521
+ }
522
+
523
+ function recordEagerToolCallChunks(args: {
524
+ graph: StandardGraph;
525
+ stepKey: string;
526
+ toolCallChunks?: ToolCallChunk[];
527
+ }): void {
528
+ const { graph, stepKey, toolCallChunks } = args;
529
+ if (toolCallChunks == null || toolCallChunks.length === 0) {
530
+ return;
531
+ }
532
+
533
+ // Streamed args can be cumulative and parseable before the provider has
534
+ // sealed the call. Recording stays separate from dispatch so the boundary
535
+ // logic can wait for either a later tool index or the final tool-call signal.
536
+ for (const toolCallChunk of toolCallChunks) {
537
+ const key = getEagerToolChunkKey(stepKey, toolCallChunk);
538
+ if (key == null) {
539
+ continue;
540
+ }
541
+
542
+ const incomingId =
543
+ toolCallChunk.id != null && toolCallChunk.id !== ''
544
+ ? toolCallChunk.id
545
+ : undefined;
546
+ const incomingName =
547
+ toolCallChunk.name != null && toolCallChunk.name !== ''
548
+ ? toolCallChunk.name
549
+ : undefined;
550
+ const previous = graph.eagerEventToolCallChunks.get(key);
551
+ const shouldReset =
552
+ previous != null &&
553
+ ((incomingId != null &&
554
+ previous.id != null &&
555
+ incomingId !== previous.id) ||
556
+ (incomingName != null &&
557
+ previous.name != null &&
558
+ incomingName !== previous.name));
559
+ const existing =
560
+ previous == null || shouldReset
561
+ ? {
562
+ argsText: '',
563
+ }
564
+ : previous;
565
+ const id = incomingId ?? existing.id;
566
+ const name = incomingName ?? existing.name;
567
+ const incomingArgs = toolCallChunk.args ?? '';
568
+ const isRepeatedObservedFragment =
569
+ incomingArgs !== '' &&
570
+ incomingArgs.length > 1 &&
571
+ incomingArgs === existing.lastArgsFragment;
572
+ const argsText = isRepeatedObservedFragment
573
+ ? existing.argsText
574
+ : mergeToolCallArgsText(existing.argsText, incomingArgs);
575
+ const next = {
576
+ id,
577
+ name,
578
+ argsText,
579
+ index: getEagerToolChunkIndex(toolCallChunk) ?? existing.index,
580
+ lastArgsFragment:
581
+ incomingArgs !== '' ? incomingArgs : existing.lastArgsFragment,
582
+ };
583
+ graph.eagerEventToolCallChunks.set(key, next);
584
+ }
585
+ }
586
+
587
+ function getStreamedReadyToolCalls(args: {
588
+ graph: StandardGraph;
589
+ stepKey: string;
590
+ toolCallChunks?: ToolCallChunk[];
591
+ seal?: StreamedToolCallSeal;
592
+ allowSequentialSeal?: boolean;
593
+ sealAll?: boolean;
594
+ }): ToolCall[] {
595
+ const {
596
+ graph,
597
+ stepKey,
598
+ toolCallChunks,
599
+ seal,
600
+ allowSequentialSeal = false,
601
+ sealAll = false,
602
+ } = args;
603
+ const currentIndices = new Set<number>();
604
+ for (const toolCallChunk of toolCallChunks ?? []) {
605
+ const index = getEagerToolChunkIndex(toolCallChunk);
606
+ if (index != null) {
607
+ currentIndices.add(index);
608
+ }
609
+ }
610
+ const highestCurrentIndex =
611
+ currentIndices.size > 0 ? Math.max(...currentIndices) : undefined;
612
+ const prefix = `${stepKey}\u0000`;
613
+ const readyEntries: Array<{
614
+ key: string;
615
+ state: t.EagerEventToolCallChunkState;
616
+ }> = [];
617
+
618
+ for (const [key, state] of graph.eagerEventToolCallChunks) {
619
+ if (!key.startsWith(prefix)) {
620
+ continue;
621
+ }
622
+ if (state.id != null && graph.eagerEventToolExecutions.has(state.id)) {
623
+ graph.eagerEventToolCallChunks.delete(key);
624
+ continue;
625
+ }
626
+ if (!isEagerToolChunkStateComplete(state)) {
627
+ continue;
628
+ }
629
+ const isSealedByLaterChunk =
630
+ allowSequentialSeal &&
631
+ highestCurrentIndex != null &&
632
+ state.index != null &&
633
+ state.index < highestCurrentIndex &&
634
+ !currentIndices.has(state.index);
635
+ const isSealedExplicitly =
636
+ seal?.kind === 'single' &&
637
+ ((seal.id != null && state.id === seal.id) ||
638
+ (seal.index != null && state.index === seal.index));
639
+ if (
640
+ sealAll ||
641
+ seal?.kind === 'all' ||
642
+ isSealedByLaterChunk ||
643
+ isSealedExplicitly
644
+ ) {
645
+ readyEntries.push({ key, state });
646
+ }
647
+ }
648
+
649
+ pruneEagerToolCallChunkStates({
650
+ graph,
651
+ stepKey,
652
+ toolCallIds: new Set(
653
+ readyEntries
654
+ .map(({ state }) => state.id)
655
+ .filter((id): id is string => id != null && id !== '')
656
+ ),
657
+ });
658
+ if (sealAll) {
659
+ pruneEagerToolCallChunkStates({ graph, stepKey, clearStep: true });
660
+ }
661
+
662
+ return readyEntries
663
+ .sort((left, right) => (left.state.index ?? 0) - (right.state.index ?? 0))
664
+ .flatMap(({ state }) => {
665
+ const args = coerceRecordArgs(state.argsText);
666
+ if (args == null) {
667
+ return [];
668
+ }
669
+ return [
670
+ {
671
+ id: state.id,
672
+ name: state.name ?? '',
673
+ args,
674
+ },
675
+ ];
676
+ });
677
+ }
678
+
679
+ function startReadyStreamedEagerToolExecutions(args: {
680
+ graph: StandardGraph;
681
+ metadata?: Record<string, unknown>;
682
+ agentContext?: AgentContext;
683
+ stepKey: string;
684
+ toolCallChunks?: ToolCallChunk[];
685
+ seal?: StreamedToolCallSeal;
686
+ allowSequentialSeal?: boolean;
687
+ sealAll?: boolean;
688
+ }): void {
689
+ const {
690
+ graph,
691
+ metadata,
692
+ agentContext,
693
+ stepKey,
694
+ toolCallChunks,
695
+ seal,
696
+ allowSequentialSeal,
697
+ sealAll,
698
+ } = args;
699
+ if (
700
+ hasPotentialDirectToolInStreamContext({ graph, agentContext }) ||
701
+ !isEagerToolExecutionEnabledForBatch({ graph, metadata, agentContext })
702
+ ) {
703
+ return;
704
+ }
705
+ const toolCalls = getStreamedReadyToolCalls({
706
+ graph,
707
+ stepKey,
708
+ toolCallChunks,
709
+ seal,
710
+ allowSequentialSeal,
711
+ sealAll,
712
+ });
713
+ if (toolCalls.length === 0) {
714
+ return;
715
+ }
716
+ startEagerToolExecutions({
717
+ graph,
718
+ metadata,
719
+ agentContext,
720
+ toolCalls,
721
+ skipExisting: true,
722
+ });
723
+ }
724
+
82
725
  export function getChunkContent({
83
726
  chunk,
84
727
  provider,
@@ -157,6 +800,7 @@ export class ChatModelStreamHandler implements t.EventHandler {
157
800
  const agentContext = graph.getAgentContext(metadata);
158
801
 
159
802
  const chunk = data.chunk as Partial<AIMessageChunk>;
803
+
160
804
  const content = getChunkContent({
161
805
  chunk,
162
806
  reasoningKey: agentContext.reasoningKey,
@@ -172,7 +816,10 @@ export class ChatModelStreamHandler implements t.EventHandler {
172
816
  return;
173
817
  }
174
818
  this.handleReasoning(chunk, agentContext);
819
+ const stepKey = graph.getStepKey(metadata);
175
820
  let hasToolCalls = false;
821
+ const hasToolCallChunks =
822
+ (chunk.tool_call_chunks && chunk.tool_call_chunks.length > 0) ?? false;
176
823
  if (
177
824
  chunk.tool_calls &&
178
825
  chunk.tool_calls.length > 0 &&
@@ -186,10 +833,20 @@ export class ChatModelStreamHandler implements t.EventHandler {
186
833
  ) {
187
834
  hasToolCalls = true;
188
835
  await handleToolCalls(chunk.tool_calls, metadata, graph);
836
+ if (hasFinalToolCallSignal(chunk)) {
837
+ startEagerToolExecutions({
838
+ graph,
839
+ metadata,
840
+ agentContext,
841
+ toolCalls: chunk.tool_calls,
842
+ skipExisting: true,
843
+ });
844
+ if (!hasToolCallChunks) {
845
+ pruneEagerToolCallChunkStates({ graph, stepKey, clearStep: true });
846
+ }
847
+ }
189
848
  }
190
849
 
191
- const hasToolCallChunks =
192
- (chunk.tool_call_chunks && chunk.tool_call_chunks.length > 0) ?? false;
193
850
  const isEmptyContent =
194
851
  typeof content === 'undefined' ||
195
852
  !content.length ||
@@ -202,26 +859,51 @@ export class ChatModelStreamHandler implements t.EventHandler {
202
859
  (chunk.id ?? '') !== '' &&
203
860
  !graph.prelimMessageIdsByStepKey.has(chunk.id ?? '')
204
861
  ) {
205
- const stepKey = graph.getStepKey(metadata);
206
862
  graph.prelimMessageIdsByStepKey.set(stepKey, chunk.id ?? '');
207
863
  } else if (isEmptyChunk) {
208
864
  return;
209
865
  }
210
866
 
211
- const stepKey = graph.getStepKey(metadata);
212
-
213
867
  if (
214
868
  hasToolCallChunks &&
215
869
  chunk.tool_call_chunks &&
216
870
  chunk.tool_call_chunks.length &&
217
871
  typeof chunk.tool_call_chunks[0]?.index === 'number'
218
872
  ) {
873
+ const streamedToolCallSeal = getStreamedToolCallSeal(
874
+ chunk.response_metadata as Record<string, unknown> | undefined
875
+ );
876
+ const allowSequentialSeal =
877
+ canPrestartSequentialStreamedToolChunks(agentContext);
878
+ const canStreamEager =
879
+ (allowSequentialSeal || hasExplicitStreamedToolCallSeals(chunk)) &&
880
+ !hasPotentialDirectToolInStreamContext({ graph, agentContext }) &&
881
+ isEagerToolExecutionEnabledForBatch({ graph, metadata, agentContext });
882
+ if (canStreamEager) {
883
+ recordEagerToolCallChunks({
884
+ graph,
885
+ stepKey,
886
+ toolCallChunks: chunk.tool_call_chunks,
887
+ });
888
+ }
219
889
  await handleToolCallChunks({
220
890
  graph,
221
891
  stepKey,
222
892
  toolCallChunks: chunk.tool_call_chunks,
223
893
  metadata,
224
894
  });
895
+ if (canStreamEager) {
896
+ startReadyStreamedEagerToolExecutions({
897
+ graph,
898
+ metadata,
899
+ agentContext,
900
+ stepKey,
901
+ toolCallChunks: chunk.tool_call_chunks,
902
+ seal: streamedToolCallSeal,
903
+ allowSequentialSeal,
904
+ sealAll: hasFinalToolCallSignal(chunk),
905
+ });
906
+ }
225
907
  }
226
908
 
227
909
  if (isEmptyContent) {
@@ -273,25 +955,33 @@ hasToolCallChunks: ${hasToolCallChunks}
273
955
  return;
274
956
  } else if (typeof content === 'string') {
275
957
  if (agentContext.currentTokenType === ContentTypes.TEXT) {
276
- await graph.dispatchMessageDelta(stepId, {
277
- content: [
278
- {
279
- type: ContentTypes.TEXT,
280
- text: content,
281
- },
282
- ],
283
- });
284
- } else if (agentContext.currentTokenType === 'think_and_text') {
285
- const { text, thinking } = parseThinkingContent(content);
286
- if (thinking) {
287
- await graph.dispatchReasoningDelta(stepId, {
958
+ await graph.dispatchMessageDelta(
959
+ stepId,
960
+ {
288
961
  content: [
289
962
  {
290
- type: ContentTypes.THINK,
291
- think: thinking,
963
+ type: ContentTypes.TEXT,
964
+ text: content,
292
965
  },
293
966
  ],
294
- });
967
+ },
968
+ metadata
969
+ );
970
+ } else if (agentContext.currentTokenType === 'think_and_text') {
971
+ const { text, thinking } = parseThinkingContent(content);
972
+ if (thinking) {
973
+ await graph.dispatchReasoningDelta(
974
+ stepId,
975
+ {
976
+ content: [
977
+ {
978
+ type: ContentTypes.THINK,
979
+ think: thinking,
980
+ },
981
+ ],
982
+ },
983
+ metadata
984
+ );
295
985
  }
296
986
  if (text) {
297
987
  agentContext.currentTokenType = ContentTypes.TEXT;
@@ -310,31 +1000,43 @@ hasToolCallChunks: ${hasToolCallChunks}
310
1000
  );
311
1001
 
312
1002
  const newStepId = graph.getStepIdByKey(newStepKey);
313
- await graph.dispatchMessageDelta(newStepId, {
1003
+ await graph.dispatchMessageDelta(
1004
+ newStepId,
1005
+ {
1006
+ content: [
1007
+ {
1008
+ type: ContentTypes.TEXT,
1009
+ text: text,
1010
+ },
1011
+ ],
1012
+ },
1013
+ metadata
1014
+ );
1015
+ }
1016
+ } else {
1017
+ await graph.dispatchReasoningDelta(
1018
+ stepId,
1019
+ {
314
1020
  content: [
315
1021
  {
316
- type: ContentTypes.TEXT,
317
- text: text,
1022
+ type: ContentTypes.THINK,
1023
+ think: content,
318
1024
  },
319
1025
  ],
320
- });
321
- }
322
- } else {
323
- await graph.dispatchReasoningDelta(stepId, {
324
- content: [
325
- {
326
- type: ContentTypes.THINK,
327
- think: content,
328
- },
329
- ],
330
- });
1026
+ },
1027
+ metadata
1028
+ );
331
1029
  }
332
1030
  } else if (
333
1031
  content.every((c) => c.type?.startsWith(ContentTypes.TEXT) ?? false)
334
1032
  ) {
335
- await graph.dispatchMessageDelta(stepId, {
336
- content,
337
- });
1033
+ await graph.dispatchMessageDelta(
1034
+ stepId,
1035
+ {
1036
+ content,
1037
+ },
1038
+ metadata
1039
+ );
338
1040
  } else if (
339
1041
  content.every(
340
1042
  (c) =>
@@ -344,16 +1046,21 @@ hasToolCallChunks: ${hasToolCallChunks}
344
1046
  c.type === 'redacted_thinking'
345
1047
  )
346
1048
  ) {
347
- await graph.dispatchReasoningDelta(stepId, {
348
- content: content.map((c) => ({
349
- type: ContentTypes.THINK,
350
- think:
351
- (c as t.ThinkingContentText).thinking ??
352
- (c as Partial<t.GoogleReasoningContentText>).reasoning ??
353
- (c as Partial<t.BedrockReasoningContentText>).reasoningText?.text ??
354
- '',
355
- })),
356
- });
1049
+ await graph.dispatchReasoningDelta(
1050
+ stepId,
1051
+ {
1052
+ content: content.map((c) => ({
1053
+ type: ContentTypes.THINK,
1054
+ think:
1055
+ (c as t.ThinkingContentText).thinking ??
1056
+ (c as Partial<t.GoogleReasoningContentText>).reasoning ??
1057
+ (c as Partial<t.BedrockReasoningContentText>).reasoningText
1058
+ ?.text ??
1059
+ '',
1060
+ })),
1061
+ },
1062
+ metadata
1063
+ );
357
1064
  }
358
1065
  }
359
1066
  handleReasoning(