@librechat/agents 3.5.1 → 3.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) 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/prompts/activityLabel.cjs +1 -0
  9. package/dist/cjs/prompts/reasoningLabel.cjs +60 -0
  10. package/dist/cjs/prompts/reasoningLabel.cjs.map +1 -0
  11. package/dist/cjs/run.cjs +161 -11
  12. package/dist/cjs/run.cjs.map +1 -1
  13. package/dist/cjs/stream.cjs +8 -6
  14. package/dist/cjs/stream.cjs.map +1 -1
  15. package/dist/cjs/tools/BashExecutor.cjs +8 -7
  16. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  17. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +5 -4
  18. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  19. package/dist/cjs/tools/CodeExecutor.cjs +26 -7
  20. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  21. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +7 -6
  22. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  23. package/dist/cjs/tools/ToolNode.cjs +33 -10
  24. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  25. package/dist/cjs/tools/ToolSearch.cjs +1 -1
  26. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  27. package/dist/cjs/tools/runStepResume.cjs +51 -0
  28. package/dist/cjs/tools/runStepResume.cjs.map +1 -0
  29. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +15 -43
  30. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  31. package/dist/cjs/tools/subagent/SubagentReplay.cjs +6 -1
  32. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
  33. package/dist/cjs/utils/toolSessions.cjs +63 -0
  34. package/dist/cjs/utils/toolSessions.cjs.map +1 -0
  35. package/dist/esm/agents/AgentContext.mjs +5 -2
  36. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  37. package/dist/esm/graphs/Graph.mjs +148 -22
  38. package/dist/esm/graphs/Graph.mjs.map +1 -1
  39. package/dist/esm/graphs/MultiAgentGraph.mjs +2 -1
  40. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  41. package/dist/esm/main.mjs +2 -2
  42. package/dist/esm/prompts/activityLabel.mjs +1 -1
  43. package/dist/esm/prompts/reasoningLabel.mjs +57 -0
  44. package/dist/esm/prompts/reasoningLabel.mjs.map +1 -0
  45. package/dist/esm/run.mjs +161 -11
  46. package/dist/esm/run.mjs.map +1 -1
  47. package/dist/esm/stream.mjs +8 -6
  48. package/dist/esm/stream.mjs.map +1 -1
  49. package/dist/esm/tools/BashExecutor.mjs +9 -8
  50. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  51. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +6 -5
  52. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  53. package/dist/esm/tools/CodeExecutor.mjs +23 -8
  54. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  55. package/dist/esm/tools/ProgrammaticToolCalling.mjs +8 -7
  56. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  57. package/dist/esm/tools/ToolNode.mjs +33 -10
  58. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  59. package/dist/esm/tools/ToolSearch.mjs +2 -2
  60. package/dist/esm/tools/ToolSearch.mjs.map +1 -1
  61. package/dist/esm/tools/runStepResume.mjs +48 -0
  62. package/dist/esm/tools/runStepResume.mjs.map +1 -0
  63. package/dist/esm/tools/subagent/SubagentExecutor.mjs +15 -43
  64. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  65. package/dist/esm/tools/subagent/SubagentReplay.mjs +6 -1
  66. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
  67. package/dist/esm/utils/toolSessions.mjs +62 -0
  68. package/dist/esm/utils/toolSessions.mjs.map +1 -0
  69. package/dist/types/agents/AgentContext.d.ts +4 -1
  70. package/dist/types/graphs/Graph.d.ts +11 -3
  71. package/dist/types/prompts/reasoningLabel.d.ts +17 -0
  72. package/dist/types/run.d.ts +8 -0
  73. package/dist/types/tools/CodeExecutor.d.ts +4 -0
  74. package/dist/types/tools/ProgrammaticToolCalling.d.ts +1 -1
  75. package/dist/types/tools/ToolNode.d.ts +3 -1
  76. package/dist/types/tools/runStepResume.d.ts +5 -0
  77. package/dist/types/tools/subagent/SubagentExecutor.d.ts +25 -0
  78. package/dist/types/tools/subagent/SubagentReplay.d.ts +2 -1
  79. package/dist/types/types/graph.d.ts +10 -1
  80. package/dist/types/types/index.d.ts +1 -0
  81. package/dist/types/types/reasoningLabel.d.ts +56 -0
  82. package/dist/types/types/stream.d.ts +18 -0
  83. package/dist/types/types/tools.d.ts +36 -16
  84. package/dist/types/utils/toolSessions.d.ts +18 -0
  85. package/package.json +1 -1
  86. package/src/agents/AgentContext.ts +7 -0
  87. package/src/graphs/Graph.ts +192 -13
  88. package/src/graphs/MultiAgentGraph.ts +1 -0
  89. package/src/prompts/reasoningLabel.ts +118 -0
  90. package/src/run.ts +330 -12
  91. package/src/stream.ts +28 -9
  92. package/src/tools/BashExecutor.ts +31 -13
  93. package/src/tools/BashProgrammaticToolCalling.ts +19 -9
  94. package/src/tools/CodeExecutor.ts +57 -16
  95. package/src/tools/ProgrammaticToolCalling.ts +26 -12
  96. package/src/tools/ToolNode.ts +58 -8
  97. package/src/tools/ToolSearch.ts +2 -2
  98. package/src/tools/runStepResume.ts +121 -0
  99. package/src/tools/subagent/SubagentExecutor.ts +67 -65
  100. package/src/tools/subagent/SubagentReplay.ts +22 -1
  101. package/src/types/graph.ts +10 -0
  102. package/src/types/index.ts +1 -0
  103. package/src/types/reasoningLabel.ts +59 -0
  104. package/src/types/stream.ts +17 -0
  105. package/src/types/tools.ts +50 -21
  106. package/src/utils/toolSessions.ts +113 -0
