@librechat/agents 3.5.1 → 3.6.0

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 (93) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +5 -2
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +148 -22
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/graphs/MultiAgentGraph.cjs +2 -1
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  7. package/dist/cjs/main.cjs +4 -0
  8. package/dist/cjs/run.cjs +29 -11
  9. package/dist/cjs/run.cjs.map +1 -1
  10. package/dist/cjs/stream.cjs +8 -6
  11. package/dist/cjs/stream.cjs.map +1 -1
  12. package/dist/cjs/tools/BashExecutor.cjs +8 -7
  13. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  14. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +5 -4
  15. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  16. package/dist/cjs/tools/CodeExecutor.cjs +26 -7
  17. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  18. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +7 -6
  19. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  20. package/dist/cjs/tools/ToolNode.cjs +33 -10
  21. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  22. package/dist/cjs/tools/ToolSearch.cjs +1 -1
  23. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  24. package/dist/cjs/tools/runStepResume.cjs +51 -0
  25. package/dist/cjs/tools/runStepResume.cjs.map +1 -0
  26. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +15 -43
  27. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  28. package/dist/cjs/tools/subagent/SubagentReplay.cjs +6 -1
  29. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
  30. package/dist/cjs/utils/toolSessions.cjs +63 -0
  31. package/dist/cjs/utils/toolSessions.cjs.map +1 -0
  32. package/dist/esm/agents/AgentContext.mjs +5 -2
  33. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  34. package/dist/esm/graphs/Graph.mjs +148 -22
  35. package/dist/esm/graphs/Graph.mjs.map +1 -1
  36. package/dist/esm/graphs/MultiAgentGraph.mjs +2 -1
  37. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  38. package/dist/esm/main.mjs +2 -2
  39. package/dist/esm/run.mjs +29 -11
  40. package/dist/esm/run.mjs.map +1 -1
  41. package/dist/esm/stream.mjs +8 -6
  42. package/dist/esm/stream.mjs.map +1 -1
  43. package/dist/esm/tools/BashExecutor.mjs +9 -8
  44. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  45. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +6 -5
  46. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  47. package/dist/esm/tools/CodeExecutor.mjs +23 -8
  48. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  49. package/dist/esm/tools/ProgrammaticToolCalling.mjs +8 -7
  50. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  51. package/dist/esm/tools/ToolNode.mjs +33 -10
  52. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  53. package/dist/esm/tools/ToolSearch.mjs +2 -2
  54. package/dist/esm/tools/ToolSearch.mjs.map +1 -1
  55. package/dist/esm/tools/runStepResume.mjs +48 -0
  56. package/dist/esm/tools/runStepResume.mjs.map +1 -0
  57. package/dist/esm/tools/subagent/SubagentExecutor.mjs +15 -43
  58. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  59. package/dist/esm/tools/subagent/SubagentReplay.mjs +6 -1
  60. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
  61. package/dist/esm/utils/toolSessions.mjs +62 -0
  62. package/dist/esm/utils/toolSessions.mjs.map +1 -0
  63. package/dist/types/agents/AgentContext.d.ts +4 -1
  64. package/dist/types/graphs/Graph.d.ts +11 -3
  65. package/dist/types/tools/CodeExecutor.d.ts +4 -0
  66. package/dist/types/tools/ProgrammaticToolCalling.d.ts +1 -1
  67. package/dist/types/tools/ToolNode.d.ts +3 -1
  68. package/dist/types/tools/runStepResume.d.ts +5 -0
  69. package/dist/types/tools/subagent/SubagentExecutor.d.ts +25 -0
  70. package/dist/types/tools/subagent/SubagentReplay.d.ts +2 -1
  71. package/dist/types/types/graph.d.ts +10 -1
  72. package/dist/types/types/stream.d.ts +18 -0
  73. package/dist/types/types/tools.d.ts +36 -16
  74. package/dist/types/utils/toolSessions.d.ts +18 -0
  75. package/package.json +1 -1
  76. package/src/agents/AgentContext.ts +7 -0
  77. package/src/graphs/Graph.ts +192 -13
  78. package/src/graphs/MultiAgentGraph.ts +1 -0
  79. package/src/run.ts +62 -11
  80. package/src/stream.ts +28 -9
  81. package/src/tools/BashExecutor.ts +31 -13
  82. package/src/tools/BashProgrammaticToolCalling.ts +19 -9
  83. package/src/tools/CodeExecutor.ts +57 -16
  84. package/src/tools/ProgrammaticToolCalling.ts +26 -12
  85. package/src/tools/ToolNode.ts +58 -8
  86. package/src/tools/ToolSearch.ts +2 -2
  87. package/src/tools/runStepResume.ts +121 -0
  88. package/src/tools/subagent/SubagentExecutor.ts +67 -65
  89. package/src/tools/subagent/SubagentReplay.ts +22 -1
  90. package/src/types/graph.ts +10 -0
  91. package/src/types/stream.ts +17 -0
  92. package/src/types/tools.ts +50 -21
  93. package/src/utils/toolSessions.ts +113 -0
