@librechat/agents 3.1.86 → 3.1.88

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