@librechat/agents 3.4.0 → 3.4.2

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