@@ -62,6 +62,7 @@ import {
62
62
  SUBAGENT_PARENT_BATCH_CONFIG_KEY,
63
63
  SUBAGENT_REPLAY_CONTROLLER,
64
64
  } from '@/tools/subagent/SubagentReplay';
65
+ import { attachRunStepResumeState } from '@/tools/runStepResume';
65
66
  import {
66
67
  INTENT_ARG,
67
68
  readOutcomeFields,
@@ -595,17 +596,18 @@ function fileIdentityKey(file: {
595
596
 
596
597
  function updateCodeSession(
597
598
  sessions: t.ToolSessionMap,
599
+ sessionKey: string,
598
600
  execSessionId: string,
599
601
  files: t.FileRefs | undefined
600
602
  ): void {
601
603
  const newFiles = files ?? [];
602
- const existingSession = sessions.get(Constants.EXECUTE_CODE) as
604
+ const existingSession = sessions.get(sessionKey) as
603
605
  | t.CodeSessionContext
604
606
  | undefined;
605
607
  const existingFiles = existingSession?.files ?? [];
606
608
 
607
609
  if (newFiles.length === 0) {
608
- sessions.set(Constants.EXECUTE_CODE, {
610
+ sessions.set(sessionKey, {
609
611
  session_id: execSessionId,
610
612
  files: existingFiles,
611
613
  lastUpdated: Date.now(),
@@ -643,7 +645,7 @@ function updateCodeSession(
643
645
  }
644
646
  }
645
647
 
646
- sessions.set(Constants.EXECUTE_CODE, {
648
+ sessions.set(sessionKey, {
647
649
  session_id: execSessionId,
648
650
  files: [...filteredExisting, ...filesWithSession],
649
651
  lastUpdated: Date.now(),
@@ -694,6 +696,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
694
696
  private programmaticCache?: t.ProgrammaticCache;
695
697
  /** Reference to Graph's sessions map for automatic session injection */
696
698
  private sessions?: t.ToolSessionMap;
699
+ /** Partition within `sessions` owned by this agent's execution profile. */
700
+ private codeSessionKey: string;
697
701
  /** When true, dispatches ON_TOOL_EXECUTE events instead of invoking tools directly */
698
702
  private eventDrivenMode: boolean = false;
699
703
  /** Opt-in stream-layer prestart config for event-driven tools. */
@@ -794,6 +798,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
794
798
  loadRuntimeTools,
795
799
  toolRegistry,
796
800
  sessions,
801
+ codeSessionKey,
797
802
  eventDrivenMode,
798
803
  eagerEventToolExecution,
799
804
  eagerEventToolExecutions,
@@ -814,11 +819,45 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
814
819
  fileCheckpointer,
815
820
  getBreakerSignal,
816
821
  getRunScope,
822
+ restoreRunStepResumeState,
823
+ createRunStepResumeState,
817
824
  }: t.ToolNodeConstructorParams) {
818
825
  super({
819
826
  name: name ?? TOOL_NODE_RUN_NAME,
820
827
  tags,
821
- func: (input, config) => this.run(input, config),
828
+ func: async (input, config) => {
829
+ const state = input as T & Pick<t.BaseGraphState, 'runStepState'>;
830
+ restoreRunStepResumeState?.(state.runStepState, config);
831
+ let result: T;
832
+ try {
833
+ result = await this.run(input, config);
834
+ } catch (error) {
835
+ if (!isGraphInterrupt(error) || createRunStepResumeState == null) {
836
+ throw error;
837
+ }
838
+ const resumeState = createRunStepResumeState();
839
+ throw new GraphInterrupt(
840
+ error.interrupts.map((pendingInterrupt) => ({
841
+ ...pendingInterrupt,
842
+ value: attachRunStepResumeState(
843
+ pendingInterrupt.value,
844
+ resumeState
845
+ ),
846
+ }))
847
+ );
848
+ }
849
+ if (createRunStepResumeState == null) {
850
+ return result;
851
+ }
852
+ const runStepState = createRunStepResumeState();
853
+ if (Array.isArray(result)) {
854
+ return [...result, { runStepState }] as T;
855
+ }
856
+ return {
857
+ ...result,
858
+ runStepState,
859
+ };
860
+ },
822
861
  });
823
862
  this.trace = trace ?? this.trace;
824
863
  this.runLangfuse = runLangfuse;
@@ -834,6 +873,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
834
873
  toolExecution,
835
874
  });
836
875
  this.sessions = sessions;
876
+ this.codeSessionKey = codeSessionKey ?? Constants.EXECUTE_CODE;
837
877
  this.eventDrivenMode = eventDrivenMode ?? false;
838
878
  this.eagerEventToolExecution = eagerEventToolExecution;
839
879
  this.eagerEventToolExecutions = eagerEventToolExecutions;
@@ -1328,7 +1368,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1328
1368
  * `/files/<session_id>` fallback was removed from the executors.
1329
1369
  */
1330
1370
  if (this.participatesInCodeSession(call.name)) {
1331
- const codeSession = this.sessions?.get(Constants.EXECUTE_CODE) as
1371
+ const codeSession = this.sessions?.get(this.codeSessionKey) as
1332
1372
  | t.CodeSessionContext
1333
1373
  | undefined;
1334
1374
  const execSessionId = codeSession?.session_id;
@@ -2356,7 +2396,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2356
2396
  return undefined;
2357
2397
  }
2358
2398
 
2359
- const codeSession = this.sessions.get(Constants.EXECUTE_CODE) as
2399
+ const codeSession = this.sessions.get(this.codeSessionKey) as
2360
2400
  | t.CodeSessionContext
2361
2401
  | undefined;
2362
2402
  if (!codeSession) {
@@ -2439,7 +2479,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2439
2479
  continue;
2440
2480
  }
2441
2481
 
2442
- updateCodeSession(this.sessions, execSessionId, artifact?.files);
2482
+ updateCodeSession(
2483
+ this.sessions,
2484
+ this.codeSessionKey,
2485
+ execSessionId,
2486
+ artifact?.files
2487
+ );
2443
2488
  }
2444
2489
  }
2445
2490
 
@@ -2502,7 +2547,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2502
2547
  | undefined;
2503
2548
  const execSessionId = artifact?.session_id;
2504
2549
  if (execSessionId != null && execSessionId !== '') {
2505
- updateCodeSession(this.sessions, execSessionId, artifact?.files);
2550
+ updateCodeSession(
2551
+ this.sessions,
2552
+ this.codeSessionKey,
2553
+ execSessionId,
2554
+ artifact?.files
2555
+ );
2506
2556
  }
2507
2557
  }
2508
2558
 
@@ -25,7 +25,7 @@ import { resolveFetchProxyAgent } from '@/utils/proxy';
25
25
  import { tool, DynamicStructuredTool } from '@langchain/core/tools';
26
26
  import type * as t from '@/types';
27
27
  import { INTENT_PROPERTY } from '@/tools/intentArg';
28
- import { getCodeBaseURL } from './CodeExecutor';
28
+ import { buildCodeApiEndpoint, getCodeBaseURL } from './CodeExecutor';
29
29
  import { Constants } from '@/common';
30
30
 
31
31
  config();
@@ -914,7 +914,7 @@ function createToolSearch(
914
914
  const schema = createToolSearchSchema(mode);
915
915
 
916
916
  const baseEndpoint = initParams.baseUrl ?? getCodeBaseURL();
917
- const EXEC_ENDPOINT = `${baseEndpoint}/exec`;
917
+ const EXEC_ENDPOINT = buildCodeApiEndpoint(baseEndpoint, 'exec');
918
918
 
919
919
  const deferredToolsListing = getDeferredToolsListing(
920
920
  initParams.toolRegistry,
@@ -0,0 +1,121 @@
1
+ import type { RunStepResumeEntry, RunStepResumeState } from '@/types';
2
+
3
+ const RUN_STEP_RESUME_STATE_KEY = '__librechat_run_step_resume_state';
4
+ const RUN_STEP_RESUME_PAYLOAD_KEY = '__librechat_run_step_resume_payload';
5
+ const RUN_STEP_RESUME_WRAPPER_KEY = '__librechat_run_step_resume_wrapper';
6
+
7
+ type RunStepResumePayload = {
8
+ [RUN_STEP_RESUME_STATE_KEY]: RunStepResumeState;
9
+ [RUN_STEP_RESUME_PAYLOAD_KEY]: unknown;
10
+ [RUN_STEP_RESUME_WRAPPER_KEY]: 1;
11
+ };
12
+
13
+ export function isRunStepResumeState(
14
+ value: unknown
15
+ ): value is RunStepResumeState {
16
+ if (value == null || typeof value !== 'object') {
17
+ return false;
18
+ }
19
+ const state = value as Partial<RunStepResumeState>;
20
+ if (
21
+ state.version !== 1 ||
22
+ !Number.isSafeInteger(state.revision) ||
23
+ (state.revision ?? -1) < 0 ||
24
+ !Number.isSafeInteger(state.nextIndex) ||
25
+ (state.nextIndex ?? -1) < 0 ||
26
+ !Array.isArray(state.toolCallSteps) ||
27
+ !Array.isArray(state.steps)
28
+ ) {
29
+ return false;
30
+ }
31
+ const toolCallSteps = new Map<string, string>();
32
+ for (const reference of state.toolCallSteps as unknown[]) {
33
+ if (reference == null || typeof reference !== 'object') {
34
+ return false;
35
+ }
36
+ const { toolCallId, stepId } = reference as {
37
+ toolCallId?: unknown;
38
+ stepId?: unknown;
39
+ };
40
+ if (
41
+ typeof toolCallId !== 'string' ||
42
+ toolCallId === '' ||
43
+ typeof stepId !== 'string' ||
44
+ stepId === '' ||
45
+ toolCallSteps.has(toolCallId)
46
+ ) {
47
+ return false;
48
+ }
49
+ toolCallSteps.set(toolCallId, stepId);
50
+ }
51
+ const stepIds = new Set<string>();
52
+ for (const value of state.steps as unknown[]) {
53
+ if (value == null || typeof value !== 'object') {
54
+ return false;
55
+ }
56
+ const entry = value as Partial<RunStepResumeEntry>;
57
+ const step = entry.step;
58
+ if (
59
+ step == null ||
60
+ typeof step.id !== 'string' ||
61
+ step.id === '' ||
62
+ !Number.isSafeInteger(step.index) ||
63
+ step.index < 0 ||
64
+ !Array.isArray(entry.pendingToolCallIds) ||
65
+ entry.pendingToolCallIds.some(
66
+ (toolCallId) => typeof toolCallId !== 'string' || toolCallId === ''
67
+ ) ||
68
+ (entry.latestCompletionAt != null &&
69
+ !Number.isFinite(entry.latestCompletionAt)) ||
70
+ typeof entry.openMessageStep !== 'boolean' ||
71
+ entry.pendingToolCallIds.some(
72
+ (toolCallId) => toolCallSteps.get(toolCallId) !== step.id
73
+ ) ||
74
+ stepIds.has(step.id)
75
+ ) {
76
+ return false;
77
+ }
78
+ stepIds.add(step.id);
79
+ }
80
+ return true;
81
+ }
82
+
83
+ function isRunStepResumePayload(value: unknown): value is RunStepResumePayload {
84
+ if (value == null || typeof value !== 'object') {
85
+ return false;
86
+ }
87
+ const payload = value as Partial<RunStepResumePayload>;
88
+ return (
89
+ payload[RUN_STEP_RESUME_WRAPPER_KEY] === 1 &&
90
+ Object.prototype.hasOwnProperty.call(value, RUN_STEP_RESUME_PAYLOAD_KEY) &&
91
+ isRunStepResumeState(payload[RUN_STEP_RESUME_STATE_KEY])
92
+ );
93
+ }
94
+
95
+ export function attachRunStepResumeState(
96
+ payload: unknown,
97
+ state: RunStepResumeState
98
+ ): object {
99
+ const publicPayload = isRunStepResumePayload(payload)
100
+ ? payload[RUN_STEP_RESUME_PAYLOAD_KEY]
101
+ : payload;
102
+ return {
103
+ [RUN_STEP_RESUME_WRAPPER_KEY]: 1,
104
+ [RUN_STEP_RESUME_PAYLOAD_KEY]: publicPayload,
105
+ [RUN_STEP_RESUME_STATE_KEY]: state,
106
+ } satisfies RunStepResumePayload;
107
+ }
108
+
109
+ export function getRunStepResumeState(
110
+ payload: unknown
111
+ ): RunStepResumeState | undefined {
112
+ return isRunStepResumePayload(payload)
113
+ ? payload[RUN_STEP_RESUME_STATE_KEY]
114
+ : undefined;
115
+ }
116
+
117
+ export function stripRunStepResumeState(payload: unknown): unknown {
118
+ return isRunStepResumePayload(payload)
119
+ ? payload[RUN_STEP_RESUME_PAYLOAD_KEY]
120
+ : payload;
121
+ }
@@ -46,6 +46,7 @@ import type {
46
46
  RunStep,
47
47
  RunStepDeltaEvent,
48
48
  RunStepClosedEvent,
49
+ RunStepStatus,
49
50
  StandardGraphInput,
50
51
  ExecutableSubagentConfigEntry,
51
52
  ResolvedSubagentConfig,
@@ -61,7 +62,6 @@ import type {
61
62
  ToolApprovalInterruptPayload,
62
63
  ToolExecuteBatchRequest,
63
64
  ToolCallDelta,
64
- ToolSessionContext,
65
65
  TokenCounter,
66
66
  ToolApprovalDecision,
67
67
  ToolApprovalDecisionMap,
@@ -87,6 +87,7 @@ import type {
87
87
  import type { GraphFactory } from '@/graphs/graphFactory';
88
88
  import type { StandardGraph } from '@/graphs/Graph';
89
89
  import type { HandlerRegistry } from '@/events';
90
+ import { stripRunStepResumeState } from '@/tools/runStepResume';
90
91
  import {
91
92
  getSubagentApprovalExecutionScope,
92
93
  SubagentDefinitionBindingError,
@@ -124,6 +125,7 @@ import {
124
125
  createChildGraphPlan,
125
126
  isGraphSubagentConfig,
126
127
  } from './childGraphConfig';
128
+ import { seedAgentInitialSessions } from '@/utils/toolSessions';
127
129
  import { stableStringify } from '@/tools/eagerEventExecution';
128
130
  import { composeAbortSignals } from '@/utils/misc';
129
131
 
@@ -147,71 +149,11 @@ const SUBAGENT_CONFIG_CHANGED_MESSAGE =
147
149
  const SUBAGENT_INVOCATION_CHANGED_MESSAGE =
148
150
  'Subagent error: Subagent invocation changed for this execution.';
149
151
 
150
- function cloneToolSessionContext(
151
- context: ToolSessionContext
152
- ): ToolSessionContext {
153
- return {
154
- ...context,
155
- ...(context.files == null
156
- ? {}
157
- : {
158
- files: context.files.map((file) => ({
159
- ...file,
160
- storage_session_id: file.storage_session_id ?? context.session_id,
161
- })),
162
- }),
163
- };
164
- }
165
-
166
152
  function seedChildGraphSessions(
167
153
  childGraph: StandardGraph,
168
154
  agents: AgentInputs[]
169
155
  ): void {
170
- const seenFilesByTool = new Map<string, Set<string>>();
171
- for (const agent of agents) {
172
- if (agent.initialSessions == null) {
173
- continue;
174
- }
175
- for (const [toolName, context] of agent.initialSessions) {
176
- const existing = childGraph.sessions.get(toolName);
177
- if (existing == null) {
178
- const cloned = cloneToolSessionContext(context);
179
- childGraph.sessions.set(toolName, cloned);
180
- seenFilesByTool.set(
181
- toolName,
182
- new Set(
183
- cloned.files?.map(
184
- (file) => `${file.storage_session_id ?? ''}\0${file.id}`
185
- ) ?? []
186
- )
187
- );
188
- continue;
189
- }
190
- if (context.files == null || context.files.length === 0) {
191
- continue;
192
- }
193
- const seenFiles =
194
- seenFilesByTool.get(toolName) ??
195
- new Set(
196
- existing.files?.map(
197
- (file) =>
198
- `${file.storage_session_id ?? existing.session_id}\0${file.id}`
199
- ) ?? []
200
- );
201
- const files = existing.files == null ? [] : [...existing.files];
202
- for (const file of context.files) {
203
- const storageSessionId = file.storage_session_id ?? context.session_id;
204
- const key = `${storageSessionId}\0${file.id}`;
205
- if (seenFiles.has(key)) {
206
- continue;
207
- }
208
- seenFiles.add(key);
209
- files.push({ ...file, storage_session_id: storageSessionId });
210
- }
211
- seenFilesByTool.set(toolName, seenFiles);
212
- childGraph.sessions.set(toolName, { ...existing, files });
213
- }
214
- }
156
+ seedAgentInitialSessions(childGraph.sessions, agents);
215
157
  }
216
158
 
217
159
  async function dispatchObservationalSubagentUpdate(
@@ -713,11 +655,11 @@ function addSubagentScope(
713
655
  resumeManifest?: SubagentResumeManifest
714
656
  ): Interrupt[] {
715
657
  return interrupts.map((childInterrupt) => {
716
- let payload = childInterrupt.value;
658
+ let payload = stripRunStepResumeState(childInterrupt.value);
717
659
  if (isToolApprovalPayload(payload)) {
718
660
  payload = {
719
- ...childInterrupt.value,
720
- subagent: childInterrupt.value.subagent ?? scope,
661
+ ...payload,
662
+ subagent: payload.subagent ?? scope,
721
663
  };
722
664
  }
723
665
  return {
@@ -1610,6 +1552,42 @@ export class SubagentExecutor {
1610
1552
  graph.clearHeavyState();
1611
1553
  }
1612
1554
 
1555
+ /**
1556
+ * Terminal sweep for a child graph's run steps. `Run.processStream` sweeps
1557
+ * only the graph it owns, and a child executes through `workflow.invoke()`
1558
+ * outside that loop — so without this, a child's last message step (no
1559
+ * successor step ever opens to close it) and every step of an aborted child
1560
+ * stay `in_progress` forever, leaving hosts with unmatched starts.
1561
+ *
1562
+ * Must run BEFORE `forwarding.drain()`: closures reach the parent only as
1563
+ * child custom events through the forwarder, which stops relaying once
1564
+ * drained.
1565
+ *
1566
+ * Deliberately not called on the interrupt path, mirroring the parent's
1567
+ * `isAwaitingResume` guard: an in-process resume continues into these same
1568
+ * steps, so closing them would strand the resumed run. A resume that
1569
+ * crosses a process boundary is a known gap either way — the child rebuilds
1570
+ * from `SubagentGraphResumeState`, which carries tool-call identity but not
1571
+ * `contentData`, so pre-interrupt steps are unreachable from the new graph
1572
+ * and stay open. That is the child-level instance of the same cross-process
1573
+ * orphan the parent graph has, and it needs persisted lifecycle state to
1574
+ * fix rather than a sweep here.
1575
+ *
1576
+ * `at` is the moment execution actually ended, threaded through so
1577
+ * observational work between then and the sweep cannot inflate the stamps.
1578
+ */
1579
+ private async closeChildRunSteps(
1580
+ graph: StandardGraph,
1581
+ status: Exclude<RunStepStatus, 'in_progress'>,
1582
+ at?: number
1583
+ ): Promise<void> {
1584
+ try {
1585
+ await graph.closeUnfinishedRunSteps(status, at);
1586
+ } catch (_e) {
1587
+ /** A failed sweep must never mask the subagent's own outcome */
1588
+ }
1589
+ }
1590
+
1613
1591
  clearHeavyState(): void {
1614
1592
  this.executions.clear((record) => {
1615
1593
  if (record.activeRun != null) {
@@ -2470,6 +2448,8 @@ export class SubagentExecutor {
2470
2448
  result = childResult;
2471
2449
  }
2472
2450
  } catch (error) {
2451
+ /** Stamped at failure, not after the error-envelope work below. */
2452
+ const childTerminalAt = Date.now();
2473
2453
  if (isGraphInterrupt(error)) {
2474
2454
  const activeChildRun = execution.activeRun;
2475
2455
  if (activeChildRun != null) {
@@ -2505,6 +2485,16 @@ export class SubagentExecutor {
2505
2485
  childBreaker.abort(error);
2506
2486
  }
2507
2487
  const errorMessage = truncateErrorMessage(error);
2488
+ /**
2489
+ * `cancelled` vs `failed` mirrors `Run.resolveSweepStatus`: an aborted
2490
+ * child was stopped on purpose (caller abort, or a breaker trip from a
2491
+ * parallel sibling), anything else died of an unexpected error.
2492
+ */
2493
+ await this.closeChildRunSteps(
2494
+ childGraph,
2495
+ childSignal.aborted ? 'cancelled' : 'failed',
2496
+ childTerminalAt
2497
+ );
2508
2498
  if (forwarding) {
2509
2499
  await forwarding.drain();
2510
2500
  await this.emitSubagentUpdate(parentRegistry!, {
@@ -2537,6 +2527,14 @@ export class SubagentExecutor {
2537
2527
  };
2538
2528
  }
2539
2529
 
2530
+ /**
2531
+ * Child execution ended here. Captured before the observational work
2532
+ * below — an awaited `SubagentStop` hook, then the forwarder drain — so a
2533
+ * slow hook cannot inflate the closure stamps with its own latency.
2534
+ * Mirrors the parent's `terminalAt` capture in `Run.processStream`.
2535
+ */
2536
+ const childTerminalAt = Date.now();
2537
+
2540
2538
  if (result == null) {
2541
2539
  throw new Error('Subagent completed without producing graph state.');
2542
2540
  }
@@ -2573,6 +2571,10 @@ export class SubagentExecutor {
2573
2571
  });
2574
2572
  }
2575
2573
 
2574
+ if (!childAlreadyCompleted) {
2575
+ await this.closeChildRunSteps(childGraph, 'completed', childTerminalAt);
2576
+ }
2577
+
2576
2578
  if (forwarding && !childAlreadyCompleted) {
2577
2579
  await forwarding.drain();
2578
2580
  await this.emitSubagentUpdate(parentRegistry!, {
@@ -2,7 +2,13 @@ import type { ToolCall, ToolMessage } from '@langchain/core/messages/tool';
2
2
  import type { RunnableConfig } from '@langchain/core/runnables';
3
3
  import type { ToolOutputReferenceState } from '@/tools/toolOutputReferences';
4
4
  import type { ToolApprovalReplaySnapshot } from '@/hooks';
5
- import type { ToolSessionContext } from '@/types';
5
+ import type { RunStepResumeState, ToolSessionContext } from '@/types';
6
+ import {
7
+ attachRunStepResumeState,
8
+ getRunStepResumeState,
9
+ isRunStepResumeState,
10
+ stripRunStepResumeState,
11
+ } from '@/tools/runStepResume';
6
12
 
7
13
  export const SUBAGENT_RESUME_MANIFEST_CONFIG_KEY =
8
14
  '__librechat_subagent_resume_manifest';
@@ -57,6 +63,7 @@ export interface SubagentGraphResumeState {
57
63
  toolNodes: SubagentToolNodeResumeState[];
58
64
  eagerToolUsage: SubagentEagerToolUsageState[];
59
65
  eagerToolSuppressions: string[];
66
+ runStepState?: RunStepResumeState;
60
67
  toolOutputReferences?: ToolOutputReferenceState;
61
68
  }
62
69
 
@@ -291,6 +298,8 @@ function isGraphResumeState(value: unknown): value is SubagentGraphResumeState {
291
298
  !state.eagerToolUsage.every(isEagerToolUsageState) ||
292
299
  !Array.isArray(state.eagerToolSuppressions) ||
293
300
  !state.eagerToolSuppressions.every(isString) ||
301
+ (state.runStepState != null &&
302
+ !isRunStepResumeState(state.runStepState)) ||
294
303
  (state.toolOutputReferences != null &&
295
304
  !isToolOutputReferenceState(state.toolOutputReferences))
296
305
  ) {
@@ -412,6 +421,7 @@ function isSubagentResumeManifest(
412
421
  export function getSubagentResumeManifest(
413
422
  payload: unknown
414
423
  ): SubagentResumeManifest | undefined {
424
+ payload = stripRunStepResumeState(payload);
415
425
  if (payload == null || typeof payload !== 'object') {
416
426
  return undefined;
417
427
  }
@@ -489,6 +499,16 @@ export function attachSubagentResumeManifest(
489
499
  payload: unknown,
490
500
  manifest: SubagentResumeManifest
491
501
  ): object {
502
+ const runStepState = getRunStepResumeState(payload);
503
+ if (runStepState != null) {
504
+ return attachRunStepResumeState(
505
+ attachSubagentResumeManifest(
506
+ stripRunStepResumeState(payload),
507
+ manifest
508
+ ),
509
+ runStepState
510
+ );
511
+ }
492
512
  if (
493
513
  isWrappedSubagentResumePayload(payload) ||
494
514
  isInlineSubagentResumePayload(payload)
@@ -512,6 +532,7 @@ export function attachSubagentResumeManifest(
512
532
  }
513
533
 
514
534
  export function stripSubagentResumeManifest(payload: unknown): unknown {
535
+ payload = stripRunStepResumeState(payload);
515
536
  if (isWrappedSubagentResumePayload(payload)) {
516
537
  return payload[SUBAGENT_RESUME_WRAPPED_PAYLOAD_KEY];
517
538
  }
@@ -20,6 +20,7 @@ import type {
20
20
  import type {
21
21
  RunStep,
22
22
  RunStepDeltaEvent,
23
+ RunStepResumeState,
23
24
  RunStepClosedEvent,
24
25
  MessageDeltaEvent,
25
26
  ReasoningDeltaEvent,
@@ -76,6 +77,7 @@ export type SystemCallbacks = {
76
77
 
77
78
  export type BaseGraphState = {
78
79
  messages: BaseMessage[];
80
+ runStepState?: RunStepResumeState;
79
81
  };
80
82
 
81
83
  export type AgentSubgraphState = BaseGraphState & {
@@ -803,6 +805,14 @@ export interface LangfuseConfig {
803
805
 
804
806
  export interface AgentInputs {
805
807
  agentId: string;
808
+ /**
809
+ * Partition key for transient code-session ids and file refs. Agents with
810
+ * the same key share those refs; different execution profiles/scopes must
811
+ * use different keys. Defaults to the legacy shared `execute_code` slot.
812
+ * Non-default partitions also disable speculative eager execution of code
813
+ * tools because their factory may target a durable backend.
814
+ */
815
+ codeSessionKey?: string;
806
816
  /** Human-readable name for the agent (used in handoff context). Defaults to agentId if not provided. */
807
817
  name?: string;
808
818
  toolEnd?: boolean;
@@ -103,6 +103,23 @@ export type RunStep = {
103
103
  // };
104
104
  };
105
105
 
106
+ /** Minimal durable lifecycle state needed to continue open run steps. */
107
+ export interface RunStepResumeEntry {
108
+ step: RunStep;
109
+ pendingToolCallIds: string[];
110
+ latestCompletionAt?: number;
111
+ openMessageStep: boolean;
112
+ }
113
+
114
+ /** SDK-private state persisted in LangGraph checkpoints for process-safe resume. */
115
+ export interface RunStepResumeState {
116
+ version: 1;
117
+ revision: number;
118
+ nextIndex: number;
119
+ toolCallSteps: Array<{ toolCallId: string; stepId: string }>;
120
+ steps: RunStepResumeEntry[];
121
+ }
122
+
106
123
  /**
107
124
  * Represents a run step delta i.e. any changed fields on a run step during
108
125
  * streaming.