@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
@@ -1137,7 +1137,7 @@ function createSummarizationChunkHandler({
1137
1137
  ? [{ type: ContentTypes.TEXT, text: raw } as t.MessageContentComplex]
1138
1138
  : raw;
1139
1139
 
1140
- safeDispatchCustomEvent(
1140
+ void safeDispatchCustomEvent(
1141
1141
  GraphEvents.ON_SUMMARIZE_DELTA,
1142
1142
  {
1143
1143
  id: stepId,
@@ -50,6 +50,10 @@ import {
50
50
  resolveLocalToolRegistry,
51
51
  resolveLocalExecutionTools,
52
52
  } from '@/tools/local';
53
+ import {
54
+ buildToolExecutionRequestPlan,
55
+ recordArgsEqual,
56
+ } from '@/tools/eagerEventExecution';
53
57
 
54
58
  /**
55
59
  * Per-call batch context for `runTool`. Bundles every optional
@@ -413,6 +417,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
413
417
  private sessions?: t.ToolSessionMap;
414
418
  /** When true, dispatches ON_TOOL_EXECUTE events instead of invoking tools directly */
415
419
  private eventDrivenMode: boolean = false;
420
+ /** Opt-in stream-layer prestart config for event-driven tools. */
421
+ private eagerEventToolExecution?: t.EagerEventToolExecutionConfig;
422
+ /** Shared per-run prestarted tool registry populated by ChatModelStreamHandler. */
423
+ private eagerEventToolExecutions?: Map<string, t.EagerEventToolExecution>;
424
+ /** Shared per-run per-tool turn counter used by eager and normal event dispatch. */
425
+ private eagerEventToolUsageCount?: Map<string, number>;
416
426
  /** Agent ID for event-driven mode */
417
427
  private agentId?: string;
418
428
  /** Tool names that bypass event dispatch and execute directly (e.g., graph-managed handoff tools) */
@@ -469,6 +479,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
469
479
  toolRegistry,
470
480
  sessions,
471
481
  eventDrivenMode,
482
+ eagerEventToolExecution,
483
+ eagerEventToolExecutions,
484
+ eagerEventToolUsageCount,
472
485
  agentId,
473
486
  directToolNames,
474
487
  maxContextTokens,
@@ -493,6 +506,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
493
506
  });
494
507
  this.sessions = sessions;
495
508
  this.eventDrivenMode = eventDrivenMode ?? false;
509
+ this.eagerEventToolExecution = eagerEventToolExecution;
510
+ this.eagerEventToolExecutions = eagerEventToolExecutions;
511
+ this.eagerEventToolUsageCount = eagerEventToolUsageCount;
496
512
  this.agentId = agentId;
497
513
  this.directToolNames = directToolNames;
498
514
  this.maxToolResultChars =
@@ -698,6 +714,34 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
698
714
  return new Map(this.toolUsageCount); // Return a copy
699
715
  }
700
716
 
717
+ private recordToolUsageTurn(
718
+ toolName: string,
719
+ turn: number,
720
+ callId?: string
721
+ ): void {
722
+ this.toolUsageCount.set(
723
+ toolName,
724
+ Math.max(this.toolUsageCount.get(toolName) ?? 0, turn + 1)
725
+ );
726
+ if (callId != null && callId !== '') {
727
+ this.toolCallTurns.set(callId, turn);
728
+ }
729
+ }
730
+
731
+ private recordEventToolPlanningTurn(
732
+ toolName: string,
733
+ turn: number,
734
+ callId?: string
735
+ ): void {
736
+ this.recordToolUsageTurn(toolName, turn, callId);
737
+ if (this.canConsumeEagerEventExecution()) {
738
+ this.eagerEventToolUsageCount?.set(
739
+ toolName,
740
+ Math.max(this.eagerEventToolUsageCount.get(toolName) ?? 0, turn + 1)
741
+ );
742
+ }
743
+ }
744
+
701
745
  /**
702
746
  * Runs a single tool call with error handling.
703
747
  *
@@ -1018,13 +1062,13 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1018
1062
  handlerError:
1019
1063
  handlerError instanceof Error
1020
1064
  ? {
1021
- message: handlerError.message,
1022
- stack: handlerError.stack ?? undefined,
1023
- }
1065
+ message: handlerError.message,
1066
+ stack: handlerError.stack ?? undefined,
1067
+ }
1024
1068
  : {
1025
- message: String(handlerError),
1026
- stack: undefined,
1027
- },
1069
+ message: String(handlerError),
1070
+ stack: undefined,
1071
+ },
1028
1072
  });
1029
1073
  }
1030
1074
  }
@@ -1032,11 +1076,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1032
1076
  const refMeta =
1033
1077
  unresolvedRefs.length > 0
1034
1078
  ? this.recordOutputReference(
1035
- runId,
1036
- errorContent,
1037
- undefined,
1038
- unresolvedRefs
1039
- )
1079
+ runId,
1080
+ errorContent,
1081
+ undefined,
1082
+ unresolvedRefs
1083
+ )
1040
1084
  : undefined;
1041
1085
  return new ToolMessage({
1042
1086
  status: 'error',
@@ -1678,12 +1722,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1678
1722
  * by `runTool`. Threaded as a local map (instead of instance state)
1679
1723
  * so concurrent batches cannot read each other's entries.
1680
1724
  */
1681
- private handleRunToolCompletions(
1725
+ private async handleRunToolCompletions(
1682
1726
  calls: ToolCall[],
1683
1727
  outputs: (BaseMessage | Command)[],
1684
1728
  config: RunnableConfig,
1685
1729
  resolvedArgsByCallId?: ResolvedArgsByCallId
1686
- ): void {
1730
+ ): Promise<void> {
1687
1731
  for (let i = 0; i < calls.length; i++) {
1688
1732
  const call = calls[i];
1689
1733
  const output = outputs[i];
@@ -1741,7 +1785,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1741
1785
  progress: 1,
1742
1786
  };
1743
1787
 
1744
- safeDispatchCustomEvent(
1788
+ await safeDispatchCustomEvent(
1745
1789
  GraphEvents.ON_RUN_STEP_COMPLETED,
1746
1790
  {
1747
1791
  result: {
@@ -1949,9 +1993,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1949
1993
  });
1950
1994
  };
1951
1995
 
1952
- const flushDeferredBlockedSideEffects = (): void => {
1996
+ const flushDeferredBlockedSideEffects = async (): Promise<void> => {
1953
1997
  for (const item of deferredBlockedSideEffects) {
1954
- this.dispatchStepCompleted(
1998
+ await this.dispatchStepCompleted(
1955
1999
  item.callId,
1956
2000
  item.toolName,
1957
2001
  item.args,
@@ -2236,7 +2280,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2236
2280
  * no risk of being rolled back by a subsequent throw, so
2237
2281
  * no risk of a duplicate `ON_RUN_STEP_COMPLETED` event.
2238
2282
  */
2239
- this.dispatchStepCompleted(
2283
+ await this.dispatchStepCompleted(
2240
2284
  entry.call.id!,
2241
2285
  entry.call.name,
2242
2286
  entry.args,
@@ -2321,7 +2365,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2321
2365
  * dispatches in the same relative position as the pre-deferral
2322
2366
  * code did (after hook processing, before tool execution).
2323
2367
  */
2324
- flushDeferredBlockedSideEffects();
2368
+ await flushDeferredBlockedSideEffects();
2325
2369
  } else {
2326
2370
  approvedEntries.push(...preToolCalls);
2327
2371
  }
@@ -2331,67 +2375,118 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2331
2375
  const batchIndexByCallId = new Map<string, number>();
2332
2376
 
2333
2377
  if (approvedEntries.length > 0) {
2334
- const requests: t.ToolCallRequest[] = approvedEntries.map((entry) => {
2335
- const turn = this.toolUsageCount.get(entry.call.name) ?? 0;
2336
- this.toolUsageCount.set(entry.call.name, turn + 1);
2378
+ const plan = buildToolExecutionRequestPlan({
2379
+ toolCalls: approvedEntries.map((entry) => {
2380
+ const codeSessionContext =
2381
+ CODE_EXECUTION_TOOLS.has(entry.call.name) ||
2382
+ entry.call.name === Constants.SKILL_TOOL ||
2383
+ entry.call.name === Constants.READ_FILE
2384
+ ? this.getCodeSessionContext()
2385
+ : undefined;
2386
+ return {
2387
+ id: entry.call.id,
2388
+ name: entry.call.name,
2389
+ args: entry.args,
2390
+ stepId: entry.stepId,
2391
+ codeSessionContext,
2392
+ };
2393
+ }),
2394
+ usageCount: this.toolUsageCount,
2395
+ invalidArgsBehavior: 'error-result',
2396
+ recordTurn: (toolName, reservedTurn, callId) => {
2397
+ this.recordEventToolPlanningTurn(toolName, reservedTurn, callId);
2398
+ },
2399
+ });
2400
+ if (plan == null) {
2401
+ throw new Error('Unable to build event tool execution request plan');
2402
+ }
2403
+ const requests = plan.requests;
2337
2404
 
2405
+ for (const entry of approvedEntries) {
2338
2406
  if (entry.batchIndex != null && entry.call.id != null) {
2339
2407
  batchIndexByCallId.set(entry.call.id, entry.batchIndex);
2340
2408
  }
2409
+ }
2341
2410
 
2342
- const request: t.ToolCallRequest = {
2343
- id: entry.call.id!,
2344
- name: entry.call.name,
2345
- args: entry.args,
2346
- stepId: entry.stepId,
2347
- turn,
2348
- };
2411
+ for (const result of plan.rejectedResults) {
2412
+ this.eagerEventToolExecutions?.delete(result.toolCallId);
2413
+ }
2349
2414
 
2350
- /**
2351
- * Emit `codeSessionContext` for any tool whose host handler may need
2352
- * to reach into the code-execution sandbox:
2353
- * - `CODE_EXECUTION_TOOLS` — direct executors that POST to /exec.
2354
- * - `SKILL_TOOL` — skill files live alongside code-env state.
2355
- * - `READ_FILE` when the requested path is a code-env artifact
2356
- * (e.g. `/mnt/data/...`) the host falls back to reading via the
2357
- * same sandbox session; without the seeded `session_id` /
2358
- * `_injected_files` here, that fallback can't see prior-turn
2359
- * artifacts on the very first call of a turn.
2360
- */
2361
- if (
2362
- CODE_EXECUTION_TOOLS.has(entry.call.name) ||
2363
- entry.call.name === Constants.SKILL_TOOL ||
2364
- entry.call.name === Constants.READ_FILE
2365
- ) {
2366
- request.codeSessionContext = this.getCodeSessionContext();
2415
+ const requestMap = new Map(plan.allRequests.map((r) => [r.id, r]));
2416
+ const eagerExecutions: Array<{
2417
+ request: t.ToolCallRequest;
2418
+ execution: t.EagerEventToolExecution;
2419
+ }> = [];
2420
+ const dispatchRequests: t.ToolCallRequest[] = [];
2421
+
2422
+ for (const request of requests) {
2423
+ const eagerExecution = this.takeMatchingEagerEventExecution(request);
2424
+ if (eagerExecution != null) {
2425
+ eagerExecutions.push({ request, execution: eagerExecution });
2426
+ } else {
2427
+ dispatchRequests.push(request);
2367
2428
  }
2429
+ }
2368
2430
 
2369
- return request;
2370
- });
2431
+ const dispatchPromise =
2432
+ dispatchRequests.length === 0
2433
+ ? Promise.resolve([] as t.ToolExecuteResult[])
2434
+ : new Promise<t.ToolExecuteResult[]>((resolve, reject) => {
2435
+ let dispatchSettled = false;
2436
+ let resultSettled = false;
2437
+ let settledResults: t.ToolExecuteResult[] | undefined;
2438
+
2439
+ const maybeResolve = (): void => {
2440
+ if (dispatchSettled && resultSettled) {
2441
+ resolve(settledResults ?? []);
2442
+ }
2443
+ };
2371
2444
 
2372
- const requestMap = new Map(requests.map((r) => [r.id, r]));
2445
+ const batchRequest: t.ToolExecuteBatchRequest = {
2446
+ toolCalls: dispatchRequests,
2447
+ userId: config.configurable?.user_id as string | undefined,
2448
+ agentId: this.agentId,
2449
+ configurable: config.configurable as
2450
+ | Record<string, unknown>
2451
+ | undefined,
2452
+ metadata: config.metadata as
2453
+ | Record<string, unknown>
2454
+ | undefined,
2455
+ resolve: (results): void => {
2456
+ resultSettled = true;
2457
+ settledResults = results;
2458
+ maybeResolve();
2459
+ },
2460
+ reject,
2461
+ };
2373
2462
 
2374
- const results = await new Promise<t.ToolExecuteResult[]>(
2375
- (resolve, reject) => {
2376
- const batchRequest: t.ToolExecuteBatchRequest = {
2377
- toolCalls: requests,
2378
- userId: config.configurable?.user_id as string | undefined,
2379
- agentId: this.agentId,
2380
- configurable: config.configurable as
2381
- | Record<string, unknown>
2382
- | undefined,
2383
- metadata: config.metadata as Record<string, unknown> | undefined,
2384
- resolve,
2385
- reject,
2386
- };
2463
+ void safeDispatchCustomEvent(
2464
+ GraphEvents.ON_TOOL_EXECUTE,
2465
+ batchRequest,
2466
+ config
2467
+ )
2468
+ .then(() => {
2469
+ dispatchSettled = true;
2470
+ maybeResolve();
2471
+ })
2472
+ .catch(reject);
2473
+ });
2387
2474
 
2388
- safeDispatchCustomEvent(
2389
- GraphEvents.ON_TOOL_EXECUTE,
2390
- batchRequest,
2391
- config
2392
- );
2393
- }
2394
- );
2475
+ const eagerResultsPromise = Promise.all(
2476
+ eagerExecutions.map(({ request, execution }) =>
2477
+ this.resolveEagerEventExecution(request, execution)
2478
+ )
2479
+ ).then((results) => results.flat());
2480
+
2481
+ const [eagerResults, dispatchedResults] = await Promise.all([
2482
+ eagerResultsPromise,
2483
+ dispatchPromise,
2484
+ ]);
2485
+ const results = [
2486
+ ...plan.rejectedResults,
2487
+ ...eagerResults,
2488
+ ...dispatchedResults,
2489
+ ];
2395
2490
 
2396
2491
  this.storeCodeSessionFromResults(results, requestMap);
2397
2492
 
@@ -2442,11 +2537,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2442
2537
  const errorRefMeta =
2443
2538
  unresolved.length > 0
2444
2539
  ? this.recordOutputReference(
2445
- registryRunId,
2446
- contentString,
2447
- undefined,
2448
- unresolved
2449
- )
2540
+ registryRunId,
2541
+ contentString,
2542
+ undefined,
2543
+ unresolved
2544
+ )
2450
2545
  : undefined;
2451
2546
  toolMessage = new ToolMessage({
2452
2547
  status: 'error',
@@ -2565,7 +2660,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2565
2660
  });
2566
2661
  }
2567
2662
 
2568
- this.dispatchStepCompleted(
2663
+ await this.dispatchStepCompleted(
2569
2664
  result.toolCallId,
2570
2665
  toolName,
2571
2666
  request?.args ?? {},
@@ -2606,6 +2701,84 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2606
2701
  return { toolMessages, injected };
2607
2702
  }
2608
2703
 
2704
+ private canConsumeEagerEventExecution(): boolean {
2705
+ return (
2706
+ this.eventDrivenMode &&
2707
+ this.eagerEventToolExecution?.enabled === true &&
2708
+ this.hookRegistry == null &&
2709
+ this.humanInTheLoop?.enabled !== true &&
2710
+ this.toolOutputRegistry == null
2711
+ );
2712
+ }
2713
+
2714
+ private takeMatchingEagerEventExecution(
2715
+ request: t.ToolCallRequest
2716
+ ): t.EagerEventToolExecution | undefined {
2717
+ if (!this.canConsumeEagerEventExecution()) {
2718
+ return undefined;
2719
+ }
2720
+
2721
+ const execution = this.eagerEventToolExecutions?.get(request.id);
2722
+ if (execution == null) {
2723
+ return undefined;
2724
+ }
2725
+
2726
+ this.eagerEventToolExecutions?.delete(request.id);
2727
+
2728
+ if (
2729
+ execution.toolName !== request.name ||
2730
+ !recordArgsEqual(execution.args, request.args) ||
2731
+ execution.request.turn !== request.turn
2732
+ ) {
2733
+ return {
2734
+ toolCallId: request.id,
2735
+ toolName: request.name,
2736
+ args: request.args,
2737
+ request,
2738
+ promise: Promise.resolve({
2739
+ results: [
2740
+ {
2741
+ toolCallId: request.id,
2742
+ status: 'error',
2743
+ content: '',
2744
+ errorMessage:
2745
+ 'Tool call changed after eager execution started; refusing to re-run the tool to avoid duplicate side effects.',
2746
+ },
2747
+ ],
2748
+ }),
2749
+ };
2750
+ }
2751
+
2752
+ return execution;
2753
+ }
2754
+
2755
+ private async resolveEagerEventExecution(
2756
+ request: t.ToolCallRequest,
2757
+ execution: t.EagerEventToolExecution
2758
+ ): Promise<t.ToolExecuteResult[]> {
2759
+ const outcome = await execution.promise;
2760
+ if (outcome.error != null) {
2761
+ throw outcome.error;
2762
+ }
2763
+
2764
+ const results = outcome.results.filter(
2765
+ (result) => result.toolCallId === request.id
2766
+ );
2767
+ if (results.length > 0) {
2768
+ return results;
2769
+ }
2770
+
2771
+ return [
2772
+ {
2773
+ toolCallId: request.id,
2774
+ status: 'error',
2775
+ content: '',
2776
+ errorMessage:
2777
+ 'Tool execution completed without a result for this tool call',
2778
+ },
2779
+ ];
2780
+ }
2781
+
2609
2782
  /**
2610
2783
  * Fires the `PostToolBatch` hook (if registered) and appends the
2611
2784
  * accumulated batch-level `additionalContext` strings to `injected`
@@ -2692,14 +2865,14 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2692
2865
  }
2693
2866
  }
2694
2867
 
2695
- private dispatchStepCompleted(
2868
+ private async dispatchStepCompleted(
2696
2869
  toolCallId: string,
2697
2870
  toolName: string,
2698
2871
  args: Record<string, unknown>,
2699
2872
  output: string,
2700
2873
  config: RunnableConfig,
2701
2874
  turn?: number
2702
- ): void {
2875
+ ): Promise<void> {
2703
2876
  const stepId = this.toolCallStepIds?.get(toolCallId) ?? '';
2704
2877
  if (!stepId) {
2705
2878
  // eslint-disable-next-line no-console
@@ -2710,7 +2883,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2710
2883
  );
2711
2884
  }
2712
2885
 
2713
- safeDispatchCustomEvent(
2886
+ await safeDispatchCustomEvent(
2714
2887
  GraphEvents.ON_RUN_STEP_COMPLETED,
2715
2888
  {
2716
2889
  result: {
@@ -2842,17 +3015,17 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2842
3015
  outputs =
2843
3016
  directAdditionalContexts.length > 0
2844
3017
  ? [
2845
- sendOutput,
2846
- new HumanMessage({
2847
- content: directAdditionalContexts.join('\n\n'),
2848
- // Match the event-driven path's marker so hosts /
2849
- // model-side annotators treat this as system intent
2850
- // rather than ordinary user text. Codex P2 [46].
2851
- additional_kwargs: { role: 'system', source: 'hook' },
2852
- }),
2853
- ]
3018
+ sendOutput,
3019
+ new HumanMessage({
3020
+ content: directAdditionalContexts.join('\n\n'),
3021
+ // Match the event-driven path's marker so hosts /
3022
+ // model-side annotators treat this as system intent
3023
+ // rather than ordinary user text. Codex P2 [46].
3024
+ additional_kwargs: { role: 'system', source: 'hook' },
3025
+ }),
3026
+ ]
2854
3027
  : [sendOutput];
2855
- this.handleRunToolCompletions(
3028
+ await this.handleRunToolCompletions(
2856
3029
  [input.lg_tool_call],
2857
3030
  // Pass only the tool output to completion handling; the
2858
3031
  // HumanMessage isn't a tool result.
@@ -3001,21 +3174,21 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3001
3174
  const directOutputs: (BaseMessage | Command)[] =
3002
3175
  directCalls.length > 0
3003
3176
  ? await Promise.all(
3004
- directCalls.map((call, i) =>
3005
- this.runDirectToolWithLifecycleHooks(call, config, {
3006
- batchIndex: directIndices[i],
3007
- turn,
3008
- batchScopeId,
3009
- resolvedArgsByCallId,
3010
- preBatchSnapshot,
3011
- additionalContextsSink: directAdditionalContexts,
3012
- })
3177
+ directCalls.map((call, i) =>
3178
+ this.runDirectToolWithLifecycleHooks(call, config, {
3179
+ batchIndex: directIndices[i],
3180
+ turn,
3181
+ batchScopeId,
3182
+ resolvedArgsByCallId,
3183
+ preBatchSnapshot,
3184
+ additionalContextsSink: directAdditionalContexts,
3185
+ })
3186
+ )
3013
3187
  )
3014
- )
3015
3188
  : [];
3016
3189
 
3017
3190
  if (directCalls.length > 0 && directOutputs.length > 0) {
3018
- this.handleRunToolCompletions(
3191
+ await this.handleRunToolCompletions(
3019
3192
  directCalls,
3020
3193
  directOutputs,
3021
3194
  config,
@@ -3026,29 +3199,29 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3026
3199
  const eventResult =
3027
3200
  eventCalls.length > 0
3028
3201
  ? await this.dispatchToolEvents(eventCalls, config, {
3029
- batchIndices: eventIndices,
3030
- turn,
3031
- batchScopeId,
3032
- preResolvedArgs: preResolvedEventArgs,
3033
- preBatchSnapshot,
3034
- })
3202
+ batchIndices: eventIndices,
3203
+ turn,
3204
+ batchScopeId,
3205
+ preResolvedArgs: preResolvedEventArgs,
3206
+ preBatchSnapshot,
3207
+ })
3035
3208
  : {
3036
- toolMessages: [] as ToolMessage[],
3037
- injected: [] as BaseMessage[],
3038
- };
3209
+ toolMessages: [] as ToolMessage[],
3210
+ injected: [] as BaseMessage[],
3211
+ };
3039
3212
 
3040
3213
  const directInjected: BaseMessage[] =
3041
3214
  directAdditionalContexts.length > 0
3042
3215
  ? [
3043
- new HumanMessage({
3044
- content: directAdditionalContexts.join('\n\n'),
3045
- // System-role metadata to match the event-driven
3046
- // path so policy/recovery guidance is treated
3047
- // consistently regardless of whether the tool ran
3048
- // direct or dispatched. Codex P2 [46].
3049
- additional_kwargs: { role: 'system', source: 'hook' },
3050
- }),
3051
- ]
3216
+ new HumanMessage({
3217
+ content: directAdditionalContexts.join('\n\n'),
3218
+ // System-role metadata to match the event-driven
3219
+ // path so policy/recovery guidance is treated
3220
+ // consistently regardless of whether the tool ran
3221
+ // direct or dispatched. Codex P2 [46].
3222
+ additional_kwargs: { role: 'system', source: 'hook' },
3223
+ }),
3224
+ ]
3052
3225
  : [];
3053
3226
  outputs = [
3054
3227
  ...directOutputs,
@@ -3076,7 +3249,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3076
3249
  })
3077
3250
  )
3078
3251
  );
3079
- this.handleRunToolCompletions(
3252
+ await this.handleRunToolCompletions(
3080
3253
  filteredCalls,
3081
3254
  toolOutputs,
3082
3255
  config,
@@ -3087,15 +3260,15 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3087
3260
  outputs =
3088
3261
  directAdditionalContexts.length > 0
3089
3262
  ? [
3090
- ...toolOutputs,
3091
- new HumanMessage({
3092
- content: directAdditionalContexts.join('\n\n'),
3093
- // Same system-role marker the event-driven path
3094
- // uses so direct vs dispatched is invisible to
3095
- // downstream consumers. Codex P2 [46].
3096
- additional_kwargs: { role: 'system', source: 'hook' },
3097
- }),
3098
- ]
3263
+ ...toolOutputs,
3264
+ new HumanMessage({
3265
+ content: directAdditionalContexts.join('\n\n'),
3266
+ // Same system-role marker the event-driven path
3267
+ // uses so direct vs dispatched is invisible to
3268
+ // downstream consumers. Codex P2 [46].
3269
+ additional_kwargs: { role: 'system', source: 'hook' },
3270
+ }),
3271
+ ]
3099
3272
  : toolOutputs;
3100
3273
  }
3101
3274
  }