@librechat/agents 3.8.5 → 3.8.7

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 (111) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +1 -1
  2. package/dist/cjs/common/constants.cjs +2 -0
  3. package/dist/cjs/common/constants.cjs.map +1 -1
  4. package/dist/cjs/graphs/Graph.cjs +18 -9
  5. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  6. package/dist/cjs/hitl/approvalReview.cjs +1 -1
  7. package/dist/cjs/hooks/types.cjs.map +1 -1
  8. package/dist/cjs/main.cjs +8 -6
  9. package/dist/cjs/messages/index.cjs +1 -1
  10. package/dist/cjs/messages/prune.cjs +1 -1
  11. package/dist/cjs/run.cjs +7 -3
  12. package/dist/cjs/run.cjs.map +1 -1
  13. package/dist/cjs/stream.cjs +13 -3
  14. package/dist/cjs/stream.cjs.map +1 -1
  15. package/dist/cjs/summarization/node.cjs +3 -1
  16. package/dist/cjs/summarization/node.cjs.map +1 -1
  17. package/dist/cjs/tools/BashExecutor.cjs +79 -27
  18. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  19. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +47 -7
  20. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  21. package/dist/cjs/tools/CodeExecutor.cjs +5 -2
  22. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  23. package/dist/cjs/tools/CodeSessionFileSummary.cjs +11 -1
  24. package/dist/cjs/tools/CodeSessionFileSummary.cjs.map +1 -1
  25. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +54 -8
  26. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  27. package/dist/cjs/tools/ToolNode.cjs +176 -43
  28. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  29. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +1 -0
  30. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -1
  31. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs +0 -1
  32. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs.map +1 -1
  33. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +120 -26
  34. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  35. package/dist/cjs/tools/toolBatchReplay.cjs +4 -2
  36. package/dist/cjs/tools/toolBatchReplay.cjs.map +1 -1
  37. package/dist/cjs/utils/index.cjs +1 -1
  38. package/dist/esm/agents/AgentContext.mjs +1 -1
  39. package/dist/esm/common/constants.mjs +2 -1
  40. package/dist/esm/common/constants.mjs.map +1 -1
  41. package/dist/esm/graphs/Graph.mjs +17 -8
  42. package/dist/esm/graphs/Graph.mjs.map +1 -1
  43. package/dist/esm/hitl/approvalReview.mjs +1 -1
  44. package/dist/esm/hooks/types.mjs.map +1 -1
  45. package/dist/esm/main.mjs +7 -7
  46. package/dist/esm/messages/index.mjs +1 -1
  47. package/dist/esm/messages/prune.mjs +1 -1
  48. package/dist/esm/run.mjs +7 -3
  49. package/dist/esm/run.mjs.map +1 -1
  50. package/dist/esm/stream.mjs +13 -3
  51. package/dist/esm/stream.mjs.map +1 -1
  52. package/dist/esm/summarization/node.mjs +3 -1
  53. package/dist/esm/summarization/node.mjs.map +1 -1
  54. package/dist/esm/tools/BashExecutor.mjs +81 -29
  55. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  56. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +48 -9
  57. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  58. package/dist/esm/tools/CodeExecutor.mjs +5 -2
  59. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  60. package/dist/esm/tools/CodeSessionFileSummary.mjs +11 -2
  61. package/dist/esm/tools/CodeSessionFileSummary.mjs.map +1 -1
  62. package/dist/esm/tools/ProgrammaticToolCalling.mjs +55 -9
  63. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  64. package/dist/esm/tools/ToolNode.mjs +176 -43
  65. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  66. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +1 -0
  67. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -1
  68. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs +0 -1
  69. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs.map +1 -1
  70. package/dist/esm/tools/subagent/SubagentExecutor.mjs +120 -26
  71. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  72. package/dist/esm/tools/toolBatchReplay.mjs +4 -2
  73. package/dist/esm/tools/toolBatchReplay.mjs.map +1 -1
  74. package/dist/esm/utils/index.mjs +1 -1
  75. package/dist/types/common/constants.d.ts +2 -0
  76. package/dist/types/graphs/Graph.d.ts +3 -2
  77. package/dist/types/hooks/types.d.ts +3 -0
  78. package/dist/types/run.d.ts +1 -0
  79. package/dist/types/summarization/node.d.ts +1 -0
  80. package/dist/types/tools/BashExecutor.d.ts +2 -0
  81. package/dist/types/tools/BashProgrammaticToolCalling.d.ts +1 -0
  82. package/dist/types/tools/CodeExecutor.d.ts +1 -1
  83. package/dist/types/tools/CodeSessionFileSummary.d.ts +3 -0
  84. package/dist/types/tools/ProgrammaticToolCalling.d.ts +2 -2
  85. package/dist/types/tools/ToolNode.d.ts +6 -2
  86. package/dist/types/tools/subagent/SubagentExecutor.d.ts +7 -4
  87. package/dist/types/tools/toolBatchReplay.d.ts +2 -0
  88. package/dist/types/types/graph.d.ts +28 -0
  89. package/dist/types/types/run.d.ts +2 -0
  90. package/dist/types/types/tools.d.ts +27 -3
  91. package/package.json +2 -2
  92. package/src/common/constants.ts +3 -0
  93. package/src/graphs/Graph.ts +32 -18
  94. package/src/hooks/types.ts +3 -0
  95. package/src/run.ts +30 -15
  96. package/src/stream.ts +27 -5
  97. package/src/summarization/node.ts +3 -0
  98. package/src/tools/BashExecutor.ts +143 -40
  99. package/src/tools/BashProgrammaticToolCalling.ts +82 -10
  100. package/src/tools/CodeExecutor.ts +12 -2
  101. package/src/tools/CodeSessionFileSummary.ts +25 -1
  102. package/src/tools/ProgrammaticToolCalling.ts +77 -6
  103. package/src/tools/ToolNode.ts +665 -170
  104. package/src/tools/local/LocalProgrammaticToolCalling.ts +1 -0
  105. package/src/tools/subagent/SubagentExecutionRegistry.ts +0 -1
  106. package/src/tools/subagent/SubagentExecutor.ts +214 -55
  107. package/src/tools/toolBatchReplay.ts +23 -1
  108. package/src/types/graph.ts +37 -0
  109. package/src/types/run.ts +2 -0
  110. package/src/types/tools.ts +30 -3
  111. package/src/utils/llmConfig.ts +0 -1
@@ -49,10 +49,21 @@ import type {
49
49
  PostToolBatchEntry,
50
50
  ToolApprovalReplayKey,
51
51
  } from '@/hooks';
52
- import type { PreparedSubagents } from '@/tools/preparedSubagents';
53
52
  import type { SettledToolBatchResult } from '@/tools/toolBatchReplay';
53
+ import type { PreparedSubagents } from '@/tools/preparedSubagents';
54
54
  import type { RunBreakerScope } from '@/llm/streamLimits';
55
55
  import type * as t from '@/types';