@@ -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;
@@ -10,3 +10,4 @@ export * from './tools';
10
10
  export * from './summarize';
11
11
  export * from './activityLabel';
12
12
  export * from './assistantPhase';
13
+ export * from './reasoningLabel';
@@ -0,0 +1,59 @@
1
+ import type { RunnableConfig } from '@langchain/core/runnables';
2
+ import type { UsageMetadata } from '@langchain/core/messages';
3
+ import type { ClientOptions } from '@/types/llm';
4
+ import type { Providers } from '@/common';
5
+
6
+ /** Lifecycle state of the visible reasoning snapshot being labeled. */
7
+ export type ReasoningLabelStatus = 'streaming' | 'complete';
8
+
9
+ /** Result of one reasoning-label revision. */
10
+ export type ReasoningLabelResult = {
11
+ label?: string;
12
+ /** Provider-reported usage for host-side billing after a durable commit. */
13
+ usage?: UsageMetadata;
14
+ };
15
+
16
+ /** Options for `Run.generateReasoningLabel`. */
17
+ export type RunReasoningLabelOptions = {
18
+ provider: Providers;
19
+ clientOptions?: ClientOptions;
20
+ /**
21
+ * Complete user-visible reasoning accumulated for this step so far. Hidden
22
+ * chain-of-thought must never be supplied through this API.
23
+ */
24
+ visibleReasoning: string;
25
+ /** Stable run-step identity shared by every revision of this label. */
26
+ reasoningStepId: string;
27
+ /** Monotonically increasing host revision for this reasoning step. */
28
+ revision: number;
29
+ /** Whether the snapshot can still grow. Default `streaming`. */
30
+ status?: ReasoningLabelStatus;
31
+ /**
32
+ * Last durably visible label for this step. The model repeats it exactly
33
+ * when the reasoning direction has not materially changed, allowing hosts
34
+ * to avoid redundant UI updates.
35
+ */
36
+ previousLabel?: string;
37
+ /**
38
+ * Agent that emitted the reasoning. Selects its Langfuse overlay and
39
+ * redaction policy. Unknown agents and omitted multi-agent ownership fail
40
+ * closed before tracing or generation.
41
+ */
42
+ agentId?: string;
43
+ /** Override for the default reasoning-label system prompt. */
44
+ prompt?: string;
45
+ /** Maximum reasoning characters retained in the prompt. Default 6000. */
46
+ charLimit?: number;
47
+ /** LangChain runnable config carrier (signal, callbacks, thread/user ids). */
48
+ chainOptions?: Partial<RunnableConfig> & {
49
+ configurable?: Record<string, unknown>;
50
+ };
51
+ /** Deterministic seed for this reasoning-label revision trace. */
52
+ traceSeed?: string;
53
+ /** Stable source run identifier recorded on the observation. */
54
+ sourceRunId?: string;
55
+ /** Source Langfuse trace id for linking this detached label trace. */
56
+ sourceTraceId?: string;
57
+ /** Host response/message identifier recorded on the observation. */
58
+ responseId?: string;
59
+ };
@@ -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.
@@ -1,10 +1,17 @@
1
1
  // src/types/tools.ts
