@librechat/agents 3.4.0 → 3.4.1

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 (114) hide show
  1. package/dist/cjs/graphs/Graph.cjs +111 -27
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/hooks/HookRegistry.cjs +83 -0
  4. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  5. package/dist/cjs/hooks/executeHooks.cjs +60 -14
  6. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  7. package/dist/cjs/hooks/index.cjs.map +1 -1
  8. package/dist/cjs/hooks/types.cjs +2 -0
  9. package/dist/cjs/hooks/types.cjs.map +1 -1
  10. package/dist/cjs/llm/init.cjs +3 -3
  11. package/dist/cjs/llm/invoke.cjs +2 -2
  12. package/dist/cjs/main.cjs +14 -13
  13. package/dist/cjs/messages/format.cjs +40 -23
  14. package/dist/cjs/messages/format.cjs.map +1 -1
  15. package/dist/cjs/run.cjs +82 -15
  16. package/dist/cjs/run.cjs.map +1 -1
  17. package/dist/cjs/session/AgentSession.cjs +37 -2
  18. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  19. package/dist/cjs/stream.cjs +1 -1
  20. package/dist/cjs/summarization/node.cjs +6 -3
  21. package/dist/cjs/summarization/node.cjs.map +1 -1
  22. package/dist/cjs/tools/BashExecutor.cjs +1 -1
  23. package/dist/cjs/tools/CodeExecutor.cjs +1 -1
  24. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +1 -1
  25. package/dist/cjs/tools/ToolNode.cjs +316 -94
  26. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  27. package/dist/cjs/tools/ToolSearch.cjs +1 -1
  28. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +630 -78
  29. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  30. package/dist/cjs/tools/subagent/SubagentReplay.cjs +175 -0
  31. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -0
  32. package/dist/cjs/tools/toolOutputReferences.cjs +20 -0
  33. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  34. package/dist/cjs/utils/index.cjs +2 -2
  35. package/dist/esm/graphs/Graph.mjs +111 -27
  36. package/dist/esm/graphs/Graph.mjs.map +1 -1
  37. package/dist/esm/hooks/HookRegistry.mjs +83 -0
  38. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  39. package/dist/esm/hooks/executeHooks.mjs +60 -14
  40. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  41. package/dist/esm/hooks/index.mjs.map +1 -1
  42. package/dist/esm/hooks/types.mjs +2 -1
  43. package/dist/esm/hooks/types.mjs.map +1 -1
  44. package/dist/esm/llm/init.mjs +1 -1
  45. package/dist/esm/llm/invoke.mjs +2 -2
  46. package/dist/esm/main.mjs +11 -11
  47. package/dist/esm/messages/format.mjs +40 -23
  48. package/dist/esm/messages/format.mjs.map +1 -1
  49. package/dist/esm/run.mjs +83 -16
  50. package/dist/esm/run.mjs.map +1 -1
  51. package/dist/esm/session/AgentSession.mjs +37 -2
  52. package/dist/esm/session/AgentSession.mjs.map +1 -1
  53. package/dist/esm/stream.mjs +1 -1
  54. package/dist/esm/summarization/node.mjs +6 -3
  55. package/dist/esm/summarization/node.mjs.map +1 -1
  56. package/dist/esm/tools/BashExecutor.mjs +1 -1
  57. package/dist/esm/tools/CodeExecutor.mjs +1 -1
  58. package/dist/esm/tools/ProgrammaticToolCalling.mjs +1 -1
  59. package/dist/esm/tools/ToolNode.mjs +317 -95
  60. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  61. package/dist/esm/tools/ToolSearch.mjs +1 -1
  62. package/dist/esm/tools/subagent/SubagentExecutor.mjs +631 -79
  63. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  64. package/dist/esm/tools/subagent/SubagentReplay.mjs +168 -0
  65. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -0
  66. package/dist/esm/tools/toolOutputReferences.mjs +20 -0
  67. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  68. package/dist/esm/utils/index.mjs +2 -2
  69. package/dist/types/graphs/Graph.d.ts +16 -1
  70. package/dist/types/hooks/HookRegistry.d.ts +10 -1
  71. package/dist/types/hooks/executeHooks.d.ts +5 -1
  72. package/dist/types/hooks/index.d.ts +2 -2
  73. package/dist/types/hooks/types.d.ts +12 -0
  74. package/dist/types/run.d.ts +4 -1
  75. package/dist/types/session/AgentSession.d.ts +1 -0
  76. package/dist/types/tools/ToolNode.d.ts +23 -15
  77. package/dist/types/tools/subagent/SubagentExecutor.d.ts +46 -7
  78. package/dist/types/tools/subagent/SubagentReplay.d.ts +81 -0
  79. package/dist/types/tools/toolOutputReferences.d.ts +12 -0
  80. package/dist/types/types/graph.d.ts +5 -5
  81. package/dist/types/types/hitl.d.ts +15 -0
  82. package/package.json +6 -2
  83. package/src/graphs/Graph.ts +246 -52
  84. package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +48 -9
  85. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1 -1
  86. package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +80 -0
  87. package/src/hooks/HookRegistry.ts +151 -1
  88. package/src/hooks/__tests__/HookRegistry.test.ts +96 -0
  89. package/src/hooks/__tests__/executeHooks.test.ts +160 -0
  90. package/src/hooks/executeHooks.ts +133 -15
  91. package/src/hooks/index.ts +3 -1
  92. package/src/hooks/types.ts +16 -0
  93. package/src/messages/format.ts +54 -26
  94. package/src/messages/formatAgentMessages.reducer.test.ts +162 -0
  95. package/src/messages/formatAgentMessages.steer.test.ts +16 -11
  96. package/src/messages/formatAgentMessages.test.ts +7 -2
  97. package/src/run.ts +164 -18
  98. package/src/session/AgentSession.ts +52 -2
  99. package/src/session/__tests__/JsonlSessionStore.test.ts +53 -0
  100. package/src/summarization/__tests__/node.test.ts +32 -14
  101. package/src/summarization/node.ts +15 -11
  102. package/src/tools/ToolNode.ts +631 -157
  103. package/src/tools/__tests__/SubagentExecutor.test.ts +542 -6
  104. package/src/tools/__tests__/SubagentReplay.test.ts +300 -0
  105. package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +208 -2
  106. package/src/tools/__tests__/directToolHITLResumeScope.test.ts +486 -2
  107. package/src/tools/__tests__/hitl.test.ts +234 -5
  108. package/src/tools/__tests__/subagentHooks.test.ts +1040 -62
  109. package/src/tools/__tests__/toolOutputReferences.test.ts +19 -2
  110. package/src/tools/subagent/SubagentExecutor.ts +1369 -131
  111. package/src/tools/subagent/SubagentReplay.ts +575 -0
  112. package/src/tools/toolOutputReferences.ts +40 -1
  113. package/src/types/graph.ts +5 -5
  114. package/src/types/hitl.ts +16 -0