56
+ import {
57
+ TOOL_BATCH_REPLAY_KEY,
58
+ getToolBatchReplayOwner,
59
+ getToolBatchReplayScope,
60
+ attachToolBatchReplayState,
61
+ restoreToolBatchReplayState,
62
+ getToolBatchReplayState,
63
+ clearStaleToolApprovalConfig,
64
+ isChildToolReplayOwner,
65
+ getToolReplayResumeStatus,
66
+ } from '@/tools/toolBatchReplay';
56
67
  import {
57
68
  type CallerCapabilityProjection,
58
69
  createCallerCapabilityProjectionSnapshot,
@@ -121,25 +132,14 @@ import { stripCodeSessionFileSummary } from '@/tools/CodeSessionFileSummary';
121
132
  import { Constants, GraphEvents, CODE_EXECUTION_TOOLS } from '@/common';
122
133
  import { PreparedSubagentError } from '@/tools/preparedSubagents';
123
134
  import { attachRunStepResumeState } from '@/tools/runStepResume';
124
- import {
125
- TOOL_BATCH_REPLAY_KEY,
126
- getToolBatchReplayOwner,
127
- getToolBatchReplayScope,
128
- attachToolBatchReplayState,
129
- restoreToolBatchReplayState,
130
- getToolBatchReplayState,
131
- clearStaleToolApprovalConfig,
132
- isChildToolReplayOwner,
133
- getToolReplayResumeStatus,
134
- } from '@/tools/toolBatchReplay';
135
135
 
136
136
  function stripToolApprovalReviewConfig(
137
137
  configurable: Record<string, unknown> | undefined
138
138
  ): Record<string, unknown> | undefined {
139
139
  if (
140
140
  configurable == null ||
141
- !(TOOL_APPROVAL_REVIEW_CONFIG_KEY in configurable) &&
142
- !(TOOL_BATCH_REPLAY_KEY in configurable)
141
+ (!(TOOL_APPROVAL_REVIEW_CONFIG_KEY in configurable) &&
142
+ !(TOOL_BATCH_REPLAY_KEY in configurable))
143
143
  ) {
144
144
  return configurable;
145
145
  }
@@ -221,6 +221,8 @@ type RunToolBatchContext<T = unknown> = {
221
221
  batchScopeId?: string;
222
222
  /** Batch-local sink for post-substitution args. */
223
223
  resolvedArgsByCallId?: ResolvedArgsByCallId;
224
+ /** File identities actually injected into each direct code call. */
225
+ codeSessionBaselineByCallId?: Map<string, ReadonlyMap<string, string>>;
224
226
  /**
225
227
  * Frozen pre-batch view of the tool-output registry. When supplied,
226
228
  * `runTool` resolves `{{tool…turn…}}` placeholders against this
@@ -474,8 +476,12 @@ function getAssistantBatchReplayKey(
474
476
  ): string {
475
477
  return JSON.stringify([
476
478
  threadId ?? null,
477
- batch.message.id == null || batch.message.id === '' ? null : batch.message.id,
478
- createHash('sha256').update(stableStringify(batch.message.tool_calls ?? [])).digest('hex'),
479
+ batch.message.id == null || batch.message.id === ''
480
+ ? null
481
+ : batch.message.id,
482
+ createHash('sha256')
483
+ .update(stableStringify(batch.message.tool_calls ?? []))
484
+ .digest('hex'),
479
485
  ]);
480
486
  }
481
487
 
@@ -647,29 +653,78 @@ function toInjectedFileRef(
647
653
 
648
654
  /* Stable file identity = `(storage_session_id, id)`. Same name in
649
655
  * different storage sessions are distinct files. */
650
- function fileIdentityKey(file: {
651
- storage_session_id?: string;
652
- id: string;
653
- }): string {
654
- return `${file.storage_session_id ?? ''}\0${file.id}`;
656
+ function fileIdentityKey(
657
+ file: {
658
+ storage_session_id?: string;
659
+ id: string;
660
+ },
661
+ fallbackSessionId = ''
662
+ ): string {
663
+ return `${file.storage_session_id ?? fallbackSessionId}\0${file.id}`;
664
+ }
665
+
666
+ function codeSessionIdentityByName(
667
+ context:
668
+ | t.ToolCallRequest['codeSessionContext']
669
+ | t.CodeSessionContext
670
+ | undefined
671
+ ): ReadonlyMap<string, string> {
672
+ return new Map(
673
+ (context?.files ?? []).map((file) => [
674
+ file.name,
675
+ fileIdentityKey(file, context?.session_id),
676
+ ])
677
+ );
678
+ }
679
+
680
+ function storageSessionIdFromIdentity(
681
+ identity: string | undefined,
682
+ fileId: string
683
+ ): string | undefined {
684
+ const suffix = `\0${fileId}`;
685
+ return identity != null && identity.endsWith(suffix)
686
+ ? identity.slice(0, identity.length - suffix.length)
687
+ : undefined;
655
688
  }
656
689
 
657
690
  function updateCodeSession(
658
691
  sessions: t.ToolSessionMap,
659
692
  sessionKey: string,
660
693
  execSessionId: string,
661
- files: t.FileRefs | undefined
694
+ files: t.FileRefs | undefined,
695
+ deletedFiles: readonly string[] | undefined,
696
+ baselineIdentityByName: ReadonlyMap<string, string> | undefined
662
697
  ): void {
663
698
  const newFiles = files ?? [];
664
699
  const existingSession = sessions.get(sessionKey) as
665
700
  | t.CodeSessionContext
666
701
  | undefined;
667
- const existingFiles = existingSession?.files ?? [];
702
+ const existingSessionId = existingSession?.session_id ?? '';
703
+ const existingFiles = (existingSession?.files ?? []).map((file) =>
704
+ file.storage_session_id == null
705
+ ? { ...file, storage_session_id: existingSessionId }
706
+ : file
707
+ );
708
+ const deletedIdentityByName = new Map<string, string>();
709
+ for (const name of deletedFiles ?? []) {
710
+ const identity = baselineIdentityByName?.get(name);
711
+ if (identity != null) {
712
+ deletedIdentityByName.set(name, identity);
713
+ }
714
+ }
668
715
 
716
+ /* Absence from `files` is not a deletion signal: older Code API versions
717
+ * and truncated responses can omit otherwise-live refs. Remove only paths
718
+ * explicitly attested by `deleted_files`, and only while the current ref
719
+ * still has the identity injected into that request. */
669
720
  if (newFiles.length === 0) {
670
721
  sessions.set(sessionKey, {
671
722
  session_id: execSessionId,
672
- files: existingFiles,
723
+ files: existingFiles.filter(
724
+ (file) =>
725
+ deletedIdentityByName.get(file.name) !==
726
+ fileIdentityKey(file, existingSessionId)
727
+ ),
673
728
  lastUpdated: Date.now(),
674
729
  });
675
730
  return;
@@ -681,26 +736,46 @@ function updateCodeSession(
681
736
  const filesWithSession: t.FileRefs = [];
682
737
  const newFileNames = new Set<string>();
683
738
  const incomingByIdentity = new Map<string, number>();
739
+ const existingIdentities = new Set(
740
+ existingFiles.map((file) => fileIdentityKey(file, existingSessionId))
741
+ );
684
742
  for (const file of newFiles) {
743
+ const baselineIdentity = baselineIdentityByName?.get(file.name);
685
744
  const withSession = {
686
745
  ...file,
687
- storage_session_id: file.storage_session_id ?? execSessionId,
746
+ storage_session_id:
747
+ file.storage_session_id ??
748
+ storageSessionIdFromIdentity(baselineIdentity, file.id) ??
749
+ execSessionId,
688
750
  };
689
- incomingByIdentity.set(
690
- fileIdentityKey(withSession),
691
- filesWithSession.length
692
- );
751
+ const identity = fileIdentityKey(withSession);
752
+ const wasTrackedByRequest = baselineIdentity === identity;
753
+ /* An inherited echo is evidence that a request observed an existing ref,
754
+ * not authority to resurrect one removed by a concurrent sibling. Fresh
755
+ * and modified outputs remain eligible to replace by path below. */
756
+ if (
757
+ file.inherited === true &&
758
+ wasTrackedByRequest &&
759
+ (!existingIdentities.has(identity) ||
760
+ deletedIdentityByName.get(file.name) === identity)
761
+ ) {
762
+ continue;
763
+ }
764
+ incomingByIdentity.set(identity, filesWithSession.length);
693
765
  newFileNames.add(withSession.name);
694
766
  filesWithSession.push(withSession);
695
767
  }
696
768
 
697
769
  const filteredExisting: t.FileRefs = [];
698
770
  for (const e of existingFiles) {
699
- const idx = incomingByIdentity.get(fileIdentityKey(e));
771
+ const idx = incomingByIdentity.get(fileIdentityKey(e, existingSessionId));
700
772
  if (idx !== undefined) {
701
773
  filesWithSession[idx] = { ...e, ...filesWithSession[idx] };
702
774
  }
703
- if (!newFileNames.has(e.name)) {
775
+ const explicitlyDeleted =
776
+ deletedIdentityByName.get(e.name) ===
777
+ fileIdentityKey(e, existingSessionId);
778
+ if (!newFileNames.has(e.name) && !explicitlyDeleted) {
704
779
  filteredExisting.push(e);
705
780
  }
706
781
  }
@@ -712,6 +787,53 @@ function updateCodeSession(
712
787
  });
713
788
  }
714
789
 
790
+ /** Retains host-provisioned inputs even when execution returns no usable artifact. */
791
+ function retainCodeSessionInputs(
792
+ sessions: t.ToolSessionMap,
793
+ sessionKey: string,
794
+ context: t.ToolCallRequest['codeSessionContext']
795
+ ): void {
796
+ if (
797
+ context?.session_id == null ||
798
+ context.session_id === '' ||
799
+ context.files == null
800
+ )
801
+ return;
802
+ const existing = sessions.get(sessionKey) as t.CodeSessionContext | undefined;
803
+ const existingFiles = existing?.files ?? [];
804
+ const files = [...existingFiles];
805
+ const indexByIdentity = new Map<string, number>();
806
+ const indexByName = new Map<string, number>();
807
+ for (let i = 0; i < files.length; i++) {
808
+ indexByIdentity.set(fileIdentityKey(files[i]), i);
809
+ indexByName.set(files[i].name, i);
810
+ }
811
+ let changed = existing?.session_id !== context.session_id;
812
+ for (const file of context.files) {
813
+ if (!file.id || !file.name || !file.storage_session_id) continue;
814
+ const identity = fileIdentityKey(file);
815
+ if (indexByIdentity.has(identity)) continue;
816
+ const replacementIndex = indexByName.get(file.name);
817
+ if (replacementIndex !== undefined) {
818
+ indexByIdentity.delete(fileIdentityKey(files[replacementIndex]));
819
+ files[replacementIndex] = { ...file };
820
+ indexByIdentity.set(identity, replacementIndex);
821
+ changed = true;
822
+ continue;
823
+ }
824
+ indexByIdentity.set(identity, files.length);
825
+ indexByName.set(file.name, files.length);
826
+ files.push({ ...file });
827
+ changed = true;
828
+ }
829
+ if (!changed) return;
830
+ sessions.set(sessionKey, {
831
+ session_id: context.session_id,
832
+ files,
833
+ lastUpdated: Date.now(),
834
+ });
835
+ }
836
+
715
837
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
716
838
  export class ToolNode<T = any> extends RunnableCallable<T, T> {
717
839
  private toolMap: Map<string, StructuredToolInterface | RunnableToolLike>;
@@ -787,6 +909,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
787
909
  * even where `agentId` (the subagent-scope marker) is undefined.
788
910
  */
789
911
  private executingAgentId?: string;
912
+ private readonly executionContext?: t.ToolNodeOptions['executionContext'];
790
913
  private executingAgentName?: string;
791
914
  private rootAgentId?: string;
792
915
  private rootAgentName?: string;
@@ -876,6 +999,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
876
999
  eagerEventToolSuppressions,
877
1000
  agentId,
878
1001
  executingAgentId,
1002
+ executionContext,
879
1003
  executingAgentName,
880
1004
  rootAgentId,
881
1005
  rootAgentName,
@@ -900,65 +1024,124 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
900
1024
  name: name ?? TOOL_NODE_RUN_NAME,
901
1025
  tags,
902
1026
  func: async (input, config) => {
903
- const messages = Array.isArray(input) ? input as BaseMessage[] :
904
- (input as { messages?: BaseMessage[] }).messages;
905
- const assistantBatch = messages == null ? undefined : findAssistantBatch(messages);
906
- const activeReplayKey = assistantBatch == null || getToolBatchReplayScope(config) == null ? undefined : getAssistantBatchReplayKey(
907
- assistantBatch, getToolBatchReplayScope(config)
908
- );
1027
+ const messages = Array.isArray(input)
1028
+ ? (input as BaseMessage[])
1029
+ : (input as { messages?: BaseMessage[] }).messages;
1030
+ const assistantBatch =
1031
+ messages == null ? undefined : findAssistantBatch(messages);
1032
+ const activeReplayKey =
1033
+ assistantBatch == null || getToolBatchReplayScope(config) == null
1034
+ ? undefined
1035
+ : getAssistantBatchReplayKey(
1036
+ assistantBatch,
1037
+ getToolBatchReplayScope(config)
1038
+ );
909
1039
  const replayOwner = getToolBatchReplayOwner(
910
1040
  config,
911
1041
  this.executingAgentId ?? this.agentId ?? ''
912
1042
  );
913
1043
  let reviewEvidence = getToolApprovalReviewEvidence(config);
914
1044
  const replayState = getToolBatchReplayState(config.configurable);
915
- const ownsParentBatch = replayState?.records.some(
916
- (record) => record.owner === replayOwner && record.batch === activeReplayKey
917
- ) === true;
1045
+ const ownsParentBatch =
1046
+ replayState?.records.some(
1047
+ (record) =>
1048
+ record.owner === replayOwner && record.batch === activeReplayKey
1049
+ ) === true;
918
1050
  let isChildReplay = false;
919
1051
  if (ownsParentBatch) {
920
- const calls = this.isSendInput(input) ? [input.lg_tool_call] : assistantBatch?.message.tool_calls ?? [];
921
- const trustedParentCallIds = new Set(calls.filter((call) =>
922
- call.name === Constants.SUBAGENT &&
923
- (this.toolMap.get(call.name) as ReplayableSubagentTool | undefined)?.[SUBAGENT_REPLAY_CONTROLLER] != null
924
- ).map((call) => call.id ?? ''));
925
- isChildReplay = reviewEvidence?.owner != null
926
- ? isChildToolReplayOwner(config, reviewEvidence.owner, trustedParentCallIds)
927
- : replayState.records.some((record) => isChildToolReplayOwner(config, record.owner, trustedParentCallIds));
1052
+ const calls = this.isSendInput(input)
1053
+ ? [input.lg_tool_call]
1054
+ : (assistantBatch?.message.tool_calls ?? []);
1055
+ const trustedParentCallIds = new Set(
1056
+ calls
1057
+ .filter(
1058
+ (call) =>
1059
+ call.name === Constants.SUBAGENT &&
1060
+ (
1061
+ this.toolMap.get(call.name) as
1062
+ | ReplayableSubagentTool
1063
+ | undefined
1064
+ )?.[SUBAGENT_REPLAY_CONTROLLER] != null
1065
+ )
1066
+ .map((call) => call.id ?? '')
1067
+ );
1068
+ isChildReplay =
1069
+ reviewEvidence?.owner != null
1070
+ ? isChildToolReplayOwner(
1071
+ config,
1072
+ reviewEvidence.owner,
1073
+ trustedParentCallIds
1074
+ )
1075
+ : replayState.records.some((record) =>
1076
+ isChildToolReplayOwner(
1077
+ config,
1078
+ record.owner,
1079
+ trustedParentCallIds
1080
+ )
1081
+ );
928
1082
  }
929
1083
  const hasApprovalAuthorization =
930
1084
  reviewEvidence?.owner != null || replayState?.approvalOwner != null;
931
- const replayResumeStatus = hasApprovalAuthorization || replayState != null
932
- ? getToolReplayResumeStatus(
933
- config,
934
- reviewEvidence?.interruptId ?? replayState?.interruptId,
935
- isChildReplay
936
- )
937
- : undefined;
1085
+ const replayResumeStatus =
1086
+ hasApprovalAuthorization || replayState != null
1087
+ ? getToolReplayResumeStatus(
1088
+ config,
1089
+ reviewEvidence?.interruptId ?? replayState?.interruptId,
1090
+ isChildReplay
1091
+ )
1092
+ : undefined;
938
1093
  if (replayResumeStatus === 'unverifiable' && hasApprovalAuthorization) {
939
- throw new Error('Cannot verify the active tool approval resume — failing closed');
1094
+ throw new Error(
1095
+ 'Cannot verify the active tool approval resume — failing closed'
1096
+ );
940
1097
  }
941
1098
  if (replayResumeStatus === 'stale') {
942
1099
  const configurable = { ...config.configurable };
943
- clearStaleToolApprovalConfig(configurable, replayOwner, activeReplayKey);
1100
+ clearStaleToolApprovalConfig(
1101
+ configurable,
1102
+ replayOwner,
1103
+ activeReplayKey
1104
+ );
944
1105
  config = {
945
1106
  ...config,
946
1107
  configurable,
947
1108
  };
948
1109
  reviewEvidence = undefined;
949
1110
  }
950
- if (reviewEvidence?.owner != null && reviewEvidence.owner !== replayOwner && !isChildReplay) {
951
- throw new Error('Tool approval execution owner changed before resume — failing closed');
1111
+ if (
1112
+ reviewEvidence?.owner != null &&
1113
+ reviewEvidence.owner !== replayOwner &&
1114
+ !isChildReplay
1115
+ ) {
1116
+ throw new Error(
1117
+ 'Tool approval execution owner changed before resume — failing closed'
1118
+ );
952
1119
  }
953
1120
  const referenceReplay: { state?: ToolOutputReferenceState } = {};
954
- const restoredBatches = await restoreToolBatchReplayState(config, replayOwner);
955
- if (restoredBatches.length > 0 && !restoredBatches.some(({ batch }) => batch === activeReplayKey)) {
956
- throw new Error('Tool batch proposal changed before approval replay — failing closed');
1121
+ const restoredBatches = await restoreToolBatchReplayState(
1122
+ config,
1123
+ replayOwner
1124
+ );
1125
+ if (
1126
+ restoredBatches.length > 0 &&
1127
+ !restoredBatches.some(({ batch }) => batch === activeReplayKey)
1128
+ ) {
1129
+ throw new Error(
1130
+ 'Tool batch proposal changed before approval replay — failing closed'
1131
+ );
957
1132
  }
958
- if (activeReplayKey != null && getToolBatchReplayState(config.configurable) != null) {
1133
+ if (
1134
+ activeReplayKey != null &&
1135
+ getToolBatchReplayState(config.configurable) != null
1136
+ ) {
959
1137
  this.settledDirectResultsByBatch.delete(activeReplayKey);
960
1138
  }
961
- for (const { batch, results, turnState, referenceState } of restoredBatches) {
1139
+ for (const {
1140
+ batch,
1141
+ results,
1142
+ turnState,
1143
+ referenceState,
1144
+ } of restoredBatches) {
962
1145
  if (batch !== activeReplayKey) {
963
1146
  continue;
964
1147
  }
@@ -978,27 +1161,45 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
978
1161
  throw error;
979
1162
  }
980
1163
  const resumeState = createRunStepResumeState?.();
981
- const activeResults = activeReplayKey == null ? undefined : this.settledDirectResultsByBatch.get(activeReplayKey);
982
- const activeBatches = activeReplayKey == null
983
- ? new Map<string, Map<string, SettledDirectToolResult>>()
984
- : new Map([[activeReplayKey, activeResults ?? new Map<string, SettledDirectToolResult>()]]);
1164
+ const activeResults =
1165
+ activeReplayKey == null
1166
+ ? undefined
1167
+ : this.settledDirectResultsByBatch.get(activeReplayKey);
1168
+ const activeBatches =
1169
+ activeReplayKey == null
1170
+ ? new Map<string, Map<string, SettledDirectToolResult>>()
1171
+ : new Map([
1172
+ [
1173
+ activeReplayKey,
1174
+ activeResults ?? new Map<string, SettledDirectToolResult>(),
1175
+ ],
1176
+ ]);
985
1177
  const turnState = this.createSubagentResumeState();
986
- const activeCallIds = new Set(assistantBatch?.message.tool_calls?.map((call) => call.id));
987
- turnState.directPathTurns = turnState.directPathTurns.filter(({ toolCallId }) => activeCallIds.has(toolCallId));
1178
+ const activeCallIds = new Set(
1179
+ assistantBatch?.message.tool_calls?.map((call) => call.id)
1180
+ );
1181
+ turnState.directPathTurns = turnState.directPathTurns.filter(
1182
+ ({ toolCallId }) => activeCallIds.has(toolCallId)
1183
+ );
988
1184
  throw new GraphInterrupt(
989
- await Promise.all(error.interrupts.map(async (pendingInterrupt) => {
990
- const value = await attachToolBatchReplayState(
991
- pendingInterrupt.value,
992
- replayOwner,
993
- activeBatches,
994
- turnState,
995
- referenceReplay.state
996
- );
997
- return {
998
- ...pendingInterrupt,
999
- value: resumeState == null ? value : attachRunStepResumeState(value, resumeState),
1000
- };
1001
- }))
1185
+ await Promise.all(
1186
+ error.interrupts.map(async (pendingInterrupt) => {
1187
+ const value = await attachToolBatchReplayState(
1188
+ pendingInterrupt.value,
1189
+ replayOwner,
1190
+ activeBatches,
1191
+ turnState,
1192
+ referenceReplay.state
1193
+ );
1194
+ return {
1195
+ ...pendingInterrupt,
1196
+ value:
1197
+ resumeState == null
1198
+ ? value
1199
+ : attachRunStepResumeState(value, resumeState),
1200
+ };
1201
+ })
1202
+ )
1002
1203
  );
1003
1204
  }
1004
1205
  if (createRunStepResumeState == null) {
@@ -1040,6 +1241,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1040
1241
  // Default to agentId so callers constructing ToolNode directly (who pass the
1041
1242
  // existing agentId option) still get attribution without knowing the new option.
1042
1243
  this.executingAgentId = executingAgentId ?? agentId;
1244
+ this.executionContext = executionContext;
1043
1245
  this.executingAgentName = executingAgentName;
1044
1246
  this.rootAgentId = rootAgentId;
1045
1247
  this.rootAgentName = rootAgentName;
@@ -1109,8 +1311,13 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1109
1311
  ? options
1110
1312
  : {
1111
1313
  ...options,
1314
+ configurable: {
1315
+ ...options?.configurable,
1316
+ executionContext: this.executionContext,
1317
+ },
1112
1318
  metadata: {
1113
1319
  ...options?.metadata,
1320
+ executionContext: this.executionContext,
1114
1321
  agentId: this.executingAgentId,
1115
1322
  activeAgentId: this.executingAgentId,
1116
1323
  ...(this.executingAgentName == null
@@ -1668,7 +1875,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1668
1875
  return next;
1669
1876
  })();
1670
1877
  this.recordToolUsageTurn(call.name, usageCount, call.id);
1671
- if (batchContext.replayBatchKey != null && call.id != null && call.id !== '') {
1878
+ if (
1879
+ batchContext.replayBatchKey != null &&
1880
+ call.id != null &&
1881
+ call.id !== ''
1882
+ ) {
1672
1883
  this.directPathTurns.set(call.id, usageCount);
1673
1884
  }
1674
1885
  let args = call.args;
@@ -1732,6 +1943,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1732
1943
  runId: (config.configurable?.run_id as string | undefined) ?? '',
1733
1944
  threadId: config.configurable?.thread_id as string | undefined,
1734
1945
  agentId: this.agentId,
1946
+ executionContext: this.executionContext,
1735
1947
  executingAgentId: this.executingAgentId,
1736
1948
  },
1737
1949
  };
@@ -1757,6 +1969,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1757
1969
  | t.CodeSessionContext
1758
1970
  | undefined;
1759
1971
  const execSessionId = codeSession?.session_id;
1972
+ if (batchContext.codeSessionBaselineByCallId != null) {
1973
+ batchContext.codeSessionBaselineByCallId.set(
1974
+ call.id ?? '',
1975
+ codeSessionIdentityByName(codeSession)
1976
+ );
1977
+ }
1760
1978
  if (execSessionId != null && execSessionId !== '') {
1761
1979
  invokeParams = {
1762
1980
  ...invokeParams,
@@ -2167,8 +2385,13 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2167
2385
  approvalReplaySessionId,
2168
2386
  approvalReplayKey
2169
2387
  );
2170
- const approvalReviewEvidence = getToolApprovalReviewEvidence(config,
2171
- getToolBatchReplayOwner(config, this.executingAgentId ?? this.agentId ?? ''));
2388
+ const approvalReviewEvidence = getToolApprovalReviewEvidence(
2389
+ config,
2390
+ getToolBatchReplayOwner(
2391
+ config,
2392
+ this.executingAgentId ?? this.agentId ?? ''
2393
+ )
2394
+ );
2172
2395
  const reviewedApproval = getReviewedToolApproval(
2173
2396
  approvalReviewEvidence?.payload,
2174
2397
  call.id
@@ -2294,6 +2517,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2294
2517
  runId,
2295
2518
  threadId,
2296
2519
  agentId: this.agentId,
2520
+ executionContext: this.executionContext,
2297
2521
  executingAgentId: this.executingAgentId,
2298
2522
  toolName: call.name,
2299
2523
  toolInput: resolvedArgs,
@@ -2305,11 +2529,17 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2305
2529
  matchQuery: call.name,
2306
2530
  onceReplayKey: approvalReplayKey,
2307
2531
  onceReplaySessionId: approvalReplaySessionId,
2308
- }).catch((): AggregatedHookResult | undefined => approvalReviewEvidence == null ? undefined : {
2309
- decision: 'deny',
2310
- reason: 'Approval policy could not be evaluated on resume',
2311
- additionalContexts: [], injectedMessages: [], errors: [],
2312
- });
2532
+ }).catch((): AggregatedHookResult | undefined =>
2533
+ approvalReviewEvidence == null
2534
+ ? undefined
2535
+ : {
2536
+ decision: 'deny',
2537
+ reason: 'Approval policy could not be evaluated on resume',
2538
+ additionalContexts: [],
2539
+ injectedMessages: [],
2540
+ errors: [],
2541
+ }
2542
+ );
2313
2543
 
2314
2544
  if (preResult != null) {
2315
2545
  // Forward any additionalContext strings hooks returned into
@@ -2651,6 +2881,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2651
2881
  runId,
2652
2882
  threadId,
2653
2883
  agentId: this.agentId,
2884
+ executionContext: this.executionContext,
2654
2885
  executingAgentId: this.executingAgentId,
2655
2886
  toolName: call.name,
2656
2887
  toolInput: effectiveCall.args as Record<string, unknown>,
@@ -2688,6 +2919,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2688
2919
  runId,
2689
2920
  threadId,
2690
2921
  agentId: this.agentId,
2922
+ executionContext: this.executionContext,
2691
2923
  executingAgentId: this.executingAgentId,
2692
2924
  toolName: call.name,
2693
2925
  toolInput: effectiveCall.args as Record<string, unknown>,
@@ -2804,6 +3036,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2804
3036
  runId,
2805
3037
  threadId,
2806
3038
  agentId: this.agentId,
3039
+ executionContext: this.executionContext,
2807
3040
  executingAgentId: this.executingAgentId,
2808
3041
  toolName: call.name,
2809
3042
  toolInput: resolvedArgs,
@@ -2935,18 +3168,26 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2935
3168
 
2936
3169
  private storeCodeSessionFromResults(
2937
3170
  results: t.ToolExecuteResult[],
2938
- requestMap: Map<string, t.ToolCallRequest>
3171
+ requestMap: Map<string, t.ToolCallRequest>,
3172
+ baselineByRequestId: ReadonlyMap<
3173
+ string,
3174
+ ReadonlyMap<string, string>
3175
+ > = new Map(),
3176
+ executionBaselineByRequestId: ReadonlyMap<
3177
+ string,
3178
+ ReadonlyMap<string, string>
3179
+ > = baselineByRequestId
2939
3180
  ): void {
2940
3181
  if (!this.sessions) {
2941
3182
  return;
2942
3183
  }
2943
3184
 
3185
+ this.retainCodeSessionInputsFromRequests(
3186
+ requestMap.values(),
3187
+ baselineByRequestId
3188
+ );
2944
3189
  for (let i = 0; i < results.length; i++) {
2945
3190
  const result = results[i];
2946
- if (result.status !== 'success' || result.artifact == null) {
2947
- continue;
2948
- }
2949
-
2950
3191
  const request = requestMap.get(result.toolCallId);
2951
3192
  if (
2952
3193
  request?.name == null ||
@@ -2957,6 +3198,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2957
3198
  continue;
2958
3199
  }
2959
3200
 
3201
+ if (result.status !== 'success' || result.artifact == null) {
3202
+ continue;
3203
+ }
3204
+
2960
3205
  const artifact = result.artifact as t.CodeExecutionArtifact | undefined;
2961
3206
  const execSessionId = artifact?.session_id;
2962
3207
  if (execSessionId == null || execSessionId === '') {
@@ -2967,11 +3212,59 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2967
3212
  this.sessions,
2968
3213
  this.codeSessionKey,
2969
3214
  execSessionId,
2970
- artifact?.files
3215
+ artifact?.files,
3216
+ artifact?.deleted_files,
3217
+ executionBaselineByRequestId.get(request.id)
2971
3218
  );
2972
3219
  }
2973
3220
  }
2974
3221
 
3222
+ private retainCodeSessionInputsFromRequests(
3223
+ requests: Iterable<t.ToolCallRequest>,
3224
+ baselineByRequestId: ReadonlyMap<
3225
+ string,
3226
+ ReadonlyMap<string, string>
3227
+ >
3228
+ ): void {
3229
+ if (!this.sessions) {
3230
+ return;
3231
+ }
3232
+ const refreshedByName = new Map<string, t.CodeEnvFile>();
3233
+ let refreshedSessionId: string | undefined;
3234
+ let fileSessionId: string | undefined;
3235
+ for (const request of requests) {
3236
+ if (
3237
+ request.name === '' ||
3238
+ (!this.participatesInCodeSession(request.name) &&
3239
+ request.name !== Constants.SKILL_TOOL &&
3240
+ request.name !== Constants.READ_FILE &&
3241
+ request.retainCodeSessionInputs !== true)
3242
+ ) {
3243
+ continue;
3244
+ }
3245
+ const context = request.codeSessionContext;
3246
+ if (context?.session_id == null || context.session_id === '') continue;
3247
+ if (context.session_id !== request.codeSessionBaselineId) {
3248
+ refreshedSessionId = context.session_id;
3249
+ }
3250
+ const baselineIdentityByName = baselineByRequestId.get(request.id);
3251
+ for (const file of context.files ?? []) {
3252
+ if (!file.id || !file.name || !file.storage_session_id) continue;
3253
+ if (baselineIdentityByName?.get(file.name) === fileIdentityKey(file)) {
3254
+ continue;
3255
+ }
3256
+ fileSessionId = context.session_id;
3257
+ refreshedByName.set(file.name, file);
3258
+ }
3259
+ }
3260
+ const sessionId = refreshedSessionId ?? fileSessionId;
3261
+ if (sessionId == null) return;
3262
+ retainCodeSessionInputs(this.sessions, this.codeSessionKey, {
3263
+ session_id: sessionId,
3264
+ files: [...refreshedByName.values()],
3265
+ });
3266
+ }
3267
+
2975
3268
  /**
2976
3269
  * Post-processes standard runTool outputs: dispatches ON_RUN_STEP_COMPLETED
2977
3270
  * and stores code session context. Mirrors the completion handling in
@@ -2990,6 +3283,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2990
3283
  outputs: (BaseMessage | Command)[],
2991
3284
  config: RunnableConfig,
2992
3285
  resolvedArgsByCallId?: ResolvedArgsByCallId,
3286
+ codeSessionBaselineByCallId?: ReadonlyMap<
3287
+ string,
3288
+ ReadonlyMap<string, string>
3289
+ >,
2993
3290
  errorOwnership?: ToolErrorOwnership
2994
3291
  ): Promise<void> {
2995
3292
  const ownership = errorOwnership ?? this.looseErrorOwnership;
@@ -3025,7 +3322,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3025
3322
  ownership.undispatched.delete(toolCallId);
3026
3323
  }
3027
3324
 
3028
- if (this.sessions && this.participatesInCodeSession(call.name)) {
3325
+ if (
3326
+ toolMessage.status !== 'error' &&
3327
+ this.sessions &&
3328
+ this.participatesInCodeSession(call.name)
3329
+ ) {
3029
3330
  const artifact = toolMessage.artifact as
3030
3331
  | t.CodeExecutionArtifact
3031
3332
  | undefined;
@@ -3035,7 +3336,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3035
3336
  this.sessions,
3036
3337
  this.codeSessionKey,
3037
3338
  execSessionId,
3038
- artifact?.files
3339
+ artifact?.files,
3340
+ artifact?.deleted_files,
3341
+ codeSessionBaselineByCallId?.get(toolCallId)
3039
3342
  );
3040
3343
  }
3041
3344
  }
@@ -3178,10 +3481,13 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3178
3481
  unresolvedByCallId.set(call.id, pre.unresolved);
3179
3482
  }
3180
3483
  } else if (registry != null) {
3181
- const options = { substituteIntentKey: this.toolDeclaresBusinessIntent(call.name) };
3182
- const { resolved, unresolved } = preBatchSnapshot != null
3183
- ? preBatchSnapshot.resolve(originalArgs, options)
3184
- : registry.resolve(registryRunId, originalArgs, options);
3484
+ const options = {
3485
+ substituteIntentKey: this.toolDeclaresBusinessIntent(call.name),
3486
+ };
3487
+ const { resolved, unresolved } =
3488
+ preBatchSnapshot != null
3489
+ ? preBatchSnapshot.resolve(originalArgs, options)
3490
+ : registry.resolve(registryRunId, originalArgs, options);
3185
3491
  resolvedArgs = resolved as Record<string, unknown>;
3186
3492
  if (unresolved.length > 0 && call.id != null) {
3187
3493
  unresolvedByCallId.set(call.id, unresolved);
@@ -3229,8 +3535,13 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3229
3535
  config,
3230
3536
  runId
3231
3537
  );
3232
- const approvalReviewEvidence = getToolApprovalReviewEvidence(config,
3233
- getToolBatchReplayOwner(config, this.executingAgentId ?? this.agentId ?? ''));
3538
+ const approvalReviewEvidence = getToolApprovalReviewEvidence(
3539
+ config,
3540
+ getToolBatchReplayOwner(
3541
+ config,
3542
+ this.executingAgentId ?? this.agentId ?? ''
3543
+ )
3544
+ );
3234
3545
  const hasPendingApproval = preToolCalls.some(
3235
3546
  (entry) =>
3236
3547
  entry.call.id != null &&
@@ -3295,6 +3606,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3295
3606
  runId,
3296
3607
  threadId,
3297
3608
  agentId: this.agentId,
3609
+ executionContext: this.executionContext,
3298
3610
  executingAgentId: this.executingAgentId,
3299
3611
  toolName: entry.call.name,
3300
3612
  toolInput: entry.args,
@@ -3397,6 +3709,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3397
3709
  runId,
3398
3710
  threadId,
3399
3711
  agentId: this.agentId,
3712
+ executionContext: this.executionContext,
3400
3713
  executingAgentId: this.executingAgentId,
3401
3714
  toolName: item.toolName,
3402
3715
  toolInput: item.args,
@@ -3504,9 +3817,15 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3504
3817
  if (hookResult.updatedInput != null) {
3505
3818
  applyInputOverride(entry, hookResult.updatedInput);
3506
3819
  } else {
3507
- const reviewed = getReviewedToolApproval(approvalReviewEvidence?.payload, entry.call.id);
3820
+ const reviewed = getReviewedToolApproval(
3821
+ approvalReviewEvidence?.payload,
3822
+ entry.call.id
3823
+ );
3508
3824
  if (reviewed != null) {
3509
- entry.args = this.coerceEventToolArgs(entry.call.name, reviewed.request.arguments);
3825
+ entry.args = this.coerceEventToolArgs(
3826
+ entry.call.name,
3827
+ reviewed.request.arguments
3828
+ );
3510
3829
  if (entry.call.id != null) {
3511
3830
  unresolvedByCallId.delete(entry.call.id);
3512
3831
  }
@@ -3877,6 +4196,18 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3877
4196
  }
3878
4197
 
3879
4198
  const requestMap = new Map(plan.allRequests.map((r) => [r.id, r]));
4199
+ for (const request of plan.allRequests) {
4200
+ request.codeSessionBaselineId = request.codeSessionContext?.session_id;
4201
+ }
4202
+ const codeSessionBaselineByRequestId = new Map<
4203
+ string,
4204
+ ReadonlyMap<string, string>
4205
+ >(
4206
+ plan.allRequests.map((request) => [
4207
+ request.id,
4208
+ codeSessionIdentityByName(request.codeSessionContext),
4209
+ ])
4210
+ );
3880
4211
  const eagerExecutions: Array<{
3881
4212
  request: t.ToolCallRequest;
3882
4213
  execution: t.EagerEventToolExecution;
@@ -3887,6 +4218,14 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3887
4218
  const eagerExecution = this.takeMatchingEagerEventExecution(request);
3888
4219
  if (eagerExecution != null) {
3889
4220
  eagerExecutions.push({ request, execution: eagerExecution });
4221
+ if (eagerExecution.codeSessionBaselineByName != null) {
4222
+ codeSessionBaselineByRequestId.set(
4223
+ request.id,
4224
+ eagerExecution.codeSessionBaselineByName
4225
+ );
4226
+ }
4227
+ request.codeSessionBaselineId =
4228
+ eagerExecution.request.codeSessionBaselineId;
3890
4229
  } else {
3891
4230
  dispatchRequests.push(request);
3892
4231
  }
@@ -3972,6 +4311,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3972
4311
  };
3973
4312
 
3974
4313
  const batchRequest: t.ToolExecuteBatchRequest = {
4314
+ executionContext: this.executionContext,
3975
4315
  toolCalls: dispatchRequests,
3976
4316
  userId: config.configurable?.user_id as string | undefined,
3977
4317
  // Dispatch attribution, NOT the hook subagent-scope marker:
@@ -4011,24 +4351,46 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4011
4351
 
4012
4352
  const eagerResultsPromise = Promise.all(
4013
4353
  eagerExecutions.map(async ({ request, execution }) => {
4014
- const results = await this.resolveEagerEventExecution(
4015
- request,
4016
- execution
4017
- );
4018
- return {
4019
- results,
4020
- completionDispatched:
4021
- execution.completionDispatched === true &&
4022
- execution.request.turn === request.turn,
4023
- toolCallId: request.id,
4024
- };
4354
+ try {
4355
+ const results = await this.resolveEagerEventExecution(
4356
+ request,
4357
+ execution
4358
+ );
4359
+ return {
4360
+ results,
4361
+ completionDispatched:
4362
+ execution.completionDispatched === true &&
4363
+ execution.request.turn === request.turn,
4364
+ toolCallId: request.id,
4365
+ };
4366
+ } finally {
4367
+ if (execution.request.codeSessionContext != null) {
4368
+ request.codeSessionContext = execution.request.codeSessionContext;
4369
+ }
4370
+ }
4025
4371
  })
4026
4372
  );
4027
4373
 
4028
- const [eagerResults, dispatchedResults] = await Promise.all([
4374
+ const [eagerOutcome, dispatchedOutcome] = await Promise.allSettled([
4029
4375
  eagerResultsPromise,
4030
4376
  dispatchPromise,
4031
4377
  ]);
4378
+ if (eagerOutcome.status === 'rejected') {
4379
+ this.retainCodeSessionInputsFromRequests(
4380
+ requestMap.values(),
4381
+ codeSessionBaselineByRequestId
4382
+ );
4383
+ throw eagerOutcome.reason;
4384
+ }
4385
+ if (dispatchedOutcome.status === 'rejected') {
4386
+ this.retainCodeSessionInputsFromRequests(
4387
+ requestMap.values(),
4388
+ codeSessionBaselineByRequestId
4389
+ );
4390
+ throw dispatchedOutcome.reason;
4391
+ }
4392
+ const eagerResults = eagerOutcome.value;
4393
+ const dispatchedResults = dispatchedOutcome.value;
4032
4394
  // Settle in-flight early completion dispatches before the batch loop
4033
4395
  // below decides which completions still need emitting.
4034
4396
  await Promise.allSettled(earlyCompletionDispatches);
@@ -4045,8 +4407,26 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4045
4407
  ...flattenedEagerResults,
4046
4408
  ...dispatchedResults,
4047
4409
  ];
4410
+ /* Hosts may lazily refresh codeSessionContext while dispatching. The
4411
+ * pre-dispatch snapshot above remains the authority for detecting those
4412
+ * additions, while deletion reconciliation must use the identities the
4413
+ * execution actually received. */
4414
+ const executionBaselineByRequestId = new Map<
4415
+ string,
4416
+ ReadonlyMap<string, string>
4417
+ >(
4418
+ plan.allRequests.map((request) => [
4419
+ request.id,
4420
+ codeSessionIdentityByName(request.codeSessionContext),
4421
+ ])
4422
+ );
4048
4423
 
4049
- this.storeCodeSessionFromResults(results, requestMap);
4424
+ this.storeCodeSessionFromResults(
4425
+ results,
4426
+ requestMap,
4427
+ codeSessionBaselineByRequestId,
4428
+ executionBaselineByRequestId
4429
+ );
4050
4430
 
4051
4431
  for (const result of results) {
4052
4432
  if (result.injectedMessages && result.injectedMessages.length > 0) {
@@ -4115,6 +4495,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4115
4495
  runId,
4116
4496
  threadId,
4117
4497
  agentId: this.agentId,
4498
+ executionContext: this.executionContext,
4118
4499
  executingAgentId: this.executingAgentId,
4119
4500
  toolName,
4120
4501
  toolInput: request?.args ?? {},
@@ -4167,6 +4548,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4167
4548
  runId,
4168
4549
  threadId,
4169
4550
  agentId: this.agentId,
4551
+ executionContext: this.executionContext,
4170
4552
  executingAgentId: this.executingAgentId,
4171
4553
  toolName,
4172
4554
  toolInput: request?.args ?? {},
@@ -4326,28 +4708,40 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4326
4708
  // effects) every round.
4327
4709
  this.eagerEventToolSuppressions?.add(request.name);
4328
4710
  this.eagerEventToolSuppressions?.add(execution.toolName);
4711
+ if (
4712
+ this.participatesInCodeSession(execution.toolName) ||
4713
+ execution.toolName === Constants.SKILL_TOOL ||
4714
+ execution.toolName === Constants.READ_FILE
4715
+ ) {
4716
+ request.retainCodeSessionInputs = true;
4717
+ }
4329
4718
  // eslint-disable-next-line no-console
4330
4719
  console.warn(
4331
4720
  '[ToolNode] eager prestart args diverged from the final request for ' +
4332
4721
  `tool "${request.name}" (toolCallId=${request.id}); suppressing ` +
4333
4722
  'eager prestart for this tool for the rest of the run'
4334
4723
  );
4724
+ const createMismatchOutcome = (): t.EagerEventToolExecutionOutcome => ({
4725
+ results: [
4726
+ {
4727
+ toolCallId: request.id,
4728
+ status: 'error',
4729
+ content: '',
4730
+ errorMessage:
4731
+ 'Tool call changed after eager execution started; refusing to re-run the tool to avoid duplicate side effects.',
4732
+ },
4733
+ ],
4734
+ });
4335
4735
  return {
4336
4736
  toolCallId: request.id,
4337
4737
  toolName: request.name,
4338
4738
  args: request.args,
4339
- request,
4340
- promise: Promise.resolve({
4341
- results: [
4342
- {
4343
- toolCallId: request.id,
4344
- status: 'error',
4345
- content: '',
4346
- errorMessage:
4347
- 'Tool call changed after eager execution started; refusing to re-run the tool to avoid duplicate side effects.',
4348
- },
4349
- ],
4350
- }),
4739
+ request: execution.request,
4740
+ codeSessionBaselineByName: execution.codeSessionBaselineByName,
4741
+ promise: execution.promise.then(
4742
+ createMismatchOutcome,
4743
+ createMismatchOutcome
4744
+ ),
4351
4745
  };
4352
4746
  }
4353
4747
 
@@ -4439,6 +4833,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4439
4833
  runId,
4440
4834
  threadId,
4441
4835
  agentId: this.agentId,
4836
+ executionContext: this.executionContext,
4442
4837
  executingAgentId: this.executingAgentId,
4443
4838
  entries: orderedBatchEntries,
4444
4839
  },
@@ -4647,13 +5042,16 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4647
5042
  position: number,
4648
5043
  result: SettledDirectToolResult
4649
5044
  ): void => {
4650
- if (
4651
- baseContext.replayBatchKey == null ||
4652
- settledBatchResults == null
4653
- ) {
5045
+ if (baseContext.replayBatchKey == null || settledBatchResults == null) {
4654
5046
  return;
4655
5047
  }
4656
- settledBatchResults.set(directReplayKey(call, baseContext.replayCallPositions?.get(call) ?? batchIndices[position]), result);
5048
+ settledBatchResults.set(
5049
+ directReplayKey(
5050
+ call,
5051
+ baseContext.replayCallPositions?.get(call) ?? batchIndices[position]
5052
+ ),
5053
+ result
5054
+ );
4657
5055
  this.settledDirectResultsByBatch.set(
4658
5056
  baseContext.replayBatchKey,
4659
5057
  settledBatchResults
@@ -4668,14 +5066,26 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4668
5066
  if (turn != null) {
4669
5067
  this.recordToolUsageTurn(call.name, turn, call.id);
4670
5068
  }
4671
- const refMeta = isBaseMessage(result.output) ? result.output.additional_kwargs as t.ToolMessageRefMetadata : undefined;
5069
+ const refMeta = isBaseMessage(result.output)
5070
+ ? (result.output.additional_kwargs as t.ToolMessageRefMetadata)
5071
+ : undefined;
4672
5072
  if (refMeta?._refKey != null && result.referenceContent != null) {
4673
- this.toolOutputRegistry?.set(baseContext.batchScopeId, refMeta._refKey, result.referenceContent);
5073
+ this.toolOutputRegistry?.set(
5074
+ baseContext.batchScopeId,
5075
+ refMeta._refKey,
5076
+ result.referenceContent
5077
+ );
4674
5078
  if (isBaseMessage(result.output)) {
4675
- result = { ...result, output: new ToolMessage({
4676
- ...(result.output as ToolMessage),
4677
- additional_kwargs: { ...result.output.additional_kwargs, _refScope: baseContext.batchScopeId },
4678
- }) };
5079
+ result = {
5080
+ ...result,
5081
+ output: new ToolMessage({
5082
+ ...(result.output as ToolMessage),
5083
+ additional_kwargs: {
5084
+ ...result.output.additional_kwargs,
5085
+ _refScope: baseContext.batchScopeId,
5086
+ },
5087
+ }),
5088
+ };
4679
5089
  }
4680
5090
  }
4681
5091
  if (
@@ -4685,17 +5095,38 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4685
5095
  ) {
4686
5096
  baseContext.resolvedArgsByCallId.set(call.id, result.resolvedArgs);
4687
5097
  }
5098
+ if (
5099
+ result.codeSessionBaseline != null &&
5100
+ baseContext.codeSessionBaselineByCallId != null
5101
+ ) {
5102
+ baseContext.codeSessionBaselineByCallId.set(
5103
+ call.id ?? '',
5104
+ new Map(result.codeSessionBaseline)
5105
+ );
5106
+ }
4688
5107
  return result;
4689
5108
  };
4690
5109
  const runOne = async (
4691
5110
  call: ToolCall,
4692
5111
  position: number
4693
5112
  ): Promise<SettledDirectToolResult> => {
4694
- const cachedResult = settledBatchResults?.get(directReplayKey(call, baseContext.replayCallPositions?.get(call) ?? batchIndices[position]));
5113
+ const cachedResult = settledBatchResults?.get(
5114
+ directReplayKey(
5115
+ call,
5116
+ baseContext.replayCallPositions?.get(call) ?? batchIndices[position]
5117
+ )
5118
+ );
4695
5119
  if (cachedResult != null) {
4696
- if (cachedResult.proposal.name !== call.name ||
4697
- !recordArgsEqual(cachedResult.proposal.args, call.args as Record<string, unknown>)) {
4698
- throw new Error('Cannot change an already completed tool call during approval replay');
5120
+ if (
5121
+ cachedResult.proposal.name !== call.name ||
5122
+ !recordArgsEqual(
5123
+ cachedResult.proposal.args,
5124
+ call.args as Record<string, unknown>
5125
+ )
5126
+ ) {
5127
+ throw new Error(
5128
+ 'Cannot change an already completed tool call during approval replay'
5129
+ );
4699
5130
  }
4700
5131
  return restoreResult(call, cachedResult);
4701
5132
  }
@@ -4709,16 +5140,33 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4709
5140
  call.id == null
4710
5141
  ? undefined
4711
5142
  : baseContext.resolvedArgsByCallId?.get(call.id);
5143
+ const codeSessionBaseline =
5144
+ baseContext.codeSessionBaselineByCallId?.get(call.id ?? '');
4712
5145
  const result: SettledDirectToolResult = {
4713
- proposal: structuredClone({ name: call.name, args: call.args as Record<string, unknown> }),
4714
- turn: call.id == null ? undefined : this.toolCallTurns.get(call.id) ?? this.directPathTurns.get(call.id),
5146
+ proposal: structuredClone({
5147
+ name: call.name,
5148
+ args: call.args as Record<string, unknown>,
5149
+ }),
5150
+ turn:
5151
+ call.id == null
5152
+ ? undefined
5153
+ : (this.toolCallTurns.get(call.id) ??
5154
+ this.directPathTurns.get(call.id)),
4715
5155
  output,
4716
5156
  additionalContexts,
4717
5157
  ...(resolvedArgs == null ? {} : { resolvedArgs }),
5158
+ ...(codeSessionBaseline == null
5159
+ ? {}
5160
+ : { codeSessionBaseline: [...codeSessionBaseline.entries()] }),
4718
5161
  };
4719
- const refMeta = isBaseMessage(output) ? output.additional_kwargs as t.ToolMessageRefMetadata : undefined;
5162
+ const refMeta = isBaseMessage(output)
5163
+ ? (output.additional_kwargs as t.ToolMessageRefMetadata)
5164
+ : undefined;
4720
5165
  if (refMeta?._refKey != null) {
4721
- result.referenceContent = this.toolOutputRegistry?.get(refMeta._refScope, refMeta._refKey);
5166
+ result.referenceContent = this.toolOutputRegistry?.get(
5167
+ refMeta._refScope,
5168
+ refMeta._refKey
5169
+ );
4722
5170
  }
4723
5171
  cacheResult(call, position, result);
4724
5172
  return restoreResult(call, result);
@@ -4734,8 +5182,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4734
5182
  directCalls.map((call, i) => runOne(call, i))
4735
5183
  );
4736
5184
  this.throwIfBreakerTripped(config);
4737
- const failed = results.find((result) => result.status === 'rejected' && !isGraphInterrupt(result.reason)) ??
4738
- results.find((result) => result.status === 'rejected');
5185
+ const failed =
5186
+ results.find(
5187
+ (result) =>
5188
+ result.status === 'rejected' && !isGraphInterrupt(result.reason)
5189
+ ) ?? results.find((result) => result.status === 'rejected');
4739
5190
  if (failed?.status === 'rejected') {
4740
5191
  throw failed.reason;
4741
5192
  }
@@ -4877,8 +5328,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4877
5328
  return (Array.isArray(input) ? outputs : { messages: outputs }) as T;
4878
5329
  }
4879
5330
 
4880
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
4881
- protected async run(input: any, config: RunnableConfig, referenceReplay: { state?: ToolOutputReferenceState } = {}): Promise<T> {
5331
+ protected async run(
5332
+ input: T,
5333
+ config: RunnableConfig,
5334
+ referenceReplay: { state?: ToolOutputReferenceState } = {}
5335
+ ): Promise<T> {
4882
5336
  /**
4883
5337
  * Breaker read once at batch entry: every downstream path (direct
4884
5338
  * `tool.invoke` runtimes and the ON_TOOL_EXECUTE dispatch) inherits this
@@ -4922,6 +5376,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4922
5376
  * ToolNode cannot read or wipe each other's entries.
4923
5377
  */
4924
5378
  const resolvedArgsByCallId = new Map<string, Record<string, unknown>>();
5379
+ const codeSessionBaselineByCallId = new Map<
5380
+ string,
5381
+ ReadonlyMap<string, string>
5382
+ >();
4925
5383
  /** Per-invocation error-completion ownership — see `ToolErrorOwnership`. */
4926
5384
  const errorOwnership = createToolErrorOwnership();
4927
5385
  /**
@@ -4938,10 +5396,20 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4938
5396
  const incomingRunId = config.configurable?.run_id as string | undefined;
4939
5397
  const batchScopeId = incomingRunId ?? `\0anon-${this.anonBatchCounter++}`;
4940
5398
  const resumedReferenceState = referenceReplay.state;
4941
- const replayInputSnapshot = resumedReferenceState == null ? undefined :
4942
- this.toolOutputRegistry?.resumeBatch(batchScopeId, resumedReferenceState);
4943
- referenceReplay.state = resumedReferenceState ?? this.toolOutputRegistry?.snapshotState(batchScopeId);
4944
- const turn = resumedReferenceState?.turnCounter ?? this.toolOutputRegistry?.nextTurn(batchScopeId) ?? 0;
5399
+ const replayInputSnapshot =
5400
+ resumedReferenceState == null
5401
+ ? undefined
5402
+ : this.toolOutputRegistry?.resumeBatch(
5403
+ batchScopeId,
5404
+ resumedReferenceState
5405
+ );
5406
+ referenceReplay.state =
5407
+ resumedReferenceState ??
5408
+ this.toolOutputRegistry?.snapshotState(batchScopeId);
5409
+ const turn =
5410
+ resumedReferenceState?.turnCounter ??
5411
+ this.toolOutputRegistry?.nextTurn(batchScopeId) ??
5412
+ 0;
4945
5413
  let outputs: (BaseMessage | Command)[];
4946
5414
  let replayBatchKey: string | undefined;
4947
5415
  /** Hoisted from the messages-state branch so the Command tail can carry
@@ -4992,6 +5460,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4992
5460
  batchScopeId,
4993
5461
  preBatchSnapshot: replayInputSnapshot,
4994
5462
  resolvedArgsByCallId,
5463
+ codeSessionBaselineByCallId,
4995
5464
  errorOwnership,
4996
5465
  additionalContextsSink: directAdditionalContexts,
4997
5466
  replayBatchKey: sendReplayBatchKey,
@@ -5020,6 +5489,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5020
5489
  [sendOutput],
5021
5490
  config,
5022
5491
  resolvedArgsByCallId,
5492
+ codeSessionBaselineByCallId,
5023
5493
  errorOwnership
5024
5494
  );
5025
5495
  } else {
@@ -5045,10 +5515,13 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5045
5515
  throw new Error('ToolNode only accepts AIMessages as input.');
5046
5516
  }
5047
5517
  const aiMessage = assistantBatch.message;
5048
- replayBatchKey = getToolBatchReplayScope(config) == null ? undefined : getAssistantBatchReplayKey(
5049
- assistantBatch,
5050
- getToolBatchReplayScope(config)
5051
- );
5518
+ replayBatchKey =
5519
+ getToolBatchReplayScope(config) == null
5520
+ ? undefined
5521
+ : getAssistantBatchReplayKey(
5522
+ assistantBatch,
5523
+ getToolBatchReplayScope(config)
5524
+ );
5052
5525
 
5053
5526
  if (this.loadRuntimeTools) {
5054
5527
  const { tools, toolMap } = this.loadRuntimeTools(
@@ -5059,7 +5532,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5059
5532
  this.applyToolExecutionOverrides();
5060
5533
  }
5061
5534
 
5062
- const replayCallPositions = new Map((aiMessage.tool_calls ?? []).map((call, i) => [call, i]));
5535
+ const replayCallPositions = new Map(
5536
+ (aiMessage.tool_calls ?? []).map((call, i) => [call, i])
5537
+ );
5063
5538
  const filteredCalls =
5064
5539
  aiMessage.tool_calls?.filter((call) => {
5065
5540
  /**
@@ -5069,7 +5544,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5069
5544
  * which are executed by the provider's API and don't require invocation
5070
5545
  */
5071
5546
  return (
5072
- (call.id == null || call.id === '' || !toolMessageIds.has(call.id)) &&
5547
+ (call.id == null ||
5548
+ call.id === '' ||
5549
+ !toolMessageIds.has(call.id)) &&
5073
5550
  !(
5074
5551
  call.id?.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX) ??
5075
5552
  false
@@ -5234,7 +5711,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5234
5711
  * same-turn direct outputs.
5235
5712
  */
5236
5713
  const preBatchSnapshot =
5237
- replayInputSnapshot ?? this.toolOutputRegistry?.snapshot(batchScopeId);
5714
+ replayInputSnapshot ??
5715
+ this.toolOutputRegistry?.snapshot(batchScopeId);
5238
5716
  if (preBatchSnapshot != null) {
5239
5717
  for (const entry of eventEntries) {
5240
5718
  if (entry.call.id != null) {
@@ -5269,6 +5747,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5269
5747
  turn,
5270
5748
  batchScopeId,
5271
5749
  resolvedArgsByCallId,
5750
+ codeSessionBaselineByCallId,
5272
5751
  errorOwnership,
5273
5752
  preBatchSnapshot,
5274
5753
  additionalContextsSink: directAdditionalContexts,
@@ -5281,11 +5760,18 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5281
5760
  : [];
5282
5761
 
5283
5762
  const settledDirectResults =
5284
- replayBatchKey == null ? undefined : this.settledDirectResultsByBatch.get(replayBatchKey);
5763
+ replayBatchKey == null
5764
+ ? undefined
5765
+ : this.settledDirectResultsByBatch.get(replayBatchKey);
5285
5766
  const unhandledDirectCalls: ToolCall[] = [];
5286
5767
  const unhandledDirectOutputs: (BaseMessage | Command)[] = [];
5287
5768
  for (let i = 0; i < directCalls.length; i++) {
5288
- const settled = settledDirectResults?.get(directReplayKey(directCalls[i], replayCallPositions.get(directCalls[i])!));
5769
+ const settled = settledDirectResults?.get(
5770
+ directReplayKey(
5771
+ directCalls[i],
5772
+ replayCallPositions.get(directCalls[i])!
5773
+ )
5774
+ );
5289
5775
  if (settled?.completionHandled === true) {
5290
5776
  continue;
5291
5777
  }
@@ -5299,10 +5785,16 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5299
5785
  unhandledDirectOutputs,
5300
5786
  config,
5301
5787
  resolvedArgsByCallId,
5788
+ codeSessionBaselineByCallId,
5302
5789
  errorOwnership
5303
5790
  );
5304
5791
  for (let i = 0; i < directCalls.length; i++) {
5305
- const settled = settledDirectResults?.get(directReplayKey(directCalls[i], replayCallPositions.get(directCalls[i])!));
5792
+ const settled = settledDirectResults?.get(
5793
+ directReplayKey(
5794
+ directCalls[i],
5795
+ replayCallPositions.get(directCalls[i])!
5796
+ )
5797
+ );
5306
5798
  if (settled != null) {
5307
5799
  settled.completionHandled = true;
5308
5800
  }
@@ -5355,7 +5847,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5355
5847
  // leak a sibling's just-registered output into a sister
5356
5848
  // call's args mid-await (Codex P1 #18).
5357
5849
  const preBatchSnapshot =
5358
- replayInputSnapshot ?? this.toolOutputRegistry?.snapshot(batchScopeId);
5850
+ replayInputSnapshot ??
5851
+ this.toolOutputRegistry?.snapshot(batchScopeId);
5359
5852
  const directAdditionalContexts: string[] = [];
5360
5853
  const approvalScopeCallIds = new Set(
5361
5854
  filteredCalls.flatMap((call) => (call.id == null ? [] : [call.id]))
@@ -5368,6 +5861,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5368
5861
  turn,
5369
5862
  batchScopeId,
5370
5863
  resolvedArgsByCallId,
5864
+ codeSessionBaselineByCallId,
5371
5865
  errorOwnership,
5372
5866
  preBatchSnapshot,
5373
5867
  additionalContextsSink: directAdditionalContexts,
@@ -5382,6 +5876,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5382
5876
  toolOutputs,
5383
5877
  config,
5384
5878
  resolvedArgsByCallId,
5879
+ codeSessionBaselineByCallId,
5385
5880
  errorOwnership
5386
5881
  );
5387
5882
  // Append accumulated additionalContexts as a single