@librechat/agents 3.8.1 → 3.8.3

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 (96) hide show
  1. package/dist/cjs/graphs/Graph.cjs +2 -2
  2. package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
  3. package/dist/cjs/hitl/approvalReview.cjs +89 -0
  4. package/dist/cjs/hitl/approvalReview.cjs.map +1 -0
  5. package/dist/cjs/hooks/index.cjs +2 -0
  6. package/dist/cjs/hooks/index.cjs.map +1 -1
  7. package/dist/cjs/hooks/types.cjs.map +1 -1
  8. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +1 -1
  9. package/dist/cjs/main.cjs +5 -3
  10. package/dist/cjs/messages/core.cjs +1 -1
  11. package/dist/cjs/messages/format.cjs +1 -1
  12. package/dist/cjs/messages/prune.cjs +2 -2
  13. package/dist/cjs/run.cjs +17 -6
  14. package/dist/cjs/run.cjs.map +1 -1
  15. package/dist/cjs/session/AgentSession.cjs +1 -1
  16. package/dist/cjs/stream.cjs +2 -2
  17. package/dist/cjs/summarization/node.cjs +1 -1
  18. package/dist/cjs/tools/BashExecutor.cjs +2 -3
  19. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  20. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +3 -2
  21. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  22. package/dist/cjs/tools/CodeExecutor.cjs +63 -11
  23. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  24. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +8 -7
  25. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  26. package/dist/cjs/tools/ToolNode.cjs +271 -84
  27. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  28. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +24 -9
  29. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  30. package/dist/cjs/tools/subagent/SubagentReplay.cjs +2 -0
  31. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
  32. package/dist/cjs/tools/toolBatchReplay.cjs +198 -0
  33. package/dist/cjs/tools/toolBatchReplay.cjs.map +1 -0
  34. package/dist/cjs/tools/toolOutputReferences.cjs +12 -0
  35. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  36. package/dist/cjs/types/hitl.cjs +4 -0
  37. package/dist/cjs/types/hitl.cjs.map +1 -1
  38. package/dist/esm/graphs/Graph.mjs +2 -2
  39. package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
  40. package/dist/esm/hitl/approvalReview.mjs +83 -0
  41. package/dist/esm/hitl/approvalReview.mjs.map +1 -0
  42. package/dist/esm/hooks/index.mjs +2 -1
  43. package/dist/esm/hooks/index.mjs.map +1 -1
  44. package/dist/esm/hooks/types.mjs.map +1 -1
  45. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +1 -1
  46. package/dist/esm/main.mjs +6 -6
  47. package/dist/esm/messages/core.mjs +1 -1
  48. package/dist/esm/messages/format.mjs +1 -1
  49. package/dist/esm/messages/prune.mjs +2 -2
  50. package/dist/esm/run.mjs +18 -7
  51. package/dist/esm/run.mjs.map +1 -1
  52. package/dist/esm/session/AgentSession.mjs +1 -1
  53. package/dist/esm/stream.mjs +2 -2
  54. package/dist/esm/summarization/node.mjs +1 -1
  55. package/dist/esm/tools/BashExecutor.mjs +2 -3
  56. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  57. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +3 -2
  58. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  59. package/dist/esm/tools/CodeExecutor.mjs +63 -12
  60. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  61. package/dist/esm/tools/ProgrammaticToolCalling.mjs +8 -7
  62. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  63. package/dist/esm/tools/ToolNode.mjs +271 -84
  64. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  65. package/dist/esm/tools/subagent/SubagentExecutor.mjs +24 -9
  66. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  67. package/dist/esm/tools/subagent/SubagentReplay.mjs +1 -1
  68. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
  69. package/dist/esm/tools/toolBatchReplay.mjs +186 -0
  70. package/dist/esm/tools/toolBatchReplay.mjs.map +1 -0
  71. package/dist/esm/tools/toolOutputReferences.mjs +12 -0
  72. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  73. package/dist/esm/types/hitl.mjs +4 -1
  74. package/dist/esm/types/hitl.mjs.map +1 -1
  75. package/dist/types/hitl/approvalReview.d.ts +32 -0
  76. package/dist/types/hooks/index.d.ts +2 -0
  77. package/dist/types/hooks/types.d.ts +6 -0
  78. package/dist/types/tools/CodeExecutor.d.ts +2 -0
  79. package/dist/types/tools/ToolNode.d.ts +12 -6
  80. package/dist/types/tools/subagent/SubagentReplay.d.ts +2 -0
  81. package/dist/types/tools/toolBatchReplay.d.ts +63 -0
  82. package/dist/types/tools/toolOutputReferences.d.ts +2 -0
  83. package/package.json +1 -1
  84. package/src/hitl/approvalReview.ts +209 -0
  85. package/src/hooks/index.ts +3 -0
  86. package/src/hooks/types.ts +6 -0
  87. package/src/run.ts +34 -20
  88. package/src/tools/BashExecutor.ts +2 -2
  89. package/src/tools/BashProgrammaticToolCalling.ts +5 -3
  90. package/src/tools/CodeExecutor.ts +111 -17
  91. package/src/tools/ProgrammaticToolCalling.ts +10 -8
  92. package/src/tools/ToolNode.ts +536 -184
  93. package/src/tools/subagent/SubagentExecutor.ts +35 -6
  94. package/src/tools/subagent/SubagentReplay.ts +2 -2
  95. package/src/tools/toolBatchReplay.ts +423 -0
  96. package/src/tools/toolOutputReferences.ts +16 -0
@@ -1,19 +1,21 @@
1
- import { calculateMaxToolResultChars, truncateToolResultContent } from "../utils/truncation.mjs";
2
1
  import { CODE_EXECUTION_TOOLS } from "../common/enum.mjs";
3
2
  import "../common/index.mjs";
3
+ import { calculateMaxToolResultChars, truncateToolResultContent } from "../utils/truncation.mjs";
4
4
  import { attachRunStepResumeState } from "./runStepResume.mjs";
5
5
  import { SUBAGENT_PARENT_BATCH_CONFIG_KEY, SUBAGENT_REPLAY_CONTROLLER, attachSubagentResumeManifest } from "./subagent/SubagentReplay.mjs";
6
6
  import { composeAbortSignals } from "../utils/misc.mjs";
7
7
  import { resolveLangfuseRuntimeScope, withLangfuseRuntimeScope } from "../langfuseRuntimeScope.mjs";
8
8
  import { cloneToolMessageWithContent, compactToolContent, hasComputerCallOutputMarker, isComputerCallOutputContent, isComputerCallOutputMessage, serializeStructuredValueBounded, serializeToolContentBounded } from "../utils/toolContent.mjs";
9
9
  import { stampSyntheticProviderMessage } from "../messages/provenance.mjs";
10
+ import { buildToolExecutionRequestPlan, coerceArgsForSchema, coerceRecordArgs, recordArgsEqual, resolveRuntimeSessionHint, stableStringify } from "./eagerEventExecution.mjs";
11
+ import { TOOL_APPROVAL_REVIEW_CONFIG_KEY, getReviewedToolApproval, getToolApprovalReviewEvidence, toolApprovalPayloadMatches, toolApprovalProposalMatches } from "../hitl/approvalReview.mjs";
10
12
  import { TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY } from "../hooks/types.mjs";