@@ -1,3 +1,4 @@
1
+ const require_SubagentReplay = require("./subagent/SubagentReplay.cjs");
1
2
  const require_misc = require("../utils/misc.cjs");
2
3
  const require_langfuseRuntimeScope = require("../langfuseRuntimeScope.cjs");
3
4
  const require_enum = require("../common/enum.cjs");
@@ -14,6 +15,7 @@ const require_run = require("../utils/run.cjs");
14
15
  require("../utils/index.cjs");
15
16
  const require_CodeSessionFileSummary = require("./CodeSessionFileSummary.cjs");
16
17
  const require_executeHooks = require("../hooks/executeHooks.cjs");
18
+ const require_types = require("../hooks/types.cjs");
17
19
  require("../hooks/index.cjs");
18
20
  const require_resolveLocalExecutionTools = require("./local/resolveLocalExecutionTools.cjs");
19
21
  require("./local/index.cjs");
@@ -29,6 +31,22 @@ function stripRunBreakerScope(configurable) {
29
31
  const { [require_streamLimits.RUN_BREAKER_SCOPE_CONFIG_KEY]: _scope, ...rest } = configurable;
30
32
  return rest;
31
33
  }
34
+ function createToolApprovalReplayKey(config, agentId, toolUseId) {
35
+ const configuredScope = config.configurable?.[require_types.TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY];
36
+ const threadId = config.configurable?.thread_id;
37
+ let executionScope = "";
38
+ if (typeof configuredScope === "string") executionScope = configuredScope;
39
+ else if (typeof threadId === "string") executionScope = threadId;
40
+ return {
41
+ executionScope,
42
+ agentId,
43
+ toolUseId
44
+ };
45
+ }
46
+ function getToolApprovalReplaySessionId(config, hookSessionId) {
47
+ const configuredScope = config.configurable?.[require_types.TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY];
48
+ return typeof configuredScope === "string" && configuredScope.length > 0 ? configuredScope : hookSessionId;
49
+ }
32
50
  function createToolErrorOwnership() {
33
51
  return {
34
52
  handlerOwned: /* @__PURE__ */ new Set(),
@@ -99,6 +117,25 @@ function describeOfferedShape(value) {
99
117
  if (Array.isArray(value)) return "array";
100
118
  return typeof value;
101
119
  }
120
+ function findAssistantBatch(messages) {
121
+ for (let i = messages.length - 1; i >= 0; i--) {
122
+ const message = messages[i];
123
+ if ((0, _langchain_core_messages.isAIMessage)(message)) return {
124
+ message,
125
+ index: i,
126
+ messageCount: messages.length
127
+ };
128
+ }
129
+ }
130
+ function getAssistantBatchReplayKey(batch, runId, threadId) {
131
+ return JSON.stringify([
132
+ runId ?? null,
133
+ threadId ?? null,
134
+ batch.message.id ?? null,
135
+ batch.index,
136
+ batch.messageCount
137
+ ]);
138
+ }
102
139
  /**
103
140
  * Build the `tool_approval` interrupt payload from the set of pending
104
141
  * `ask`-decision entries collected during PreToolUse hook handling.
@@ -106,9 +143,10 @@ function describeOfferedShape(value) {
106
143
  * scope. The interrupt itself is raised by the caller (which still
107
144
  * needs `interrupt()` plus the AsyncLocalStorage anchoring shim).
108
145
  */
109
- function buildToolApprovalInterruptPayload(askEntries) {
146
+ function buildToolApprovalInterruptPayload(askEntries, hookSessionId) {
110
147
  return {
111
148
  type: "tool_approval",
149
+ ...hookSessionId == null || hookSessionId === "" ? {} : { hook_session_id: hookSessionId },
112
150
  action_requests: askEntries.map(({ entry, reason }) => {
113
151
  const request = {
114
152
  tool_call_id: entry.call.id,
@@ -287,6 +325,10 @@ var ToolNode = class extends require_run.RunnableCallable {
287
325
  * the Run ends.
288
326
  */
289
327
  directPathTurns = /* @__PURE__ */ new Map();
328
+ /** Terminal results from interrupting siblings that must survive a
329
+ * LangGraph replay of the containing ToolNode. Includes the sidecar data
330
+ * the fresh batch needs for hook-context injection and completion events. */
331
+ settledInterruptingResults = /* @__PURE__ */ new Map();
290
332
  /** Tool registry for filtering (lazy computation of programmatic maps) */
291
333
  toolRegistry;
292
334
  /** Cached programmatic tools (computed once on first PTC call) */
@@ -520,16 +562,16 @@ var ToolNode = class extends require_run.RunnableCallable {
520
562
  return `Tool "${callName}" not found. Did you mean "${suggestion}"? Handoff tool names must match exactly.`;
521
563
  }
522
564
  /**
523
- * Flush the per-Run direct-path turn cache. Called by the Graph at
524
- * end-of-Run via `clearHeavyState`. The map intentionally survives
525
- * `run()` re-entry so an interrupt + resume reuses the original
526
- * slot (Codex P2 #30), but it would otherwise grow linearly with
527
- * tool calls and could collide across Runs if a provider reused
528
- * call IDs (Codex P2 #33). Hosts can also call this directly if
565
+ * Flush per-Run direct replay state. Called by the Graph at end-of-Run via
566
+ * `clearHeavyState`. The state intentionally survives `run()` re-entry so
567
+ * interrupt + resume keeps both original turn slots and terminal sibling
568
+ * outputs, but it would otherwise grow linearly and could collide across
569
+ * Runs if a provider reused call IDs. Hosts can also call this directly if
529
570
  * they reuse a ToolNode across batches outside of a Graph.
530
571
  */
531
572
  clearDirectPathTurns() {
532
573
  this.directPathTurns.clear();
574
+ this.settledInterruptingResults.clear();
533
575
  }
534
576
  /**
535
577
  * Returns cached programmatic tools, computing once on first access.
@@ -559,6 +601,29 @@ var ToolNode = class extends require_run.RunnableCallable {
559
601
  getToolUsageCounts() {
560
602
  return new Map(this.toolUsageCount);
561
603
  }
604
+ createSubagentResumeState() {
605
+ return {
606
+ stateKey: JSON.stringify([
607
+ this.executingAgentId ?? "",
608
+ this.agentId ?? "",
609
+ this.name
610
+ ]),
611
+ toolUsageCounts: [...this.toolUsageCount].map(([toolName, count]) => ({
612
+ toolName,
613
+ count
614
+ })),
615
+ directPathTurns: [...this.directPathTurns].map(([toolCallId, turn]) => ({
616
+ toolCallId,
617
+ turn
618
+ }))
619
+ };
620
+ }
621
+ restoreSubagentResumeState(state) {
622
+ this.toolUsageCount.clear();
623
+ for (const { toolName, count } of state.toolUsageCounts) this.toolUsageCount.set(toolName, count);
624
+ this.directPathTurns.clear();
625
+ for (const { toolCallId, turn } of state.directPathTurns) this.directPathTurns.set(toolCallId, turn);
626
+ }
562
627
  recordToolUsageTurn(toolName, turn, callId) {
563
628
  this.toolUsageCount.set(toolName, Math.max(this.toolUsageCount.get(toolName) ?? 0, turn + 1));
564
629
  if (callId != null && callId !== "") this.toolCallTurns.set(callId, turn);
@@ -860,9 +925,9 @@ var ToolNode = class extends require_run.RunnableCallable {
860
925
  * `blockDirectCall`, `respond` returns the host-supplied
861
926
  * `responseText` as a synthetic success ToolMessage,
862
927
  * `edit` re-runs with edited args. LangGraph re-enters
863
- * ToolNode.run from the start on resume; the hook fires
864
- * again and the resume value distinguishes "first ask" from
865
- * "second pass with decision".
928
+ * ToolNode.run from the start on resume. Reusable hooks fire
929
+ * again; a consumed one-shot hook replays its pending approval
930
+ * result. In both cases `interrupt()` consumes the resume value.
866
931
  * • When HITL is off: collapses to a fail-closed deny (matches
867
932
  * the rest of the SDK's HITL-disabled default). One-time
868
933
  * warning logged so hosts notice the gap.
@@ -882,13 +947,49 @@ var ToolNode = class extends require_run.RunnableCallable {
882
947
  * scopes and is left to a follow-up.
883
948
  */
884
949
  async runDirectToolWithLifecycleHooks(call, config, batchContext = {}) {
950
+ const replayController = this.toolMap.get(call.name)?.[require_SubagentReplay.SUBAGENT_REPLAY_CONTROLLER];
951
+ const replayConfig = replayController == null || batchContext.replayBatchKey == null ? config : {
952
+ ...config,
953
+ configurable: {
954
+ ...config.configurable,
955
+ [require_SubagentReplay.SUBAGENT_PARENT_BATCH_CONFIG_KEY]: batchContext.replayBatchKey
956
+ }
957
+ };
958
+ const settledOutput = await replayController?.getSettledOutput(call, replayConfig);
959
+ if (settledOutput != null) {
960
+ if (batchContext.additionalContextsSink != null && settledOutput.additionalContexts.length > 0) batchContext.additionalContextsSink.push(...settledOutput.additionalContexts);
961
+ if (batchContext.resolvedArgsByCallId != null && call.id != null && settledOutput.resolvedArgs != null) batchContext.resolvedArgsByCallId.set(call.id, settledOutput.resolvedArgs);
962
+ const refMeta = settledOutput.output.additional_kwargs;
963
+ if (this.toolOutputRegistry != null && refMeta?._refKey != null && settledOutput.referenceContent != null) this.toolOutputRegistry.set(refMeta._refScope, refMeta._refKey, settledOutput.referenceContent);
964
+ return settledOutput.output;
965
+ }
966
+ const replayAdditionalContexts = [];
967
+ const persistOutput = async (output, terminalArgs) => {
968
+ const refMeta = output.additional_kwargs;
969
+ if (terminalArgs != null && call.id != null && batchContext.resolvedArgsByCallId != null) batchContext.resolvedArgsByCallId.set(call.id, terminalArgs);
970
+ const resolvedArgs = terminalArgs ?? (call.id == null ? void 0 : batchContext.resolvedArgsByCallId?.get(call.id));
971
+ const referenceContent = this.toolOutputRegistry == null || refMeta?._refKey == null ? void 0 : this.toolOutputRegistry.get(refMeta._refScope, refMeta._refKey);
972
+ await replayController?.persistSettledOutput(call, replayConfig, {
973
+ output,
974
+ additionalContexts: replayAdditionalContexts,
975
+ ...resolvedArgs == null ? {} : { resolvedArgs },
976
+ ...referenceContent == null ? {} : { referenceContent }
977
+ });
978
+ return output;
979
+ };
885
980
  const runId = config.configurable?.run_id ?? "";
981
+ const threadId = config.configurable?.thread_id;
886
982
  const hookRegistry = this.hookRegistry;
887
983
  const hasPreHook = hookRegistry?.hasHookFor("PreToolUse", runId) === true;
984
+ const approvalReplayKey = call.id == null || call.id === "" ? void 0 : createToolApprovalReplayKey(config, this.executingAgentId ?? this.agentId ?? "", call.id);
985
+ const approvalReplaySessionId = getToolApprovalReplaySessionId(config, runId);
986
+ const pendingApproval = approvalReplayKey == null ? void 0 : hookRegistry?.getPendingToolApproval(approvalReplaySessionId, approvalReplayKey);
888
987
  const hasPostHook = hookRegistry?.hasHookFor("PostToolUse", runId) === true;
889
988
  const hasFailureHook = hookRegistry?.hasHookFor("PostToolUseFailure", runId) === true;
890
- if (hookRegistry == null || !hasPreHook && !hasPostHook && !hasFailureHook) return this.runTool(call, config, batchContext);
891
- const threadId = config.configurable?.thread_id;
989
+ if (hookRegistry == null || !hasPreHook && pendingApproval == null && !hasPostHook && !hasFailureHook) {
990
+ const output = await this.runTool(call, replayConfig, batchContext);
991
+ return output instanceof _langchain_core_messages.ToolMessage ? persistOutput(output) : output;
992
+ }
892
993
  const registryRunId = batchContext.batchScopeId ?? config.configurable?.run_id;
893
994
  let usageCount;
894
995
  const cachedTurn = call.id != null && call.id !== "" ? this.directPathTurns.get(call.id) ?? this.toolCallTurns.get(call.id) : void 0;
@@ -913,7 +1014,7 @@ var ToolNode = class extends require_run.RunnableCallable {
913
1014
  resolvedArgs = resolved;
914
1015
  }
915
1016
  let effectiveCall = call;
916
- if (hasPreHook) {
1017
+ if (hasPreHook || pendingApproval != null) {
917
1018
  const preResult = await require_executeHooks.executeHooks({
918
1019
  registry: hookRegistry,
919
1020
  input: {
@@ -929,34 +1030,48 @@ var ToolNode = class extends require_run.RunnableCallable {
929
1030
  turn
930
1031
  },
931
1032
  sessionId: runId,
932
- matchQuery: call.name
1033
+ matchQuery: call.name,
1034
+ onceReplayKey: approvalReplayKey,
1035
+ onceReplaySessionId: approvalReplaySessionId
933
1036
  }).catch(() => void 0);
934
1037
  if (preResult != null) {
935
- if (batchContext.additionalContextsSink != null && preResult.additionalContexts.length > 0) batchContext.additionalContextsSink.push(...preResult.additionalContexts);
1038
+ if (preResult.additionalContexts.length > 0) {
1039
+ replayAdditionalContexts.push(...preResult.additionalContexts);
1040
+ batchContext.additionalContextsSink?.push(...preResult.additionalContexts);
1041
+ }
936
1042
  if (preResult.updatedInput != null) effectiveCall = {
937
1043
  ...call,
938
1044
  args: preResult.updatedInput
939
1045
  };
940
- if (preResult.decision === "deny") return this.blockDirectCall({
1046
+ if (preResult.decision === "deny") return persistOutput(this.blockDirectCall({
941
1047
  call,
942
1048
  resolvedArgs,
943
1049
  reason: preResult.reason ?? "Blocked by hook",
944
1050
  hookRegistry,
945
1051
  runId,
946
1052
  threadId
947
- });
1053
+ }), effectiveCall.args);
948
1054
  if (preResult.decision === "ask") {
949
1055
  if (this.humanInTheLoop?.enabled !== true) {
950
1056
  const reason = this.resolveAskDecisionForDirectTool(preResult.reason, call.name);
951
- return this.blockDirectCall({
1057
+ return persistOutput(this.blockDirectCall({
952
1058
  call,
953
1059
  resolvedArgs,
954
1060
  reason,
955
1061
  hookRegistry,
956
1062
  runId,
957
1063
  threadId
958
- });
1064
+ }), effectiveCall.args);
959
1065
  }
1066
+ const toolCallId = call.id;
1067
+ if (toolCallId == null || toolCallId === "" || approvalReplayKey == null) return persistOutput(this.blockDirectCall({
1068
+ call,
1069
+ resolvedArgs,
1070
+ reason: "Tool approval requires a non-empty tool call ID — failing closed",
1071
+ hookRegistry,
1072
+ runId,
1073
+ threadId
1074
+ }), effectiveCall.args);
960
1075
  const payload = buildToolApprovalInterruptPayload([{
961
1076
  entry: {
962
1077
  call: effectiveCall,
@@ -965,59 +1080,71 @@ var ToolNode = class extends require_run.RunnableCallable {
965
1080
  },
966
1081
  reason: preResult.reason,
967
1082
  allowedDecisions: preResult.allowedDecisions
968
- }]);
1083
+ }], runId);
969
1084
  const resumeValue = _langchain_core_singletons.AsyncLocalStorageProviderSingleton.runWithConfig(config, () => (0, _langchain_langgraph.interrupt)(payload));
970
- const decision = normalizeApprovalDecisions([call.id], resumeValue).get(call.id) ?? {
1085
+ hookRegistry.clearPendingToolApproval(approvalReplaySessionId, approvalReplayKey);
1086
+ const decision = normalizeApprovalDecisions([toolCallId], resumeValue).get(toolCallId) ?? {
971
1087
  type: "reject",
972
1088
  reason: "No decision provided for tool approval"
973
1089
  };
974
1090
  const declaredType = decision.type;
975
- if (preResult.allowedDecisions != null && (typeof declaredType !== "string" || !preResult.allowedDecisions.includes(declaredType))) return this.blockDirectCall({
1091
+ if (preResult.allowedDecisions != null && (typeof declaredType !== "string" || !preResult.allowedDecisions.includes(declaredType))) return persistOutput(this.blockDirectCall({
976
1092
  call,
977
1093
  resolvedArgs,
978
1094
  reason: `Decision "${typeof declaredType === "string" ? declaredType : "<missing>"}" not in allowedDecisions [${preResult.allowedDecisions.join(", ")}] — failing closed`,
979
1095
  hookRegistry,
980
1096
  runId,
981
1097
  threadId
982
- });
983
- if (decision.type === "reject") return this.blockDirectCall({
1098
+ }), effectiveCall.args);
1099
+ if (decision.type === "reject") return persistOutput(this.blockDirectCall({
984
1100
  call,
985
1101
  resolvedArgs,
986
1102
  reason: decision.reason ?? preResult.reason ?? "Rejected by user",
987
1103
  hookRegistry,
988
1104
  runId,
989
1105
  threadId
990
- });
1106
+ }), effectiveCall.args);
991
1107
  if (decision.type === "respond") {
992
1108
  const responseText = decision.responseText;
993
- if (typeof responseText !== "string") return this.blockDirectCall({
1109
+ if (typeof responseText !== "string") return persistOutput(this.blockDirectCall({
994
1110
  call,
995
1111
  resolvedArgs,
996
1112
  reason: "Approval payload `respond` was missing a string `responseText`",
997
1113
  hookRegistry,
998
1114
  runId,
999
1115
  threadId
1000
- });
1001
- return new _langchain_core_messages.ToolMessage({
1116
+ }), effectiveCall.args);
1117
+ return persistOutput(new _langchain_core_messages.ToolMessage({
1002
1118
  status: "success",
1003
- content: responseText,
1119
+ content: require_truncation.truncateToolResultContent(responseText, this.maxToolResultChars),
1004
1120
  name: call.name,
1005
1121
  tool_call_id: call.id ?? ""
1006
- });
1122
+ }), effectiveCall.args);
1007
1123
  }
1008
1124
  if (decision.type === "edit") {
1009
1125
  const updatedInput = decision.updatedInput;
1010
- if (updatedInput === null || typeof updatedInput !== "object" || Array.isArray(updatedInput)) return new _langchain_core_messages.ToolMessage({
1126
+ if (updatedInput === null || typeof updatedInput !== "object" || Array.isArray(updatedInput)) return persistOutput(new _langchain_core_messages.ToolMessage({
1011
1127
  status: "error",
1012
1128
  content: "Decision \"edit\" missing object updatedInput — failing closed.",
1013
1129
  name: call.name,
1014
1130
  tool_call_id: call.id ?? ""
1015
- });
1131
+ }), effectiveCall.args);
1016
1132
  effectiveCall = {
1017
1133
  ...call,
1018
1134
  args: updatedInput
1019
1135
  };
1020
1136
  }
1137
+ if (declaredType !== "approve" && declaredType !== "edit") {
1138
+ const unknownType = typeof declaredType === "string" ? declaredType : "<missing>";
1139
+ return persistOutput(this.blockDirectCall({
1140
+ call,
1141
+ resolvedArgs,
1142
+ reason: `Unknown approval decision type "${unknownType}" — failing closed`,
1143
+ hookRegistry,
1144
+ runId,
1145
+ threadId
1146
+ }), effectiveCall.args);
1147
+ }
1021
1148
  }
1022
1149
  }
1023
1150
  }
@@ -1031,7 +1158,7 @@ var ToolNode = class extends require_run.RunnableCallable {
1031
1158
  * which is strictly more accurate.
1032
1159
  */
1033
1160
  if (effectiveCall !== call && batchContext.resolvedArgsByCallId != null && call.id != null && call.id !== "") batchContext.resolvedArgsByCallId.set(call.id, effectiveCall.args);
1034
- const output = await this.runTool(effectiveCall, config, {
1161
+ const output = await this.runTool(effectiveCall, replayConfig, {
1035
1162
  ...batchContext,
1036
1163
  usageCount
1037
1164
  });
@@ -1055,8 +1182,11 @@ var ToolNode = class extends require_run.RunnableCallable {
1055
1182
  sessionId: runId,
1056
1183
  matchQuery: call.name
1057
1184
  }).catch(() => void 0);
1058
- if (failureResult != null && batchContext.additionalContextsSink != null && failureResult.additionalContexts.length > 0) batchContext.additionalContextsSink.push(...failureResult.additionalContexts);
1059
- return output;
1185
+ if (failureResult != null && failureResult.additionalContexts.length > 0) {
1186
+ replayAdditionalContexts.push(...failureResult.additionalContexts);
1187
+ batchContext.additionalContextsSink?.push(...failureResult.additionalContexts);
1188
+ }
1189
+ return persistOutput(output);
1060
1190
  }
1061
1191
  if (output.status !== "error" && hasPostHook) {
1062
1192
  const postResult = await require_executeHooks.executeHooks({
@@ -1077,21 +1207,24 @@ var ToolNode = class extends require_run.RunnableCallable {
1077
1207
  sessionId: runId,
1078
1208
  matchQuery: call.name
1079
1209
  }).catch(() => void 0);
1080
- if (postResult != null && batchContext.additionalContextsSink != null && postResult.additionalContexts.length > 0) batchContext.additionalContextsSink.push(...postResult.additionalContexts);
1210
+ if (postResult != null && postResult.additionalContexts.length > 0) {
1211
+ replayAdditionalContexts.push(...postResult.additionalContexts);
1212
+ batchContext.additionalContextsSink?.push(...postResult.additionalContexts);
1213
+ }
1081
1214
  if (postResult?.updatedOutput != null) {
1082
1215
  if (require_toolContent.hasComputerCallOutputMarker(output)) {
1083
1216
  if (!require_toolContent.isComputerCallOutputContent(postResult.updatedOutput)) throw new Error("PostToolUse updatedOutput for a computer call must be a valid screenshot URL or screenshot content block.");
1084
- return require_toolContent.cloneToolMessageWithContent(output, postResult.updatedOutput);
1217
+ return persistOutput(require_toolContent.cloneToolMessageWithContent(output, postResult.updatedOutput));
1085
1218
  }
1086
1219
  const refMeta = output.additional_kwargs;
1087
1220
  const refKey = refMeta?._refKey;
1088
1221
  const refScope = refMeta?._refScope;
1089
1222
  const replaced = serializeToolOutputWithinLimits(postResult.updatedOutput, this.maxToolResultChars, this.toolOutputRegistry != null && refKey != null ? this.toolOutputRegistry.perOutputLimit : 0);
1090
1223
  if (this.toolOutputRegistry != null && refKey != null) this.toolOutputRegistry.set(refScope, refKey, replaced.registryContent);
1091
- return require_toolContent.cloneToolMessageWithContent(output, replaced.content);
1224
+ return persistOutput(require_toolContent.cloneToolMessageWithContent(output, replaced.content));
1092
1225
  }
1093
1226
  }
1094
- return output;
1227
+ return persistOutput(output);
1095
1228
  }
1096
1229
  /**
1097
1230
  * `ask` decisions on direct-path tools collapse to fail-closed deny
@@ -1383,13 +1516,10 @@ var ToolNode = class extends require_run.RunnableCallable {
1383
1516
  injectedMessages: [],
1384
1517
  errors: []
1385
1518
  });
1386
- if (this.hookRegistry?.hasHookFor("PreToolUse", runId) === true) {
1387
- /**
1388
- * Capture as a non-null local so the inner `blockEntry` closure
1389
- * doesn't lose narrowing on `this.hookRegistry` and we don't have
1390
- * to defensively `?.` it across every reference inside.
1391
- */
1392
- const hookRegistry = this.hookRegistry;
1519
+ const hookRegistry = this.hookRegistry;
1520
+ const approvalReplaySessionId = getToolApprovalReplaySessionId(config, runId);
1521
+ const hasPendingApproval = preToolCalls.some((entry) => entry.call.id != null && hookRegistry?.getPendingToolApproval(approvalReplaySessionId, createToolApprovalReplayKey(config, this.executingAgentId ?? this.agentId ?? "", entry.call.id)) != null);
1522
+ if (hookRegistry != null && (hookRegistry.hasHookFor("PreToolUse", runId) || hasPendingApproval)) {
1393
1523
  /**
1394
1524
  * Pull each call's prestarted eager record BEFORE awaiting the hooks:
1395
1525
  * an async deny must never race the eager host promise into emitting a
@@ -1409,23 +1539,29 @@ var ToolNode = class extends require_run.RunnableCallable {
1409
1539
  this.eagerEventToolExecutions.delete(callId);
1410
1540
  }
1411
1541
  }
1412
- const preResults = await Promise.all(preToolCalls.map((entry) => require_executeHooks.executeHooks({
1413
- registry: hookRegistry,
1414
- input: {
1415
- hook_event_name: "PreToolUse",
1416
- runId,
1417
- threadId,
1418
- agentId: this.agentId,
1419
- executingAgentId: this.executingAgentId,
1420
- toolName: entry.call.name,
1421
- toolInput: entry.args,
1422
- toolUseId: entry.call.id,
1423
- stepId: entry.stepId,
1424
- turn: this.toolUsageCount.get(entry.call.name) ?? 0
1425
- },
1426
- sessionId: runId,
1427
- matchQuery: entry.call.name
1428
- }).catch(() => HOOK_FALLBACK)));
1542
+ const preResults = await Promise.all(preToolCalls.map((entry) => {
1543
+ const toolUseId = entry.call.id;
1544
+ const approvalReplayKey = toolUseId == null ? void 0 : createToolApprovalReplayKey(config, this.executingAgentId ?? this.agentId ?? "", toolUseId);
1545
+ return require_executeHooks.executeHooks({
1546
+ registry: hookRegistry,
1547
+ input: {
1548
+ hook_event_name: "PreToolUse",
1549
+ runId,
1550
+ threadId,
1551
+ agentId: this.agentId,
1552
+ executingAgentId: this.executingAgentId,
1553
+ toolName: entry.call.name,
1554
+ toolInput: entry.args,
1555
+ toolUseId: entry.call.id,
1556
+ stepId: entry.stepId,
1557
+ turn: this.toolUsageCount.get(entry.call.name) ?? 0
1558
+ },
1559
+ sessionId: runId,
1560
+ matchQuery: entry.call.name,
1561
+ onceReplayKey: approvalReplayKey,
1562
+ onceReplaySessionId: approvalReplaySessionId
1563
+ }).catch(() => HOOK_FALLBACK);
1564
+ }));
1429
1565
  /**
1430
1566
  * Side effects deferred from `blockEntry` until after any pending
1431
1567
  * `interrupt()` resolves. Without deferral, a batch that mixes a
@@ -1582,12 +1718,11 @@ var ToolNode = class extends require_run.RunnableCallable {
1582
1718
  * pauses, gathers human input, and resumes the run with one
1583
1719
  * decision per request. On resume LangGraph re-executes this node
1584
1720
  * from the start; `interrupt()` then returns the resume value
1585
- * instead of throwing, so the loop above re-runs and the same
1586
- * `askEntries` list is rebuilt deterministically (assuming hooks
1587
- * are pure — see `humanInTheLoop` docs).
1721
+ * instead of throwing. Reusable hooks rebuild their entries, while
1722
+ * consumed one-shot hooks replay their pending approval aggregate.
1588
1723
  */
1589
1724
  if (askEntries.length > 0) {
1590
- const payload = buildToolApprovalInterruptPayload(askEntries);
1725
+ const payload = buildToolApprovalInterruptPayload(askEntries, runId);
1591
1726
  /**
1592
1727
  * `interrupt()` reads the current `RunnableConfig` from
1593
1728
  * AsyncLocalStorage. ToolNode usually runs with tracing disabled
@@ -1598,6 +1733,7 @@ var ToolNode = class extends require_run.RunnableCallable {
1598
1733
  * suspend and resume.
1599
1734
  */
1600
1735
  const resumeValue = _langchain_core_singletons.AsyncLocalStorageProviderSingleton.runWithConfig(config, () => (0, _langchain_langgraph.interrupt)(payload));
1736
+ for (const { entry } of askEntries) hookRegistry.clearPendingToolApproval(approvalReplaySessionId, createToolApprovalReplayKey(config, this.executingAgentId ?? this.agentId ?? "", entry.call.id));
1601
1737
  const decisionByCallId = normalizeApprovalDecisions(askEntries.map(({ entry }) => entry.call.id), resumeValue);
1602
1738
  for (const { entry, reason: askReason, allowedDecisions } of askEntries) {
1603
1739
  const decision = decisionByCallId.get(entry.call.id) ?? {
@@ -2189,16 +2325,16 @@ var ToolNode = class extends require_run.RunnableCallable {
2189
2325
  * call throws a `GraphInterrupt`, the `await` below rejects and unwinds the
2190
2326
  * whole ToolNode *before* any non-interrupting sibling has started — so a
2191
2327
  * sibling with real side effects (send_email, billing) never executes on
2192
- * the first pass. On the resume pass LangGraph re-runs the batch from the
2193
- * top; the interrupting tool resolves with the host's answer instead of
2194
- * throwing, and the siblings execute for the FIRST time, exactly once.
2328
+ * the first pass. Terminal interrupting siblings are cached by call id, so
2329
+ * LangGraph replay reuses their complete lifecycle output instead of
2330
+ * repeating model calls, hooks, or side effects.
2195
2331
  *
2196
2332
  * Without this ordering, a flat `Promise.all` starts every sibling
2197
2333
  * concurrently, so a non-idempotent sibling can complete its side effect
2198
2334
  * before the interrupt unwinds and then run a SECOND time on resume — the
2199
- * duplicate side effect this method exists to prevent. Interrupting tools
2200
- * are expected to be side-effect-free (they only suspend), so running them
2201
- * as a group and re-running them on resume is harmless.
2335
+ * duplicate side effect this method exists to prevent. A tool that actually
2336
+ * suspends re-enters until it reaches a terminal result; siblings that
2337
+ * already settled do not re-enter.
2202
2338
  *
2203
2339
  * `batchIndices[i]` is `directCalls[i]`'s position within the parent
2204
2340
  * ToolNode batch (used for `{{tool<i>turn<n>}}` registration); it is
@@ -2207,28 +2343,95 @@ var ToolNode = class extends require_run.RunnableCallable {
2207
2343
  * per-call here.
2208
2344
  */
2209
2345
  async runDirectBatchInterruptSafe(directCalls, batchIndices, config, baseContext) {
2210
- const runOne = (call, position) => this.runDirectToolWithLifecycleHooks(call, config, {
2211
- ...baseContext,
2212
- batchIndex: batchIndices[position]
2213
- });
2346
+ const settledBatchResults = baseContext.replayBatchKey == null ? void 0 : this.settledInterruptingResults.get(baseContext.replayBatchKey);
2347
+ const restoreResult = (call, result) => {
2348
+ baseContext.additionalContextsSink?.push(...result.additionalContexts);
2349
+ if (call.id != null && result.resolvedArgs != null && baseContext.resolvedArgsByCallId != null) baseContext.resolvedArgsByCallId.set(call.id, result.resolvedArgs);
2350
+ return result;
2351
+ };
2352
+ const runOne = async (call, position) => {
2353
+ const cachedResult = typeof call.id === "string" ? settledBatchResults?.get(call.id) : void 0;
2354
+ if (cachedResult != null) return restoreResult(call, cachedResult);
2355
+ const additionalContexts = [];
2356
+ const output = await this.runDirectToolWithLifecycleHooks(call, config, {
2357
+ ...baseContext,
2358
+ batchIndex: batchIndices[position],
2359
+ additionalContextsSink: additionalContexts
2360
+ });
2361
+ const resolvedArgs = call.id == null ? void 0 : baseContext.resolvedArgsByCallId?.get(call.id);
2362
+ return restoreResult(call, {
2363
+ output,
2364
+ additionalContexts,
2365
+ ...resolvedArgs == null ? {} : { resolvedArgs }
2366
+ });
2367
+ };
2214
2368
  const interrupting = this.interruptingToolNames;
2215
- if (!(interrupting != null && directCalls.some((call) => interrupting.has(call.name)))) return Promise.all(directCalls.map((call, i) => runOne(call, i)));
2369
+ if (!(interrupting != null && directCalls.some((call) => interrupting.has(call.name)))) return (await Promise.all(directCalls.map((call, i) => runOne(call, i)))).map((result) => result.output);
2216
2370
  const outputs = new Array(directCalls.length);
2217
2371
  const interruptingPositions = [];
2218
2372
  const regularPositions = [];
2219
2373
  for (let i = 0; i < directCalls.length; i++) if (interrupting.has(directCalls[i].name)) interruptingPositions.push(i);
2220
2374
  else regularPositions.push(i);
2221
- const interruptingOutputs = await Promise.all(interruptingPositions.map((i) => runOne(directCalls[i], i)));
2222
- interruptingPositions.forEach((i, k) => {
2223
- outputs[i] = interruptingOutputs[k];
2224
- });
2375
+ const interruptingResults = await Promise.allSettled(interruptingPositions.map((i) => runOne(directCalls[i], i)));
2376
+ /** A sibling may have tripped the run-wide breaker while another raised
2377
+ * a GraphInterrupt. Safety failures take precedence over approval pauses
2378
+ * regardless of input order. */
2379
+ this.throwIfBreakerTripped(config);
2380
+ let hasInterruptingError = false;
2381
+ let interruptingError;
2382
+ for (let i = 0; i < interruptingResults.length; i++) {
2383
+ const result = interruptingResults[i];
2384
+ if (result.status === "rejected") {
2385
+ if (!hasInterruptingError) {
2386
+ hasInterruptingError = true;
2387
+ interruptingError = result.reason;
2388
+ }
2389
+ continue;
2390
+ }
2391
+ const position = interruptingPositions[i];
2392
+ outputs[position] = result.value.output;
2393
+ const callId = directCalls[position].id;
2394
+ if (baseContext.replayBatchKey != null && typeof callId === "string" && callId !== "") {
2395
+ const batchResults = this.settledInterruptingResults.get(baseContext.replayBatchKey) ?? /* @__PURE__ */ new Map();
2396
+ batchResults.set(callId, result.value);
2397
+ this.settledInterruptingResults.set(baseContext.replayBatchKey, batchResults);
2398
+ }
2399
+ }
2400
+ if (hasInterruptingError) {
2401
+ if ((0, _langchain_langgraph.isGraphInterrupt)(interruptingError)) {
2402
+ const controllers = /* @__PURE__ */ new Set();
2403
+ const parentToolCallIds = /* @__PURE__ */ new Set();
2404
+ for (const call of directCalls) {
2405
+ const controller = this.toolMap.get(call.name)?.[require_SubagentReplay.SUBAGENT_REPLAY_CONTROLLER];
2406
+ if (controller?.getResumeManifest != null) controllers.add(controller);
2407
+ if (call.id != null && call.id !== "") parentToolCallIds.add(call.id);
2408
+ }
2409
+ const executionsByParentCall = /* @__PURE__ */ new Map();
2410
+ for (const controller of controllers) {
2411
+ const manifest = await controller.getResumeManifest?.(parentToolCallIds);
2412
+ if (manifest != null) for (const execution of manifest.executions) executionsByParentCall.set(execution.parentToolCallId, execution);
2413
+ }
2414
+ const executions = [...executionsByParentCall.values()];
2415
+ if (executions.length > 0) {
2416
+ const manifest = {
2417
+ version: 1,
2418
+ executions
2419
+ };
2420
+ throw new _langchain_langgraph.GraphInterrupt(interruptingError.interrupts.map((pendingInterrupt) => ({
2421
+ ...pendingInterrupt,
2422
+ value: require_SubagentReplay.attachSubagentResumeManifest(pendingInterrupt.value, manifest)
2423
+ })));
2424
+ }
2425
+ }
2426
+ throw interruptingError;
2427
+ }
2225
2428
  /** The breaker can trip while the interrupting group is awaited — e.g.
2226
2429
  * a tool that ignores cancellation and completes normally. Recheck
2227
2430
  * before starting the non-idempotent siblings. */
2228
2431
  this.throwIfBreakerTripped(config);
2229
2432
  const regularOutputs = await Promise.all(regularPositions.map((i) => runOne(directCalls[i], i)));
2230
2433
  regularPositions.forEach((i, k) => {
2231
- outputs[i] = regularOutputs[k];
2434
+ outputs[i] = regularOutputs[k].output;
2232
2435
  });
2233
2436
  return outputs;
2234
2437
  }
@@ -2284,9 +2487,23 @@ var ToolNode = class extends require_run.RunnableCallable {
2284
2487
  const resolvedArgsByCallId = /* @__PURE__ */ new Map();
2285
2488
  /** Per-invocation error-completion ownership — see `ToolErrorOwnership`. */
2286
2489
  const errorOwnership = createToolErrorOwnership();
2287
- const batchScopeId = config.configurable?.run_id ?? `\0anon-${this.anonBatchCounter++}`;
2490
+ /**
2491
+ * Claim this batch's turn synchronously from the registry (or
2492
+ * fall back to 0 when the feature is disabled). The registry is
2493
+ * partitioned by scope id so overlapping batches cannot
2494
+ * overwrite each other's state even under a shared registry.
2495
+ *
2496
+ * For anonymous callers (no `run_id` in config), mint a unique
2497
+ * per-batch scope id so two concurrent anonymous invocations
2498
+ * don't target the same bucket. The scope is threaded down to
2499
+ * every subsequent registry call on this batch.
2500
+ */
2501
+ const incomingRunId = config.configurable?.run_id;
2502
+ const incomingThreadId = config.configurable?.thread_id;
2503
+ const batchScopeId = incomingRunId ?? `\0anon-${this.anonBatchCounter++}`;
2288
2504
  const turn = this.toolOutputRegistry?.nextTurn(batchScopeId) ?? 0;
2289
2505
  let outputs;
2506
+ let replayBatchKey;
2290
2507
  /** Hoisted from the messages-state branch so the Command tail can carry
2291
2508
  * the promotion into handoff updates (same-id state copies there would
2292
2509
  * otherwise overwrite the replacement message). */
@@ -2301,6 +2518,9 @@ var ToolNode = class extends require_run.RunnableCallable {
2301
2518
  });
2302
2519
  const directAdditionalContexts = [];
2303
2520
  const { lg_tool_call: _sendToolCall, ...sendState } = input;
2521
+ const sendMessages = sendState.messages;
2522
+ const sendAssistantBatch = Array.isArray(sendMessages) ? findAssistantBatch(sendMessages) : void 0;
2523
+ const sendReplayBatchKey = sendAssistantBatch == null ? void 0 : getAssistantBatchReplayKey(sendAssistantBatch, incomingRunId, incomingThreadId);
2304
2524
  const sendOutput = await this.runDirectToolWithLifecycleHooks(input.lg_tool_call, config, {
2305
2525
  batchIndex: 0,
2306
2526
  turn,
@@ -2308,6 +2528,7 @@ var ToolNode = class extends require_run.RunnableCallable {
2308
2528
  resolvedArgsByCallId,
2309
2529
  errorOwnership,
2310
2530
  additionalContextsSink: directAdditionalContexts,
2531
+ replayBatchKey: sendReplayBatchKey,
2311
2532
  runInput: sendState
2312
2533
  });
2313
2534
  outputs = directAdditionalContexts.length > 0 ? [sendOutput, new _langchain_core_messages.HumanMessage({
@@ -2324,15 +2545,10 @@ var ToolNode = class extends require_run.RunnableCallable {
2324
2545
  else if (this.isMessagesState(input)) messages = input.messages;
2325
2546
  else throw new Error("ToolNode only accepts BaseMessage[] or { messages: BaseMessage[] } as input.");
2326
2547
  const toolMessageIds = new Set(messages.filter((msg) => msg._getType() === "tool").map((msg) => msg.tool_call_id));
2327
- let aiMessage;
2328
- for (let i = messages.length - 1; i >= 0; i--) {
2329
- const message = messages[i];
2330
- if ((0, _langchain_core_messages.isAIMessage)(message)) {
2331
- aiMessage = message;
2332
- break;
2333
- }
2334
- }
2335
- if (aiMessage == null || !(0, _langchain_core_messages.isAIMessage)(aiMessage)) throw new Error("ToolNode only accepts AIMessages as input.");
2548
+ const assistantBatch = findAssistantBatch(messages);
2549
+ if (assistantBatch == null) throw new Error("ToolNode only accepts AIMessages as input.");
2550
+ const aiMessage = assistantBatch.message;
2551
+ replayBatchKey = getAssistantBatchReplayKey(assistantBatch, incomingRunId, incomingThreadId);
2336
2552
  if (this.loadRuntimeTools) {
2337
2553
  const { tools, toolMap } = this.loadRuntimeTools(aiMessage.tool_calls ?? []);
2338
2554
  this.toolMap = toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
@@ -2447,6 +2663,7 @@ var ToolNode = class extends require_run.RunnableCallable {
2447
2663
  errorOwnership,
2448
2664
  preBatchSnapshot,
2449
2665
  additionalContextsSink: directAdditionalContexts,
2666
+ replayBatchKey,
2450
2667
  runInput: input
2451
2668
  }) : [];
2452
2669
  if (directCalls.length > 0 && directOutputs.length > 0) await this.handleRunToolCompletions(directCalls, directOutputs, config, resolvedArgsByCallId, errorOwnership);
@@ -2485,6 +2702,7 @@ var ToolNode = class extends require_run.RunnableCallable {
2485
2702
  errorOwnership,
2486
2703
  preBatchSnapshot,
2487
2704
  additionalContextsSink: directAdditionalContexts,
2705
+ replayBatchKey,
2488
2706
  runInput: input
2489
2707
  });
2490
2708
  await this.handleRunToolCompletions(filteredCalls, toolOutputs, config, resolvedArgsByCallId, errorOwnership);
@@ -2521,7 +2739,10 @@ var ToolNode = class extends require_run.RunnableCallable {
2521
2739
  await this.dispatchStepCompleted(result.tool_call_id, result.name ?? "unknown", {}, typeof result.content === "string" ? result.content : "", config);
2522
2740
  }
2523
2741
  }
2524
- if (!outputs.some(_langchain_langgraph.isCommand)) return Array.isArray(input) ? outputs : { messages: outputs };
2742
+ if (!outputs.some(_langchain_langgraph.isCommand)) {
2743
+ if (replayBatchKey != null) this.settledInterruptingResults.delete(replayBatchKey);
2744
+ return Array.isArray(input) ? outputs : { messages: outputs };
2745
+ }
2525
2746
  /**
2526
2747
  * Carry the invalid-call promotion into handoff commands. A handoff
2527
2748
  * tool's Command snapshots `update.messages` from the PRE-promotion
@@ -2608,6 +2829,7 @@ var ToolNode = class extends require_run.RunnableCallable {
2608
2829
  /** Single handoff - pass through as-is */
2609
2830
  combinedOutputs.push(handoffCommands[0]);
2610
2831
  if (parentCommand) combinedOutputs.push(parentCommand);
2832
+ if (replayBatchKey != null) this.settledInterruptingResults.delete(replayBatchKey);
2611
2833
  return combinedOutputs;
2612
2834
  }
2613
2835
  isSendInput(input) {