2
2
  import type { StructuredToolInterface } from '@langchain/core/tools';
3
- import type { RunnableToolLike } from '@langchain/core/runnables';
3
+ import type {
4
+ RunnableConfig,
5
+ RunnableToolLike,
6
+ } from '@langchain/core/runnables';
4
7
  import type { ToolCall } from '@langchain/core/messages/tool';
5
8
  import type { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
6
9
  import type { RunBreakerScope } from '@/llm/streamLimits';
7
- import type { MessageContentComplex, ToolErrorData } from './stream';
10
+ import type {
11
+ MessageContentComplex,
12
+ RunStepResumeState,
13
+ ToolErrorData,
14
+ } from './stream';
8
15
  import type { HumanInTheLoopConfig } from './hitl';
9
16
  import type { LangfuseConfig } from './graph';
10
17
  import type { HookRegistry } from '@/hooks';
@@ -128,6 +135,8 @@ export type ToolNodeOptions = {
128
135
  toolRegistry?: LCToolRegistry;
129
136
  /** Reference to Graph's sessions map for automatic session injection */
130
137
  sessions?: ToolSessionMap;
138
+ /** Partition within `sessions` used by this agent's code tools. */
139
+ codeSessionKey?: string;
131
140
  /** When true, dispatches ON_TOOL_EXECUTE events instead of invoking tools directly */
132
141
  eventDrivenMode?: boolean;
133
142
  /** Tool definitions for event-driven mode (used for context, not invocation) */
@@ -274,6 +283,13 @@ export type ToolNodeOptions = {
274
283
  * controller.
275
284
  */
276
285
  getRunScope?: () => RunBreakerScope;
286
+ /** SDK-owned checkpoint bridge for open run-step lifecycle state. */
287
+ restoreRunStepResumeState?: (
288
+ state?: RunStepResumeState,
289
+ config?: RunnableConfig
290
+ ) => void;
291
+ /** SDK-owned checkpoint snapshot for open run-step lifecycle state. */
292
+ createRunStepResumeState?: () => RunStepResumeState;
277
293
  };
278
294
 
279
295
  export type ToolNodeConstructorParams = ToolRefs & ToolNodeOptions;
@@ -375,6 +391,19 @@ export type CodeEnvFile =
375
391
  export type CodeExecutionToolParams =
376
392
  | undefined
377
393
  | {
394
+ /** Trusted Code API endpoint selected by the host for this agent. */
395
+ baseUrl?: string;
396
+ /**
397
+ * Expected Code API execution profile. Sent as a request assertion so
398
+ * endpoint/configuration drift fails closed instead of silently using
399
+ * the wrong sandbox backend.
400
+ */
401
+ executionProfile?: CodeApiExecutionProfile;
402
+ /**
403
+ * Trusted warm-runtime affinity hint selected by the host for this
404
+ * agent invocation. This overrides the legacy ToolNode-injected hint.
405
+ */
406
+ runtimeSessionHint?: string;
378
407
  /** Execution session — see `CodeSessionContext.session_id`. */
379
408
  session_id?: string;
380
409
  user_id?: string;
@@ -382,16 +411,16 @@ export type CodeExecutionToolParams =
382
411
  /** Optional host-supplied Code API auth headers. */
383
412
  authHeaders?: CodeApiAuthHeaders;
384
413
  /**
385
- * Advertise best-effort stateful sessions in the tool description
386
- * (variables/files may persist between calls, may reset). Prompt text
387
- * only, and it must be set here because the description is bound to the
388
- * LLM at construction time. Pair it with the run-scoped
389
- * `toolExecution.sandbox.statefulSessions` gate, which drives the wire
390
- * hint — set both from one flag so the prompt and the backend agree.
414
+ * Advertise filesystem-tier stateful sessions in the tool description
415
+ * and allow a trusted runtime affinity hint onto the wire. Resolve this,
416
+ * `baseUrl`, `executionProfile`, and `runtimeSessionHint` together from
417
+ * the executing agent so the prompt and backend agree.
391
418
  */
392
419
  statefulSessions?: boolean;
393
420
  };
394
421
 
422
+ export type CodeApiExecutionProfile = 'default' | 'stateful';
423
+
395
424
  export type CodeApiAuthHeaderMap = Record<string, string>;
396
425
 
397
426
  export type CodeApiAuthHeaders =
@@ -1106,19 +1135,17 @@ export type CloudflareSandboxExecutionConfig = {
1106
1135
 
1107
1136
  export type SandboxExecutionConfig = {
1108
1137
  /**
1109
- * Opt into best-effort stateful runtime sessions on the remote Code API
1110
- * (its warm per-session MicroVM backend). This gate is run-scoped: it only
1111
- * controls the wire behavior (ToolNode injecting the session hint on
1112
- * execute_code/bash calls). The transport is otherwise unchanged.
1138
+ * Legacy run-scoped opt-in for warm runtime hint injection. New hosts that
1139
+ * need mixed execution profiles should resolve `statefulSessions`,
1140
+ * `baseUrl`, `executionProfile`, and `runtimeSessionHint` on each executing
1141
+ * agent's tool factories instead. Direct executors ignore this injected hint
1142
+ * unless their own factory has `statefulSessions: true`.
1113
1143
  *
1114
1144
  * It does NOT change the model-facing tool description. Tool descriptions are
1115
1145
  * bound to the LLM at construction time (`createCodeExecutionTool` /
1116
1146
  * `createBashExecutionTool`), before this run config is applied inside the
1117
1147
  * graph, so they can only be adjusted via the tools' own `statefulSessions`
1118
- * factory param. Set BOTH from one flag (as LibreChat does): with this on but
1119
- * the factory param off, the backend runs statefully while the model is still
1120
- * told the environment is stateless (non-corrupting — the model just won't
1121
- * exploit persistence).
1148
+ * factory param.
1122
1149
  */
1123
1150
  statefulSessions?: boolean;
1124
1151
  /**
@@ -1294,6 +1321,10 @@ export type BashProgrammaticToolCallingParams = ProgrammaticToolCallingParams;
1294
1321
  export type ProgrammaticToolCallingParams = {
1295
1322
  /** Code API base URL (or use CODE_BASEURL env var) */
1296
1323
  baseUrl?: string;
1324
+ /** Expected Code API execution profile (host-controlled). */
1325
+ executionProfile?: CodeApiExecutionProfile;
1326
+ /** Trusted warm-runtime affinity hint for this agent invocation. */
1327
+ runtimeSessionHint?: string;
1297
1328
  /** Safety limit for round-trips (default: 20) */
1298
1329
  maxRoundTrips?: number;
1299
1330
  /** Maximum per-sandbox-run timeout for PTC's legacy `timeout` field. */
@@ -1304,11 +1335,9 @@ export type ProgrammaticToolCallingParams = {
1304
1335
  debug?: boolean;
1305
1336
  /** Optional host-supplied Code API auth headers. */
1306
1337
  authHeaders?: CodeApiAuthHeaders;
1307
- /* No `statefulSessions` here: PTC is stateless in v1. The initial
1308
- * /exec/programmatic request still forwards a ToolNode-injected
1309
- * `_runtime_session_hint` when present, but there is no factory-level opt-in
1310
- * to advertise (it would be a no-op). Re-add with real behavior when PTC
1311
- * stateful prompting lands. */
1338
+ /* No `statefulSessions` here: PTC keeps a stateless runtime prompt in v1.
1339
+ * An explicit stateful execution profile may still use a warm filesystem;
1340
+ * runtime/in-memory state never carries across calls. */
1312
1341
  };
1313
1342
 
1314
1343
  // ============================================================================
@@ -0,0 +1,113 @@
1
+ import type * as t from '@/types';
2
+ import { Constants } from '@/common';
3
+
4
+ type CodeSessionAgent = Pick<t.AgentInputs, 'codeSessionKey' | 'initialSessions'>;
5
+
6
+ function cloneToolSessionContext(
7
+ context: t.ToolSessionContext
8
+ ): t.ToolSessionContext {
9
+ return {
10
+ ...context,
11
+ ...(context.files == null
12
+ ? {}
13
+ : {
14
+ files: context.files.map((file) => ({
15
+ ...file,
16
+ storage_session_id:
17
+ file.storage_session_id ?? context.session_id,
18
+ })),
19
+ }),
20
+ };
21
+ }
22
+
23
+ function mergeToolSessionContext(
24
+ sessions: t.ToolSessionMap,
25
+ key: string,
26
+ context: t.ToolSessionContext
27
+ ): void {
28
+ const existing = sessions.get(key);
29
+ if (existing == null) {
30
+ sessions.set(key, cloneToolSessionContext(context));
31
+ return;
32
+ }
33
+ if (context.files == null || context.files.length === 0) {
34
+ return;
35
+ }
36
+
37
+ const seenFiles = new Set(
38
+ existing.files?.map(
39
+ (file) =>
40
+ `${file.storage_session_id ?? existing.session_id}\0${file.id}`
41
+ ) ?? []
42
+ );
43
+ const files = existing.files == null ? [] : [...existing.files];
44
+ for (const file of context.files) {
45
+ const storageSessionId = file.storage_session_id ?? context.session_id;
46
+ const fileKey = `${storageSessionId}\0${file.id}`;
47
+ if (seenFiles.has(fileKey)) {
48
+ continue;
49
+ }
50
+ seenFiles.add(fileKey);
51
+ files.push({ ...file, storage_session_id: storageSessionId });
52
+ }
53
+ sessions.set(key, { ...existing, files });
54
+ }
55
+
56
+ /**
57
+ * Seeds a top-level graph while preserving the legacy run-wide session map.
58
+ * A legacy `execute_code` seed is copied into every custom agent partition
59
+ * unless the host already supplied an explicit seed for that partition.
60
+ */
61
+ export function seedRunInitialSessions(args: {
62
+ sessions: t.ToolSessionMap;
63
+ initialSessions: t.ToolSessionMap;
64
+ agents: Iterable<Pick<t.AgentInputs, 'codeSessionKey'>>;
65
+ }): void {
66
+ const { sessions, initialSessions, agents } = args;
67
+ for (const [key, context] of initialSessions) {
68
+ mergeToolSessionContext(sessions, key, context);
69
+ }
70
+
71
+ const legacyCodeSession = initialSessions.get(Constants.EXECUTE_CODE);
72
+ if (legacyCodeSession == null) {
73
+ return;
74
+ }
75
+ for (const agent of agents) {
76
+ const key = agent.codeSessionKey ?? Constants.EXECUTE_CODE;
77
+ if (
78
+ key === Constants.EXECUTE_CODE ||
79
+ initialSessions.has(key) ||
80
+ sessions.has(key)
81
+ ) {
82
+ continue;
83
+ }
84
+ sessions.set(key, cloneToolSessionContext(legacyCodeSession));
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Seeds an isolated child graph from each selected agent. Legacy code-session
90
+ * entries are remapped to the owning agent's partition before they are merged.
91
+ */
92
+ export function seedAgentInitialSessions(
93
+ sessions: t.ToolSessionMap,
94
+ agents: Iterable<CodeSessionAgent>
95
+ ): void {
96
+ for (const agent of agents) {
97
+ if (agent.initialSessions == null) {
98
+ continue;
99
+ }
100
+ const codeSessionKey = agent.codeSessionKey ?? Constants.EXECUTE_CODE;
101
+ const hasExplicitCodeSession =
102
+ codeSessionKey !== Constants.EXECUTE_CODE &&
103
+ agent.initialSessions.has(codeSessionKey);
104
+ for (const [toolName, context] of agent.initialSessions) {
105
+ if (toolName === Constants.EXECUTE_CODE && hasExplicitCodeSession) {
106
+ continue;
107
+ }
108
+ const key =
109
+ toolName === Constants.EXECUTE_CODE ? codeSessionKey : toolName;
110
+ mergeToolSessionContext(sessions, key, context);
111
+ }
112
+ }
113
+ }