13
+ import { TOOL_BATCH_REPLAY_KEY, attachToolBatchReplayState, getToolBatchReplayOwner, getToolBatchReplayScope, getToolBatchReplayState, isChildToolReplayOwner, isToolReplayResume, restoreToolBatchReplayState } from "./toolBatchReplay.mjs";
11
14
  import { executeHooks } from "../hooks/executeHooks.mjs";
12
15
  import "../hooks/index.mjs";
13
16
  import { convertInjectedMessages } from "../messages/injected.mjs";
14
17
  import { safeDispatchCustomEvent } from "../utils/events.mjs";
15
18
  import { RUN_BREAKER_SCOPE_CONFIG_KEY, StreamLimitExceededError } from "../llm/streamLimits.mjs";
16
- import { buildToolExecutionRequestPlan, coerceArgsForSchema, coerceRecordArgs, recordArgsEqual, resolveRuntimeSessionHint } from "./eagerEventExecution.mjs";
17
19
  import { INTENT_ARG, isIntentLabelProperty, outcomeFieldsFromResult, readOutcomeFields, resolveToolOutcome } from "./intentArg.mjs";
18
20
  import { ToolOutputReferenceRegistry, buildReferenceKey } from "./toolOutputReferences.mjs";
19
21
  import { PreparedSubagentError } from "./preparedSubagents.mjs";
@@ -28,10 +30,17 @@ import { RunnableLambda } from "@langchain/core/runnables";
28
30
  import { AIMessage, HumanMessage, ToolMessage, isAIMessage, isBaseMessage } from "@langchain/core/messages";
29
31
  import { Command, END, GraphInterrupt, Send, interrupt, isCommand, isGraphInterrupt } from "@langchain/langgraph";
30
32
  import { AsyncLocalStorageProviderSingleton } from "@langchain/core/singletons";
33
+ import { createHash } from "crypto";
31
34
  //#region src/tools/ToolNode.ts
32
- function stripRunBreakerScope(configurable) {
33
- if (configurable == null || !("lc_run_breaker_scope" in configurable)) return configurable;
34
- const { [RUN_BREAKER_SCOPE_CONFIG_KEY]: _scope, ...rest } = configurable;
35
+ function stripToolApprovalReviewConfig(configurable) {
36
+ if (configurable == null || !("__librechat_tool_approval_review" in configurable) && !("__librechat_tool_batch_replay" in configurable)) return configurable;
37
+ const { [TOOL_APPROVAL_REVIEW_CONFIG_KEY]: _review, [TOOL_BATCH_REPLAY_KEY]: _replay, ...rest } = configurable;
38
+ return rest;
39
+ }
40
+ function stripHostExecutionConfig(configurable) {
41
+ const withoutReview = stripToolApprovalReviewConfig(configurable);
42
+ if (withoutReview == null || !("lc_run_breaker_scope" in withoutReview)) return withoutReview;
43
+ const { [RUN_BREAKER_SCOPE_CONFIG_KEY]: _scope, ...rest } = withoutReview;
35
44
  return rest;
36
45
  }
37
46
  function createToolApprovalReplayKey(config, agentId, toolUseId) {
@@ -60,6 +69,9 @@ function withSubagentReplayBatch(config, replayBatchKey) {
60
69
  }
61
70
  };
62
71
  }
72
+ function directReplayKey(call, batchIndex) {
73
+ return call.id == null || call.id === "" ? JSON.stringify(["position", batchIndex]) : JSON.stringify(["id", call.id]);
74
+ }
63
75
  function createToolErrorOwnership() {
64
76
  return {
65
77
  handlerOwned: /* @__PURE__ */ new Set(),
@@ -113,20 +125,14 @@ function describeOfferedShape(value) {
113
125
  function findAssistantBatch(messages) {
114
126
  for (let i = messages.length - 1; i >= 0; i--) {
115
127
  const message = messages[i];
116
- if (isAIMessage(message)) return {
117
- message,
118
- index: i,
119
- messageCount: messages.length
120
- };
128
+ if (isAIMessage(message)) return { message };
121
129
  }
122
130
  }
123
- function getAssistantBatchReplayKey(batch, runId, threadId) {
131
+ function getAssistantBatchReplayKey(batch, threadId) {
124
132
  return JSON.stringify([
125
- runId ?? null,
126
133
  threadId ?? null,
127
- batch.message.id ?? null,
128
- batch.index,
129
- batch.messageCount
134
+ batch.message.id == null || batch.message.id === "" ? null : batch.message.id,
135
+ createHash("sha256").update(stableStringify(batch.message.tool_calls ?? [])).digest("hex")
130
136
  ]);
131
137
  }
132
138
  function buildToolApprovalInterruptPayload(askEntries, hookSessionId) {
@@ -244,7 +250,7 @@ var ToolNode = class extends RunnableCallable {
244
250
  toolUsageCount;
245
251
  toolCallTurns = /* @__PURE__ */ new Map();
246
252
  directPathTurns = /* @__PURE__ */ new Map();
247
- settledInterruptingResults = /* @__PURE__ */ new Map();
253
+ settledDirectResultsByBatch = /* @__PURE__ */ new Map();
248
254
  toolRegistry;
249
255
  toolDefinitions;
250
256
  localImplementationNames = /* @__PURE__ */ new Set();
@@ -279,16 +285,59 @@ var ToolNode = class extends RunnableCallable {
279
285
  name: name ?? TOOL_NODE_RUN_NAME,
280
286
  tags,
281
287
  func: async (input, config) => {
288
+ const messages = Array.isArray(input) ? input : input.messages;
289
+ const assistantBatch = messages == null ? void 0 : findAssistantBatch(messages);
290
+ const activeReplayKey = assistantBatch == null || getToolBatchReplayScope(config) == null ? void 0 : getAssistantBatchReplayKey(assistantBatch, getToolBatchReplayScope(config));
291
+ const replayOwner = getToolBatchReplayOwner(config, this.executingAgentId ?? this.agentId ?? "");
292
+ let reviewEvidence = getToolApprovalReviewEvidence(config);
293
+ const replayState = getToolBatchReplayState(config.configurable);
294
+ const ownsParentBatch = replayState?.records.some((record) => record.owner === replayOwner && record.batch === activeReplayKey) === true;
295
+ let isChildReplay = false;
296
+ if (ownsParentBatch) {
297
+ const calls = this.isSendInput(input) ? [input.lg_tool_call] : assistantBatch?.message.tool_calls ?? [];
298
+ const trustedParentCallIds = new Set(calls.filter((call) => call.name === "subagent" && this.toolMap.get(call.name)?.[SUBAGENT_REPLAY_CONTROLLER] != null).map((call) => call.id ?? ""));
299
+ isChildReplay = reviewEvidence?.owner != null ? isChildToolReplayOwner(config, reviewEvidence.owner, trustedParentCallIds) : replayState.records.some((record) => isChildToolReplayOwner(config, record.owner, trustedParentCallIds));
300
+ }
301
+ if ((reviewEvidence?.owner != null || replayState != null) && !isToolReplayResume(config, reviewEvidence?.interruptId ?? replayState?.interruptId, isChildReplay)) {
302
+ config = {
303
+ ...config,
304
+ configurable: {
305
+ ...config.configurable,
306
+ [TOOL_APPROVAL_REVIEW_CONFIG_KEY]: void 0,
307
+ [TOOL_BATCH_REPLAY_KEY]: void 0
308
+ }
309
+ };
310
+ reviewEvidence = void 0;
311
+ }
312
+ if (reviewEvidence?.owner != null && reviewEvidence.owner !== replayOwner && !isChildReplay) throw new Error("Tool approval execution owner changed before resume — failing closed");
313
+ const referenceReplay = {};
314
+ const restoredBatches = await restoreToolBatchReplayState(config, replayOwner);
315
+ if (restoredBatches.length > 0 && !restoredBatches.some(({ batch }) => batch === activeReplayKey)) throw new Error("Tool batch proposal changed before approval replay — failing closed");
316
+ if (activeReplayKey != null && getToolBatchReplayState(config.configurable) != null) this.settledDirectResultsByBatch.delete(activeReplayKey);
317
+ for (const { batch, results, turnState, referenceState } of restoredBatches) {
318
+ if (batch !== activeReplayKey) continue;
319
+ this.settledDirectResultsByBatch.set(batch, new Map(results));
320
+ if (turnState != null) this.restoreSubagentResumeState(turnState);
321
+ referenceReplay.state = referenceState;
322
+ }
282
323
  restoreRunStepResumeState?.(input.runStepState, config);
283
324
  let result;
284
325
  try {
285
- result = await this.run(input, config);
326
+ result = await this.run(input, config, referenceReplay);
286
327
  } catch (error) {
287
- if (!isGraphInterrupt(error) || createRunStepResumeState == null) throw error;
288
- const resumeState = createRunStepResumeState();
289
- throw new GraphInterrupt(error.interrupts.map((pendingInterrupt) => ({
290
- ...pendingInterrupt,
291
- value: attachRunStepResumeState(pendingInterrupt.value, resumeState)
328
+ if (!isGraphInterrupt(error)) throw error;
329
+ const resumeState = createRunStepResumeState?.();
330
+ const activeResults = activeReplayKey == null ? void 0 : this.settledDirectResultsByBatch.get(activeReplayKey);
331
+ const activeBatches = activeReplayKey == null ? /* @__PURE__ */ new Map() : new Map([[activeReplayKey, activeResults ?? /* @__PURE__ */ new Map()]]);
332
+ const turnState = this.createSubagentResumeState();
333
+ const activeCallIds = new Set(assistantBatch?.message.tool_calls?.map((call) => call.id));
334
+ turnState.directPathTurns = turnState.directPathTurns.filter(({ toolCallId }) => activeCallIds.has(toolCallId));
335
+ throw new GraphInterrupt(await Promise.all(error.interrupts.map(async (pendingInterrupt) => {
336
+ const value = await attachToolBatchReplayState(pendingInterrupt.value, replayOwner, activeBatches, turnState, referenceReplay.state);
337
+ return {
338
+ ...pendingInterrupt,
339
+ value: resumeState == null ? value : attachRunStepResumeState(value, resumeState)
340
+ };
292
341
  })));
293
342
  }
294
343
  if (createRunStepResumeState == null) return result;
@@ -416,7 +465,7 @@ var ToolNode = class extends RunnableCallable {
416
465
  }
417
466
  clearDirectPathTurns() {
418
467
  this.directPathTurns.clear();
419
- this.settledInterruptingResults.clear();
468
+ this.settledDirectResultsByBatch.clear();
420
469
  }
421
470
  getCallerCapabilityProjection() {
422
471
  const discoveredToolNames = new Set(this.getDiscoveredToolNames?.() ?? []);
@@ -486,11 +535,16 @@ var ToolNode = class extends RunnableCallable {
486
535
  }
487
536
  invokeWithRuntime(tool, invokeParams, call, config, runInput) {
488
537
  const lgConfig = config;
489
- const runtime = {
538
+ const isTrustedSubagentHop = call.name === "subagent" && tool[SUBAGENT_REPLAY_CONTROLLER] != null;
539
+ const toolConfig = {
490
540
  ...config,
541
+ configurable: isTrustedSubagentHop ? config.configurable : stripToolApprovalReviewConfig(config.configurable)
542
+ };
543
+ const runtime = {
544
+ ...toolConfig,
491
545
  state: runInput,
492
546
  toolCallId: call.id ?? "",
493
- config,
547
+ config: toolConfig,
494
548
  context: lgConfig.context,
495
549
  store: lgConfig.store ?? null,
496
550
  writer: lgConfig.writer ?? config.configurable?.writer ?? null
@@ -576,6 +630,8 @@ var ToolNode = class extends RunnableCallable {
576
630
  if (call.id != null && call.id !== "") this.toolCallTurns.set(call.id, next);
577
631
  return next;
578
632
  })();
633
+ this.recordToolUsageTurn(call.name, usageCount, call.id);
634
+ if (batchContext.replayBatchKey != null && call.id != null && call.id !== "") this.directPathTurns.set(call.id, usageCount);
579
635
  let args = call.args;
580
636
  if (resolveFn != null) {
581
637
  const { resolved, unresolved } = resolveFn(runId, args);
@@ -724,6 +780,7 @@ var ToolNode = class extends RunnableCallable {
724
780
  throw new PreparedSubagentError("Tool hooks changed after a subagent started.");
725
781
  }
726
782
  const replayController = this.toolMap.get(call.name)?.[SUBAGENT_REPLAY_CONTROLLER];
783
+ const isTrustedSubagentReviewHop = call.name === "subagent" && replayController != null;
727
784
  const replayConfig = replayController == null ? config : withSubagentReplayBatch(config, batchContext.replayBatchKey);
728
785
  const settledOutput = await replayController?.getSettledOutput(call, replayConfig);
729
786
  if (settledOutput != null) {
@@ -754,9 +811,12 @@ var ToolNode = class extends RunnableCallable {
754
811
  const approvalReplayKey = call.id == null || call.id === "" ? void 0 : createToolApprovalReplayKey(config, this.executingAgentId ?? this.agentId ?? "", call.id);
755
812
  const approvalReplaySessionId = getToolApprovalReplaySessionId(config, runId);
756
813
  const pendingApproval = approvalReplayKey == null ? void 0 : hookRegistry?.getPendingToolApproval(approvalReplaySessionId, approvalReplayKey);
814
+ const approvalReviewEvidence = getToolApprovalReviewEvidence(config, getToolBatchReplayOwner(config, this.executingAgentId ?? this.agentId ?? ""));
815
+ const reviewedApproval = getReviewedToolApproval(approvalReviewEvidence?.payload, call.id);
816
+ const approvalEvidenceAppliesToBatch = approvalReviewEvidence?.payload.action_requests.some((request) => batchContext.approvalScopeCallIds == null ? request.tool_call_id === call.id : batchContext.approvalScopeCallIds.has(request.tool_call_id)) === true;
757
817
  const hasPostHook = hookRegistry?.hasHookFor("PostToolUse", runId) === true;
758
818
  const hasFailureHook = hookRegistry?.hasHookFor("PostToolUseFailure", runId) === true;
759
- if (hookRegistry == null || !hasPreHook && pendingApproval == null && !hasPostHook && !hasFailureHook) {
819
+ if (!hasPreHook && pendingApproval == null && reviewedApproval == null && approvalReviewEvidence == null && !hasPostHook && !hasFailureHook) {
760
820
  const output = await this.runTool(call, replayConfig, batchContext);
761
821
  return output instanceof ToolMessage ? persistOutput(output) : output;
762
822
  }
@@ -774,18 +834,21 @@ var ToolNode = class extends RunnableCallable {
774
834
  }
775
835
  const turn = usageCount;
776
836
  const stepId = this.toolCallStepIds?.get(call.id ?? "") ?? "";
777
- let resolvedArgs = call.args;
778
837
  const hookResolveOptions = { substituteIntentKey: this.toolDeclaresBusinessIntent(call.name) };
779
- if (batchContext.preBatchSnapshot != null) {
780
- const { resolved } = batchContext.preBatchSnapshot.resolve(call.args, hookResolveOptions);
781
- resolvedArgs = resolved;
782
- } else if (this.toolOutputRegistry != null) {
783
- const { resolved } = this.toolOutputRegistry.resolve(registryRunId, call.args, hookResolveOptions);
784
- resolvedArgs = resolved;
785
- }
838
+ const resolveDirectArgs = (args) => {
839
+ if (batchContext.preBatchSnapshot != null) return batchContext.preBatchSnapshot.resolve(args, hookResolveOptions).resolved;
840
+ if (this.toolOutputRegistry != null) return this.toolOutputRegistry.resolve(registryRunId, args, hookResolveOptions).resolved;
841
+ return args;
842
+ };
843
+ const originalResolvedArgs = resolveDirectArgs(call.args);
844
+ let resolvedArgs = hasPreHook || pendingApproval != null ? originalResolvedArgs : reviewedApproval?.request.arguments ?? originalResolvedArgs;
786
845
  let effectiveCall = call;
787
- if (hasPreHook || pendingApproval != null) {
788
- const preResult = await executeHooks({
846
+ if (hasPreHook || pendingApproval != null || reviewedApproval != null || approvalReviewEvidence != null) {
847
+ const preResult = hookRegistry == null ? {
848
+ additionalContexts: [],
849
+ injectedMessages: [],
850
+ errors: []
851
+ } : await executeHooks({
789
852
  registry: hookRegistry,
790
853
  input: {
791
854
  hook_event_name: "PreToolUse",
@@ -803,16 +866,31 @@ var ToolNode = class extends RunnableCallable {
803
866
  matchQuery: call.name,
804
867
  onceReplayKey: approvalReplayKey,
805
868
  onceReplaySessionId: approvalReplaySessionId
806
- }).catch(() => void 0);
869
+ }).catch(() => approvalReviewEvidence == null ? void 0 : {
870
+ decision: "deny",
871
+ reason: "Approval policy could not be evaluated on resume",
872
+ additionalContexts: [],
873
+ injectedMessages: [],
874
+ errors: []
875
+ });
807
876
  if (preResult != null) {
808
877
  if (preResult.additionalContexts.length > 0) {
809
878
  replayAdditionalContexts.push(...preResult.additionalContexts);
810
879
  batchContext.additionalContextsSink?.push(...preResult.additionalContexts);
811
880
  }
812
- if (preResult.updatedInput != null) effectiveCall = {
813
- ...call,
814
- args: preResult.updatedInput
815
- };
881
+ if (preResult.updatedInput != null) {
882
+ resolvedArgs = resolveDirectArgs(preResult.updatedInput);
883
+ effectiveCall = {
884
+ ...call,
885
+ args: resolvedArgs
886
+ };
887
+ } else if (reviewedApproval != null) {
888
+ resolvedArgs = reviewedApproval.request.arguments;
889
+ effectiveCall = {
890
+ ...call,
891
+ args: resolvedArgs
892
+ };
893
+ }
816
894
  if (preResult.decision === "deny") return persistOutput(this.blockDirectCall({
817
895
  call,
818
896
  resolvedArgs,
@@ -821,7 +899,15 @@ var ToolNode = class extends RunnableCallable {
821
899
  runId,
822
900
  threadId
823
901
  }), effectiveCall.args);
824
- if (preResult.decision === "ask") {
902
+ if (approvalEvidenceAppliesToBatch && reviewedApproval == null && !isTrustedSubagentReviewHop && preResult.decision !== "ask") return persistOutput(this.blockDirectCall({
903
+ call,
904
+ resolvedArgs,
905
+ reason: "Tool call was not included in the restored approval batch — failing closed",
906
+ hookRegistry,
907
+ runId,
908
+ threadId
909
+ }), resolvedArgs);
910
+ if (preResult.decision === "ask" || reviewedApproval != null) {
825
911
  if (this.humanInTheLoop?.enabled !== true) {
826
912
  const reason = this.resolveAskDecisionForDirectTool(preResult.reason, call.name);
827
913
  return persistOutput(this.blockDirectCall({
@@ -842,26 +928,41 @@ var ToolNode = class extends RunnableCallable {
842
928
  runId,
843
929
  threadId
844
930
  }), effectiveCall.args);
931
+ const allowedDecisions = preResult.decision === "ask" ? preResult.allowedDecisions : reviewedApproval?.reviewConfig.allowed_decisions;
932
+ const askReason = preResult.decision === "ask" ? preResult.reason : reviewedApproval?.request.description;
933
+ if (effectiveCall === call) effectiveCall = {
934
+ ...call,
935
+ args: resolvedArgs
936
+ };
845
937
  const payload = buildToolApprovalInterruptPayload([{
846
938
  entry: {
847
939
  call: effectiveCall,
848
940
  args: effectiveCall.args,
849
941
  stepId
850
942
  },
851
- reason: preResult.reason,
852
- allowedDecisions: preResult.allowedDecisions
943
+ reason: askReason,
944
+ allowedDecisions
853
945
  }], runId);
854
946
  const resumeValue = AsyncLocalStorageProviderSingleton.runWithConfig(config, () => interrupt(payload));
855
- hookRegistry.clearPendingToolApproval(approvalReplaySessionId, approvalReplayKey);
947
+ hookRegistry?.clearPendingToolApproval(approvalReplaySessionId, approvalReplayKey);
948
+ const currentApproval = getReviewedToolApproval(payload, toolCallId);
949
+ if (reviewedApproval != null && (currentApproval == null || !toolApprovalProposalMatches(currentApproval, reviewedApproval))) return persistOutput(this.blockDirectCall({
950
+ call,
951
+ resolvedArgs,
952
+ reason: "Reviewed tool proposal changed before resume; retry approval",
953
+ hookRegistry,
954
+ runId,
955
+ threadId
956
+ }), effectiveCall.args);
856
957
  const decision = normalizeApprovalDecisions([toolCallId], resumeValue).get(toolCallId) ?? {
857
958
  type: "reject",
858
959
  reason: "No decision provided for tool approval"
859
960
  };
860
961
  const declaredType = decision.type;
861
- if (preResult.allowedDecisions != null && (typeof declaredType !== "string" || !preResult.allowedDecisions.includes(declaredType))) return persistOutput(this.blockDirectCall({
962
+ if (allowedDecisions != null && (typeof declaredType !== "string" || !allowedDecisions.includes(declaredType))) return persistOutput(this.blockDirectCall({
862
963
  call,
863
964
  resolvedArgs,
864
- reason: `Decision "${typeof declaredType === "string" ? declaredType : "<missing>"}" not in allowedDecisions [${preResult.allowedDecisions.join(", ")}] — failing closed`,
965
+ reason: `Decision "${typeof declaredType === "string" ? declaredType : "<missing>"}" not in allowedDecisions [${allowedDecisions.join(", ")}] — failing closed`,
865
966
  hookRegistry,
866
967
  runId,
867
968
  threadId
@@ -869,7 +970,7 @@ var ToolNode = class extends RunnableCallable {
869
970
  if (decision.type === "reject") return persistOutput(this.blockDirectCall({
870
971
  call,
871
972
  resolvedArgs,
872
- reason: decision.reason ?? preResult.reason ?? "Rejected by user",
973
+ reason: decision.reason ?? askReason ?? "Rejected by user",
873
974
  hookRegistry,
874
975
  runId,
875
976
  threadId
@@ -997,7 +1098,7 @@ var ToolNode = class extends RunnableCallable {
997
1098
  }
998
1099
  blockDirectCall(args) {
999
1100
  const { call, resolvedArgs, reason, hookRegistry, runId, threadId } = args;
1000
- if (hookRegistry.hasHookFor("PermissionDenied", runId) === true) executeHooks({
1101
+ if (hookRegistry?.hasHookFor("PermissionDenied", runId) === true) executeHooks({
1001
1102
  registry: hookRegistry,
1002
1103
  input: {
1003
1104
  hook_event_name: "PermissionDenied",
@@ -1125,7 +1226,8 @@ var ToolNode = class extends RunnableCallable {
1125
1226
  resolvedArgs = pre.resolved;
1126
1227
  if (pre.unresolved.length > 0 && call.id != null) unresolvedByCallId.set(call.id, pre.unresolved);
1127
1228
  } else if (registry != null) {
1128
- const { resolved, unresolved } = registry.resolve(registryRunId, originalArgs, { substituteIntentKey: this.toolDeclaresBusinessIntent(call.name) });
1229
+ const options = { substituteIntentKey: this.toolDeclaresBusinessIntent(call.name) };
1230
+ const { resolved, unresolved } = preBatchSnapshot != null ? preBatchSnapshot.resolve(originalArgs, options) : registry.resolve(registryRunId, originalArgs, options);
1129
1231
  resolvedArgs = resolved;
1130
1232
  if (unresolved.length > 0 && call.id != null) unresolvedByCallId.set(call.id, unresolved);
1131
1233
  }
@@ -1147,8 +1249,9 @@ var ToolNode = class extends RunnableCallable {
1147
1249
  });
1148
1250
  const hookRegistry = this.hookRegistry;
1149
1251
  const approvalReplaySessionId = getToolApprovalReplaySessionId(config, runId);
1252
+ const approvalReviewEvidence = getToolApprovalReviewEvidence(config, getToolBatchReplayOwner(config, this.executingAgentId ?? this.agentId ?? ""));
1150
1253
  const hasPendingApproval = preToolCalls.some((entry) => entry.call.id != null && hookRegistry?.getPendingToolApproval(approvalReplaySessionId, createToolApprovalReplayKey(config, this.executingAgentId ?? this.agentId ?? "", entry.call.id)) != null);
1151
- if (hookRegistry != null && (hookRegistry.hasHookFor("PreToolUse", runId) || hasPendingApproval)) {
1254
+ if (hookRegistry?.hasHookFor("PreToolUse", runId) === true || hasPendingApproval || approvalReviewEvidence != null) {
1152
1255
  const preemptedEagerRecords = /* @__PURE__ */ new Map();
1153
1256
  if (this.eagerEventToolExecutions != null) for (const entry of preToolCalls) {
1154
1257
  const callId = entry.call.id;
@@ -1159,7 +1262,7 @@ var ToolNode = class extends RunnableCallable {
1159
1262
  this.eagerEventToolExecutions.delete(callId);
1160
1263
  }
1161
1264
  }
1162
- const preResults = await Promise.all(preToolCalls.map((entry) => {
1265
+ const preResults = hookRegistry == null ? preToolCalls.map(() => HOOK_FALLBACK) : await Promise.all(preToolCalls.map((entry) => {
1163
1266
  const toolUseId = entry.call.id;
1164
1267
  const approvalReplayKey = toolUseId == null ? void 0 : createToolApprovalReplayKey(config, this.executingAgentId ?? this.agentId ?? "", toolUseId);
1165
1268
  return executeHooks({
@@ -1212,7 +1315,7 @@ var ToolNode = class extends RunnableCallable {
1212
1315
  const flushDeferredBlockedSideEffects = async () => {
1213
1316
  for (const item of deferredBlockedSideEffects) {
1214
1317
  await this.dispatchStepCompleted(item.callId, item.toolName, item.args, item.contentString, config);
1215
- if (hookRegistry.hasHookFor("PermissionDenied", runId)) executeHooks({
1318
+ if (hookRegistry?.hasHookFor("PermissionDenied", runId) === true) executeHooks({
1216
1319
  registry: hookRegistry,
1217
1320
  input: {
1218
1321
  hook_event_name: "PermissionDenied",
@@ -1256,6 +1359,13 @@ var ToolNode = class extends RunnableCallable {
1256
1359
  continue;
1257
1360
  }
1258
1361
  if (hookResult.updatedInput != null) applyInputOverride(entry, hookResult.updatedInput);
1362
+ else {
1363
+ const reviewed = getReviewedToolApproval(approvalReviewEvidence?.payload, entry.call.id);
1364
+ if (reviewed != null) {
1365
+ entry.args = this.coerceEventToolArgs(entry.call.name, reviewed.request.arguments);
1366
+ if (entry.call.id != null) unresolvedByCallId.delete(entry.call.id);
1367
+ }
1368
+ }
1259
1369
  askEntries.push({
1260
1370
  entry,
1261
1371
  reason: hookResult.reason,
@@ -1264,6 +1374,19 @@ var ToolNode = class extends RunnableCallable {
1264
1374
  continue;
1265
1375
  }
1266
1376
  if (hookResult.updatedInput != null) applyInputOverride(entry, hookResult.updatedInput);
1377
+ const reviewedApproval = getReviewedToolApproval(approvalReviewEvidence?.payload, entry.call.id);
1378
+ if (reviewedApproval != null) {
1379
+ if (hookResult.updatedInput == null) {
1380
+ entry.args = this.coerceEventToolArgs(entry.call.name, reviewedApproval.request.arguments);
1381
+ if (entry.call.id != null) unresolvedByCallId.delete(entry.call.id);
1382
+ }
1383
+ askEntries.push({
1384
+ entry,
1385
+ reason: reviewedApproval.request.description,
1386
+ allowedDecisions: reviewedApproval.reviewConfig.allowed_decisions
1387
+ });
1388
+ continue;
1389
+ }
1267
1390
  if (entry.call.id != null) {
1268
1391
  const preempted = preemptedEagerRecords.get(entry.call.id);
1269
1392
  if (preempted != null) this.eagerEventToolExecutions?.set(entry.call.id, preempted);
@@ -1273,9 +1396,12 @@ var ToolNode = class extends RunnableCallable {
1273
1396
  if (askEntries.length > 0) {
1274
1397
  const payload = buildToolApprovalInterruptPayload(askEntries, runId);
1275
1398
  const resumeValue = AsyncLocalStorageProviderSingleton.runWithConfig(config, () => interrupt(payload));
1276
- for (const { entry } of askEntries) hookRegistry.clearPendingToolApproval(approvalReplaySessionId, createToolApprovalReplayKey(config, this.executingAgentId ?? this.agentId ?? "", entry.call.id));
1277
- const decisionByCallId = normalizeApprovalDecisions(askEntries.map(({ entry }) => entry.call.id), resumeValue);
1278
- for (const { entry, reason: askReason, allowedDecisions } of askEntries) {
1399
+ for (const { entry } of askEntries) hookRegistry?.clearPendingToolApproval(approvalReplaySessionId, createToolApprovalReplayKey(config, this.executingAgentId ?? this.agentId ?? "", entry.call.id));
1400
+ const proposalMatches = approvalReviewEvidence == null || toolApprovalPayloadMatches(payload, approvalReviewEvidence.payload);
1401
+ if (!proposalMatches) for (const { entry } of askEntries) blockEntry(entry, "Reviewed tool proposal changed before resume; retry approval");
1402
+ const decisionEntries = proposalMatches ? askEntries : [];
1403
+ const decisionByCallId = normalizeApprovalDecisions(decisionEntries.map(({ entry }) => entry.call.id), resumeValue);
1404
+ for (const { entry, reason: askReason, allowedDecisions } of decisionEntries) {
1279
1405
  const decision = decisionByCallId.get(entry.call.id) ?? {
1280
1406
  type: "reject",
1281
1407
  reason: "No decision provided for tool approval"
@@ -1401,7 +1527,7 @@ var ToolNode = class extends RunnableCallable {
1401
1527
  userId: config.configurable?.user_id,
1402
1528
  agentId: this.executingAgentId,
1403
1529
  callerCapabilityProjection: this.getCallerCapabilityProjectionSnapshot(),
1404
- configurable: stripRunBreakerScope(config.configurable),
1530
+ configurable: stripHostExecutionConfig(config.configurable),
1405
1531
  metadata: config.metadata,
1406
1532
  signal: config.signal,
1407
1533
  resolve: (results) => {
@@ -1659,15 +1785,39 @@ var ToolNode = class extends RunnableCallable {
1659
1785
  return this.dispatchStepCompleted(result.toolCallId, request.name, request.args, output, config, request.turn, resolveToolOutcome(request.args, outcomeFieldsFromResult(result), { isError: result.status === "error" }));
1660
1786
  }
1661
1787
  async runDirectBatchInterruptSafe(directCalls, batchIndices, config, baseContext) {
1662
- const settledBatchResults = baseContext.replayBatchKey == null ? void 0 : this.settledInterruptingResults.get(baseContext.replayBatchKey);
1788
+ const settledBatchResults = baseContext.replayBatchKey == null ? void 0 : this.settledDirectResultsByBatch.get(baseContext.replayBatchKey) ?? /* @__PURE__ */ new Map();
1789
+ const cacheResult = (call, position, result) => {
1790
+ if (baseContext.replayBatchKey == null || settledBatchResults == null) return;
1791
+ settledBatchResults.set(directReplayKey(call, baseContext.replayCallPositions?.get(call) ?? batchIndices[position]), result);
1792
+ this.settledDirectResultsByBatch.set(baseContext.replayBatchKey, settledBatchResults);
1793
+ };
1663
1794
  const restoreResult = (call, result) => {
1664
1795
  baseContext.additionalContextsSink?.push(...result.additionalContexts);
1796
+ const turn = result.turn;
1797
+ if (turn != null) this.recordToolUsageTurn(call.name, turn, call.id);
1798
+ const refMeta = isBaseMessage(result.output) ? result.output.additional_kwargs : void 0;
1799
+ if (refMeta?._refKey != null && result.referenceContent != null) {
1800
+ this.toolOutputRegistry?.set(baseContext.batchScopeId, refMeta._refKey, result.referenceContent);
1801
+ if (isBaseMessage(result.output)) result = {
1802
+ ...result,
1803
+ output: new ToolMessage({
1804
+ ...result.output,
1805
+ additional_kwargs: {
1806
+ ...result.output.additional_kwargs,
1807
+ _refScope: baseContext.batchScopeId
1808
+ }
1809
+ })
1810
+ };
1811
+ }
1665
1812
  if (call.id != null && result.resolvedArgs != null && baseContext.resolvedArgsByCallId != null) baseContext.resolvedArgsByCallId.set(call.id, result.resolvedArgs);
1666
1813
  return result;
1667
1814
  };
1668
1815
  const runOne = async (call, position) => {
1669
- const cachedResult = typeof call.id === "string" ? settledBatchResults?.get(call.id) : void 0;
1670
- if (cachedResult != null) return restoreResult(call, cachedResult);
1816
+ const cachedResult = settledBatchResults?.get(directReplayKey(call, baseContext.replayCallPositions?.get(call) ?? batchIndices[position]));
1817
+ if (cachedResult != null) {
1818
+ if (cachedResult.proposal.name !== call.name || !recordArgsEqual(cachedResult.proposal.args, call.args)) throw new Error("Cannot change an already completed tool call during approval replay");
1819
+ return restoreResult(call, cachedResult);
1820
+ }
1671
1821
  const additionalContexts = [];
1672
1822
  const output = await this.runDirectToolWithLifecycleHooks(call, config, {
1673
1823
  ...baseContext,
@@ -1675,14 +1825,32 @@ var ToolNode = class extends RunnableCallable {
1675
1825
  additionalContextsSink: additionalContexts
1676
1826
  });
1677
1827
  const resolvedArgs = call.id == null ? void 0 : baseContext.resolvedArgsByCallId?.get(call.id);
1678
- return restoreResult(call, {
1828
+ const result = {
1829
+ proposal: structuredClone({
1830
+ name: call.name,
1831
+ args: call.args
1832
+ }),
1833
+ turn: call.id == null ? void 0 : this.toolCallTurns.get(call.id) ?? this.directPathTurns.get(call.id),
1679
1834
  output,
1680
1835
  additionalContexts,
1681
1836
  ...resolvedArgs == null ? {} : { resolvedArgs }
1682
- });
1837
+ };
1838
+ const refMeta = isBaseMessage(output) ? output.additional_kwargs : void 0;
1839
+ if (refMeta?._refKey != null) result.referenceContent = this.toolOutputRegistry?.get(refMeta._refScope, refMeta._refKey);
1840
+ cacheResult(call, position, result);
1841
+ return restoreResult(call, result);
1683
1842
  };
1684
1843
  const interrupting = this.interruptingToolNames;
1685
- 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);
1844
+ if (!(interrupting != null && directCalls.some((call) => interrupting.has(call.name)))) {
1845
+ const results = await Promise.allSettled(directCalls.map((call, i) => runOne(call, i)));
1846
+ this.throwIfBreakerTripped(config);
1847
+ const failed = results.find((result) => result.status === "rejected" && !isGraphInterrupt(result.reason)) ?? results.find((result) => result.status === "rejected");
1848
+ if (failed?.status === "rejected") throw failed.reason;
1849
+ return results.map((result) => {
1850
+ if (result.status === "rejected") throw result.reason;
1851
+ return result.value.output;
1852
+ });
1853
+ }
1686
1854
  const outputs = new Array(directCalls.length);
1687
1855
  const interruptingPositions = [];
1688
1856
  const regularPositions = [];
@@ -1703,12 +1871,6 @@ var ToolNode = class extends RunnableCallable {
1703
1871
  }
1704
1872
  const position = interruptingPositions[i];
1705
1873
  outputs[position] = result.value.output;
1706
- const callId = directCalls[position].id;
1707
- if (baseContext.replayBatchKey != null && typeof callId === "string" && callId !== "") {
1708
- const batchResults = this.settledInterruptingResults.get(baseContext.replayBatchKey) ?? /* @__PURE__ */ new Map();
1709
- batchResults.set(callId, result.value);
1710
- this.settledInterruptingResults.set(baseContext.replayBatchKey, batchResults);
1711
- }
1712
1874
  }
1713
1875
  if (hasInterruptingError) {
1714
1876
  if (isGraphInterrupt(interruptingError)) {
@@ -1750,7 +1912,7 @@ var ToolNode = class extends RunnableCallable {
1750
1912
  const outputs = [...toolMessages, ...injected];
1751
1913
  return Array.isArray(input) ? outputs : { messages: outputs };
1752
1914
  }
1753
- async run(input, config) {
1915
+ async run(input, config, referenceReplay = {}) {
1754
1916
  const breakerSignal = this.getBreakerSignal?.();
1755
1917
  const composedSignal = composeAbortSignals(config.signal, breakerSignal);
1756
1918
  const batchRunScope = this.getRunScope?.();
@@ -1766,10 +1928,11 @@ var ToolNode = class extends RunnableCallable {
1766
1928
  this.toolCallTurns.clear();
1767
1929
  const resolvedArgsByCallId = /* @__PURE__ */ new Map();
1768
1930
  const errorOwnership = createToolErrorOwnership();
1769
- const incomingRunId = config.configurable?.run_id;
1770
- const incomingThreadId = config.configurable?.thread_id;
1771
- const batchScopeId = incomingRunId ?? `\0anon-${this.anonBatchCounter++}`;
1772
- const turn = this.toolOutputRegistry?.nextTurn(batchScopeId) ?? 0;
1931
+ const batchScopeId = config.configurable?.run_id ?? `\0anon-${this.anonBatchCounter++}`;
1932
+ const resumedReferenceState = referenceReplay.state;
1933
+ const replayInputSnapshot = resumedReferenceState == null ? void 0 : this.toolOutputRegistry?.resumeBatch(batchScopeId, resumedReferenceState);
1934
+ referenceReplay.state = resumedReferenceState ?? this.toolOutputRegistry?.snapshotState(batchScopeId);
1935
+ const turn = resumedReferenceState?.turnCounter ?? this.toolOutputRegistry?.nextTurn(batchScopeId) ?? 0;
1773
1936
  let outputs;
1774
1937
  let replayBatchKey;
1775
1938
  let promotedAiMessage;
@@ -1779,17 +1942,19 @@ var ToolNode = class extends RunnableCallable {
1779
1942
  if (this.eventDrivenMode && !isLocalTool) return this.executeViaEvent([input.lg_tool_call], config, input, {
1780
1943
  batchIndices: [0],
1781
1944
  turn,
1782
- batchScopeId
1945
+ batchScopeId,
1946
+ preBatchSnapshot: replayInputSnapshot
1783
1947
  });
1784
1948
  const directAdditionalContexts = [];
1785
1949
  const { lg_tool_call: _sendToolCall, ...sendState } = input;
1786
1950
  const sendMessages = sendState.messages;
1787
1951
  const sendAssistantBatch = Array.isArray(sendMessages) ? findAssistantBatch(sendMessages) : void 0;
1788
- const sendReplayBatchKey = sendAssistantBatch == null ? void 0 : getAssistantBatchReplayKey(sendAssistantBatch, incomingRunId, incomingThreadId);
1952
+ const sendReplayBatchKey = sendAssistantBatch == null || getToolBatchReplayScope(config) == null ? void 0 : getAssistantBatchReplayKey(sendAssistantBatch, getToolBatchReplayScope(config));
1789
1953
  const sendOutput = await this.runDirectToolWithLifecycleHooks(input.lg_tool_call, config, {
1790
1954
  batchIndex: 0,
1791
1955
  turn,
1792
1956
  batchScopeId,
1957
+ preBatchSnapshot: replayInputSnapshot,
1793
1958
  resolvedArgsByCallId,
1794
1959
  errorOwnership,
1795
1960
  additionalContextsSink: directAdditionalContexts,
@@ -1813,14 +1978,15 @@ var ToolNode = class extends RunnableCallable {
1813
1978
  const assistantBatch = findAssistantBatch(messages);
1814
1979
  if (assistantBatch == null) throw new Error("ToolNode only accepts AIMessages as input.");
1815
1980
  const aiMessage = assistantBatch.message;
1816
- replayBatchKey = getAssistantBatchReplayKey(assistantBatch, incomingRunId, incomingThreadId);
1981
+ replayBatchKey = getToolBatchReplayScope(config) == null ? void 0 : getAssistantBatchReplayKey(assistantBatch, getToolBatchReplayScope(config));
1817
1982
  if (this.loadRuntimeTools) {
1818
1983
  const { tools, toolMap } = this.loadRuntimeTools(aiMessage.tool_calls ?? []);
1819
1984
  this.toolMap = toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
1820
1985
  this.applyToolExecutionOverrides();
1821
1986
  }
1987
+ const replayCallPositions = new Map((aiMessage.tool_calls ?? []).map((call, i) => [call, i]));
1822
1988
  const filteredCalls = aiMessage.tool_calls?.filter((call) => {
1823
- return (call.id == null || !toolMessageIds.has(call.id)) && !(call.id?.startsWith("srvtoolu_") ?? false);
1989
+ return (call.id == null || call.id === "" || !toolMessageIds.has(call.id)) && !(call.id?.startsWith("srvtoolu_") ?? false);
1824
1990
  }) ?? [];
1825
1991
  const attributableInvalidCalls = !(!Array.isArray(input) && typeof aiMessage.id === "string" && aiMessage.id.length > 0) ? [] : (aiMessage.invalid_tool_calls ?? []).filter((call) => call.id != null && call.id !== "" && !toolMessageIds.has(call.id) && !call.id.startsWith("srvtoolu_"));
1826
1992
  invalidCallResults = attributableInvalidCalls.map((call) => new ToolMessage({
@@ -1862,14 +2028,16 @@ var ToolNode = class extends RunnableCallable {
1862
2028
  if (directEntries.length === 0 && invalidCallResults.length === 0) return this.executeViaEvent(filteredCalls, config, input, {
1863
2029
  batchIndices: eventEntries.map((entry) => entry.batchIndex),
1864
2030
  turn,
1865
- batchScopeId
2031
+ batchScopeId,
2032
+ preBatchSnapshot: replayInputSnapshot
1866
2033
  });
1867
2034
  const directCalls = directEntries.map((e) => e.call);
1868
2035
  const directIndices = directEntries.map((e) => e.batchIndex);
1869
2036
  const eventCalls = eventEntries.map((e) => e.call);
1870
2037
  const eventIndices = eventEntries.map((e) => e.batchIndex);
2038
+ const approvalScopeCallIds = new Set(filteredCalls.flatMap((call) => call.id == null ? [] : [call.id]));
1871
2039
  const preResolvedEventArgs = /* @__PURE__ */ new Map();
1872
- const preBatchSnapshot = this.toolOutputRegistry?.snapshot(batchScopeId);
2040
+ const preBatchSnapshot = replayInputSnapshot ?? this.toolOutputRegistry?.snapshot(batchScopeId);
1873
2041
  if (preBatchSnapshot != null) {
1874
2042
  for (const entry of eventEntries) if (entry.call.id != null) {
1875
2043
  const { resolved, unresolved } = preBatchSnapshot.resolve(entry.call.args, { substituteIntentKey: this.toolDeclaresBusinessIntent(entry.call.name) });
@@ -1888,9 +2056,25 @@ var ToolNode = class extends RunnableCallable {
1888
2056
  preBatchSnapshot,
1889
2057
  additionalContextsSink: directAdditionalContexts,
1890
2058
  replayBatchKey,
2059
+ replayCallPositions,
2060
+ approvalScopeCallIds,
1891
2061
  runInput: input
1892
2062
  }) : [];
1893
- if (directCalls.length > 0 && directOutputs.length > 0) await this.handleRunToolCompletions(directCalls, directOutputs, config, resolvedArgsByCallId, errorOwnership);
2063
+ const settledDirectResults = replayBatchKey == null ? void 0 : this.settledDirectResultsByBatch.get(replayBatchKey);
2064
+ const unhandledDirectCalls = [];
2065
+ const unhandledDirectOutputs = [];
2066
+ for (let i = 0; i < directCalls.length; i++) {
2067
+ if ((settledDirectResults?.get(directReplayKey(directCalls[i], replayCallPositions.get(directCalls[i]))))?.completionHandled === true) continue;
2068
+ unhandledDirectCalls.push(directCalls[i]);
2069
+ unhandledDirectOutputs.push(directOutputs[i]);
2070
+ }
2071
+ if (unhandledDirectCalls.length > 0) {
2072
+ await this.handleRunToolCompletions(unhandledDirectCalls, unhandledDirectOutputs, config, resolvedArgsByCallId, errorOwnership);
2073
+ for (let i = 0; i < directCalls.length; i++) {
2074
+ const settled = settledDirectResults?.get(directReplayKey(directCalls[i], replayCallPositions.get(directCalls[i])));
2075
+ if (settled != null) settled.completionHandled = true;
2076
+ }
2077
+ }
1894
2078
  const eventResult = eventCalls.length > 0 ? await this.dispatchToolEvents(eventCalls, config, {
1895
2079
  batchIndices: eventIndices,
1896
2080
  turn,
@@ -1917,8 +2101,9 @@ var ToolNode = class extends RunnableCallable {
1917
2101
  ...eventResult.injected
1918
2102
  ];
1919
2103
  } else {
1920
- const preBatchSnapshot = this.toolOutputRegistry?.snapshot(batchScopeId);
2104
+ const preBatchSnapshot = replayInputSnapshot ?? this.toolOutputRegistry?.snapshot(batchScopeId);
1921
2105
  const directAdditionalContexts = [];
2106
+ const approvalScopeCallIds = new Set(filteredCalls.flatMap((call) => call.id == null ? [] : [call.id]));
1922
2107
  const toolOutputs = await this.runDirectBatchInterruptSafe(filteredCalls, filteredCalls.map((_call, i) => i), config, {
1923
2108
  turn,
1924
2109
  batchScopeId,
@@ -1927,6 +2112,8 @@ var ToolNode = class extends RunnableCallable {
1927
2112
  preBatchSnapshot,
1928
2113
  additionalContextsSink: directAdditionalContexts,
1929
2114
  replayBatchKey,
2115
+ replayCallPositions,
2116
+ approvalScopeCallIds,
1930
2117
  runInput: input
1931
2118
  });
1932
2119
  await this.handleRunToolCompletions(filteredCalls, toolOutputs, config, resolvedArgsByCallId, errorOwnership);
@@ -1955,7 +2142,7 @@ var ToolNode = class extends RunnableCallable {
1955
2142
  }
1956
2143
  }
1957
2144
  if (!outputs.some(isCommand)) {
1958
- if (replayBatchKey != null) this.settledInterruptingResults.delete(replayBatchKey);
2145
+ if (replayBatchKey != null) this.settledDirectResultsByBatch.delete(replayBatchKey);
1959
2146
  return Array.isArray(input) ? outputs : { messages: outputs };
1960
2147
  }
1961
2148
  if (promotedAiMessage != null) outputs = outputs.map((output) => isCommand(output) ? patchCommandUpdateForPromotedInvalidCalls(output, promotedAiMessage, invalidCallResults) : output);
@@ -2005,7 +2192,7 @@ var ToolNode = class extends RunnableCallable {
2005
2192
  combinedOutputs.push(parallelCommand);
2006
2193
  } else if (handoffCommands.length === 1) combinedOutputs.push(handoffCommands[0]);
2007
2194
  if (parentCommand) combinedOutputs.push(parentCommand);
2008
- if (replayBatchKey != null) this.settledInterruptingResults.delete(replayBatchKey);
2195
+ if (replayBatchKey != null) this.settledDirectResultsByBatch.delete(replayBatchKey);
2009
2196
  return combinedOutputs;
2010
2197
  }
2011
2198
  isSendInput(input) {