@librechat/agents 3.2.56 → 3.2.57

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.
@@ -81,6 +81,7 @@ export declare abstract class Graph<T extends t.BaseGraphState = t.BaseGraphStat
81
81
  * consumes the settled promises while preserving final ToolMessage order.
82
82
  */
83
83
  eagerEventToolExecution: t.EagerEventToolExecutionConfig | undefined;
84
+ codeSessionToolNames: string[] | undefined;
84
85
  eagerEventToolExecutions: Map<string, t.EagerEventToolExecution>;
85
86
  eagerEventToolUsageCount: Map<string, number>;
86
87
  private eagerEventToolUsageCountsByAgentId;
@@ -13,6 +13,7 @@ export declare class Run<_T extends t.BaseGraphState> {
13
13
  private langfuse?;
14
14
  private toolOutputReferences?;
15
15
  private eagerEventToolExecution?;
16
+ private codeSessionToolNames?;
16
17
  private toolExecution?;
17
18
  private subagentUsageSink?;
18
19
  private indexTokenCountMap?;
@@ -91,6 +91,8 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
91
91
  private eventDrivenMode;
92
92
  /** Opt-in stream-layer prestart config for event-driven tools. */
93
93
  private eagerEventToolExecution?;
94
+ /** Host tools that write to the code sandbox and share its exec session. */
95
+ private codeSessionToolNames?;
94
96
  /** Shared per-run prestarted tool registry populated by ChatModelStreamHandler. */
95
97
  private eagerEventToolExecutions?;
96
98
  /** Shared per-run per-tool turn counter used by eager and normal event dispatch. */
@@ -145,7 +147,7 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
145
147
  * other's in-flight state.
146
148
  */
147
149
  private anonBatchCounter;
148
- constructor({ tools, toolMap, name, tags, trace, runLangfuse, agentLangfuse, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, eagerEventToolExecution, eagerEventToolExecutions, eagerEventToolUsageCount, agentId, executingAgentId, directToolNames, maxContextTokens, maxToolResultChars, hookRegistry, humanInTheLoop, toolOutputReferences, toolOutputRegistry, toolExecution, fileCheckpointer, }: t.ToolNodeConstructorParams);
150
+ constructor({ tools, toolMap, name, tags, trace, runLangfuse, agentLangfuse, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, eagerEventToolExecution, eagerEventToolExecutions, eagerEventToolUsageCount, agentId, executingAgentId, directToolNames, codeSessionToolNames, maxContextTokens, maxToolResultChars, hookRegistry, humanInTheLoop, toolOutputReferences, toolOutputRegistry, toolExecution, fileCheckpointer, }: t.ToolNodeConstructorParams);
149
151
  invoke(input: any, options?: Partial<RunnableConfig>): Promise<any>;
150
152
  /**
151
153
  * Returns the run-scoped tool output registry, or `undefined` when
@@ -304,6 +306,14 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
304
306
  * Extracts code execution session context from tool results and stores in Graph.sessions.
305
307
  * Mirrors the session storage logic in handleRunToolCompletions for direct execution.
306
308
  */
309
+ /**
310
+ * True when a tool's successful result should fold its returned exec
311
+ * `session_id` into the shared code session: built-in `CODE_EXECUTION_TOOLS`,
312
+ * plus host-declared sandbox-writing tools (`codeSessionToolNames`, e.g.
313
+ * create_file/edit_file). Kept name-scoped rather than a blanket artifact
314
+ * opt-in so only host-declared tools can influence the shared session.
315
+ */
316
+ private participatesInCodeSession;
307
317
  private storeCodeSessionFromResults;
308
318
  /**
309
319
  * Post-processes standard runTool outputs: dispatches ON_RUN_STEP_COMPLETED
@@ -170,6 +170,15 @@ export type RunConfig = {
170
170
  * eager dispatch unsafe.
171
171
  */
172
172
  eagerEventToolExecution?: EagerEventToolExecutionConfig;
173
+ /**
174
+ * Names of host tools that write to the code-execution sandbox but are not
175
+ * built-in `CODE_EXECUTION_TOOLS` (e.g. LibreChat's create_file/edit_file).
176
+ * Their successful results fold the returned exec `session_id` into the
177
+ * shared code session, so a file such a tool writes is visible to later
178
+ * bash_tool/execute_code calls running in the same sandbox. Host-declared so
179
+ * the SDK stays name-agnostic.
180
+ */
181
+ codeSessionToolNames?: string[];
173
182
  /**
174
183
  * Selects the execution backend for built-in code tools. Omit this to keep
175
184
  * the remote LibreChat Code API sandbox. Set `{ engine: 'local' }` to run
@@ -100,6 +100,12 @@ export type ToolNodeOptions = {
100
100
  directToolNames?: Set<string>;
101
101
  /** Opt-in eager execution for event-driven tool calls. */
102
102
  eagerEventToolExecution?: EagerEventToolExecutionConfig;
103
+ /**
104
+ * Host tool names that write to the code-execution sandbox but are not
105
+ * built-in `CODE_EXECUTION_TOOLS`. Their exec `session_id` is folded into the
106
+ * shared code session so later bash_tool/execute_code calls see written files.
107
+ */
108
+ codeSessionToolNames?: string[];
103
109
  /** Shared per-run eager execution registry populated by the stream handler. */
104
110
  eagerEventToolExecutions?: Map<string, EagerEventToolExecution>;
105
111
  /** Shared per-run per-tool turn counter used by eager and normal event dispatch. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.2.56",
3
+ "version": "3.2.57",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -4772,6 +4772,50 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
4772
4772
  expect(graph.eagerEventToolExecutions.has('call_file')).toBe(false);
4773
4773
  });
4774
4774
 
4775
+ it('does not prestart codeSessionToolNames tools even without excludeToolNames', async () => {
4776
+ // A declared session-writing host tool is side-effecting, so it must not be
4777
+ // eagerly prestarted even when the host didn't also list it in excludeToolNames.
4778
+ const graph = createGraph({
4779
+ eagerEventToolExecution: { enabled: true },
4780
+ codeSessionToolNames: ['create_file', 'edit_file'],
4781
+ });
4782
+ const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
4783
+ jest
4784
+ .spyOn(events, 'safeDispatchCustomEvent')
4785
+ .mockImplementation(async (event, data): Promise<void> => {
4786
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
4787
+ return;
4788
+ }
4789
+ const batch = data as t.ToolExecuteBatchRequest;
4790
+ toolExecuteCalls.push(batch);
4791
+ batch.resolve([
4792
+ { toolCallId: 'call_cf2', status: 'success', content: 'ok' },
4793
+ ]);
4794
+ });
4795
+
4796
+ await new ChatModelStreamHandler().handle(
4797
+ GraphEvents.CHAT_MODEL_STREAM,
4798
+ {
4799
+ chunk: {
4800
+ content: '',
4801
+ tool_calls: [
4802
+ {
4803
+ id: 'call_cf2',
4804
+ name: 'create_file',
4805
+ args: { path: '/mnt/data/y.py', content: 'print(2)' },
4806
+ },
4807
+ ],
4808
+ response_metadata: finalToolCallResponseMetadata,
4809
+ } as unknown as t.StreamChunk,
4810
+ },
4811
+ { langgraph_node: 'agent' },
4812
+ graph
4813
+ );
4814
+
4815
+ expect(toolExecuteCalls).toHaveLength(0);
4816
+ expect(graph.eagerEventToolExecutions.has('call_cf2')).toBe(false);
4817
+ });
4818
+
4775
4819
  it('keeps the direct-tool batch guard when an excluded tool is also direct', async () => {
4776
4820
  // edit_file is both a direct graph tool AND excluded from eager. A mixed
4777
4821
  // batch with a direct tool must suppress eager for the whole batch, so the
@@ -66,8 +66,8 @@ import {
66
66
  type CallbackEntry,
67
67
  } from '@/utils/callbacks';
68
68
  import { partitionAndMarkOpenRouterToolCache } from '@/llm/openrouter/toolCache';
69
- import { shouldTraceToolNodeForLangfuse } from '@/langfuseToolOutputTracing';
70
69
  import { ToolNode as CustomToolNode, toolsCondition } from '@/tools/ToolNode';
70
+ import { shouldTraceToolNodeForLangfuse } from '@/langfuseToolOutputTracing';
71
71
  import { createLocalCodingToolBundle } from '@/tools/local/LocalCodingTools';
72
72
  import { SubagentExecutor, resolveSubagentConfigs } from '@/tools/subagent';
73
73
  import { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
@@ -608,6 +608,7 @@ export abstract class Graph<
608
608
  * consumes the settled promises while preserving final ToolMessage order.
609
609
  */
610
610
  eagerEventToolExecution: t.EagerEventToolExecutionConfig | undefined;
611
+ codeSessionToolNames: string[] | undefined;
611
612
  eagerEventToolExecutions: Map<string, t.EagerEventToolExecution> = new Map();
612
613
  eagerEventToolUsageCount: Map<string, number> = new Map();
613
614
  private eagerEventToolUsageCountsByAgentId: Map<string, Map<string, number>> =
@@ -656,6 +657,7 @@ export abstract class Graph<
656
657
  this.humanInTheLoop = undefined;
657
658
  this.toolOutputReferences = undefined;
658
659
  this.eagerEventToolExecution = undefined;
660
+ this.codeSessionToolNames = undefined;
659
661
  this.eagerEventToolExecutions.clear();
660
662
  this.clearEagerEventToolUsageCounts();
661
663
  this.eagerEventToolCallChunks.clear();
@@ -1260,6 +1262,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1260
1262
  hookRegistry: this.hookRegistry,
1261
1263
  humanInTheLoop: this.humanInTheLoop,
1262
1264
  eagerEventToolExecution: this.eagerEventToolExecution,
1265
+ codeSessionToolNames: this.codeSessionToolNames,
1263
1266
  eagerEventToolExecutions: this.eagerEventToolExecutions,
1264
1267
  eagerEventToolUsageCount: this.getEagerEventToolUsageCount(
1265
1268
  agentContext?.agentId
@@ -1309,6 +1312,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1309
1312
  toolRegistry: agentContext?.toolRegistry,
1310
1313
  sessions: this.sessions,
1311
1314
  toolExecution: this.toolExecution,
1315
+ codeSessionToolNames: this.codeSessionToolNames,
1312
1316
  hookRegistry: this.hookRegistry,
1313
1317
  humanInTheLoop: this.humanInTheLoop,
1314
1318
  maxContextTokens: agentContext?.maxContextTokens,
@@ -2333,6 +2337,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2333
2337
  */
2334
2338
  childGraph.toolOutputReferences = this.toolOutputReferences;
2335
2339
  childGraph.eagerEventToolExecution = this.eagerEventToolExecution;
2340
+ childGraph.codeSessionToolNames = this.codeSessionToolNames;
2336
2341
  childGraph.toolExecution = this.toolExecution;
2337
2342
  childGraph.eventToolExecutionAvailable =
2338
2343
  this.handlerRegistry?.getHandler(GraphEvents.ON_TOOL_EXECUTE) !=
package/src/run.ts CHANGED
@@ -127,6 +127,7 @@ export class Run<_T extends t.BaseGraphState> {
127
127
  private langfuse?: t.LangfuseConfig;
128
128
  private toolOutputReferences?: t.ToolOutputReferencesConfig;
129
129
  private eagerEventToolExecution?: t.EagerEventToolExecutionConfig;
130
+ private codeSessionToolNames?: string[];
130
131
  private toolExecution?: t.ToolExecutionConfig;
131
132
  private subagentUsageSink?: t.SubagentUsageSink;
132
133
  private indexTokenCountMap?: Record<string, number>;
@@ -182,6 +183,7 @@ export class Run<_T extends t.BaseGraphState> {
182
183
  this.langfuse = config.langfuse;
183
184
  this.toolOutputReferences = config.toolOutputReferences;
184
185
  this.eagerEventToolExecution = config.eagerEventToolExecution;
186
+ this.codeSessionToolNames = config.codeSessionToolNames;
185
187
  this.toolExecution = config.toolExecution;
186
188
  this.subagentUsageSink = config.subagentUsageSink;
187
189
 
@@ -268,6 +270,7 @@ export class Run<_T extends t.BaseGraphState> {
268
270
  standardGraph.humanInTheLoop = this.humanInTheLoop;
269
271
  standardGraph.toolOutputReferences = this.toolOutputReferences;
270
272
  standardGraph.eagerEventToolExecution = this.eagerEventToolExecution;
273
+ standardGraph.codeSessionToolNames = this.codeSessionToolNames;
271
274
  standardGraph.toolExecution = this.toolExecution;
272
275
  this.Graph = standardGraph;
273
276
  return standardGraph.createWorkflow();
@@ -297,6 +300,7 @@ export class Run<_T extends t.BaseGraphState> {
297
300
  multiAgentGraph.humanInTheLoop = this.humanInTheLoop;
298
301
  multiAgentGraph.toolOutputReferences = this.toolOutputReferences;
299
302
  multiAgentGraph.eagerEventToolExecution = this.eagerEventToolExecution;
303
+ multiAgentGraph.codeSessionToolNames = this.codeSessionToolNames;
300
304
  multiAgentGraph.toolExecution = this.toolExecution;
301
305
  this.Graph = multiAgentGraph;
302
306
  return multiAgentGraph.createWorkflow();
package/src/stream.ts CHANGED
@@ -135,7 +135,14 @@ function isEagerExecutionExcludedTool(
135
135
  return false;
136
136
  }
137
137
  const excluded = graph.eagerEventToolExecution?.excludeToolNames;
138
- return excluded != null && excluded.includes(name);
138
+ if (excluded != null && excluded.includes(name)) {
139
+ return true;
140
+ }
141
+ // A code-session participant writes to the shared sandbox, so it is
142
+ // side-effecting: never prestart it speculatively (a revised/superseded turn
143
+ // would leave the write applied). Implies exclusion without the host having
144
+ // to also list the name in excludeToolNames.
145
+ return graph.codeSessionToolNames?.includes(name) === true;
139
146
  }
140
147
 
141
148
  function isDirectGraphTool(
@@ -195,7 +202,8 @@ function getCodeSessionContext(
195
202
  if (
196
203
  !CODE_EXECUTION_TOOLS.has(name) &&
197
204
  name !== Constants.SKILL_TOOL &&
198
- name !== Constants.READ_FILE
205
+ name !== Constants.READ_FILE &&
206
+ graph.codeSessionToolNames?.includes(name) !== true
199
207
  ) {
200
208
  return undefined;
201
209
  }
@@ -439,6 +439,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
439
439
  private eventDrivenMode: boolean = false;
440
440
  /** Opt-in stream-layer prestart config for event-driven tools. */
441
441
  private eagerEventToolExecution?: t.EagerEventToolExecutionConfig;
442
+ /** Host tools that write to the code sandbox and share its exec session. */
443
+ private codeSessionToolNames?: ReadonlySet<string>;
442
444
  /** Shared per-run prestarted tool registry populated by ChatModelStreamHandler. */
443
445
  private eagerEventToolExecutions?: Map<string, t.EagerEventToolExecution>;
444
446
  /** Shared per-run per-tool turn counter used by eager and normal event dispatch. */
@@ -515,6 +517,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
515
517
  agentId,
516
518
  executingAgentId,
517
519
  directToolNames,
520
+ codeSessionToolNames,
518
521
  maxContextTokens,
519
522
  maxToolResultChars,
520
523
  hookRegistry,
@@ -552,6 +555,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
552
555
  // existing agentId option) still get attribution without knowing the new option.
553
556
  this.executingAgentId = executingAgentId ?? agentId;
554
557
  this.directToolNames = directToolNames;
558
+ this.codeSessionToolNames =
559
+ codeSessionToolNames != null && codeSessionToolNames.length > 0
560
+ ? new Set(codeSessionToolNames)
561
+ : undefined;
555
562
  this.maxToolResultChars =
556
563
  maxToolResultChars ?? calculateMaxToolResultChars(maxContextTokens);
557
564
  this.hookRegistry = hookRegistry;
@@ -973,7 +980,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
973
980
  * still need to travel through `_injected_files`; the legacy
974
981
  * `/files/<session_id>` fallback was removed from the executors.
975
982
  */
976
- if (CODE_EXECUTION_TOOLS.has(call.name)) {
983
+ if (this.participatesInCodeSession(call.name)) {
977
984
  const codeSession = this.sessions?.get(Constants.EXECUTE_CODE) as
978
985
  | t.CodeSessionContext
979
986
  | undefined;
@@ -1759,6 +1766,23 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1759
1766
  * Extracts code execution session context from tool results and stores in Graph.sessions.
1760
1767
  * Mirrors the session storage logic in handleRunToolCompletions for direct execution.
1761
1768
  */
1769
+ /**
1770
+ * True when a tool's successful result should fold its returned exec
1771
+ * `session_id` into the shared code session: built-in `CODE_EXECUTION_TOOLS`,
1772
+ * plus host-declared sandbox-writing tools (`codeSessionToolNames`, e.g.
1773
+ * create_file/edit_file). Kept name-scoped rather than a blanket artifact
1774
+ * opt-in so only host-declared tools can influence the shared session.
1775
+ */
1776
+ private participatesInCodeSession(name: string): boolean {
1777
+ if (name === '') {
1778
+ return false;
1779
+ }
1780
+ return (
1781
+ CODE_EXECUTION_TOOLS.has(name) ||
1782
+ this.codeSessionToolNames?.has(name) === true
1783
+ );
1784
+ }
1785
+
1762
1786
  private storeCodeSessionFromResults(
1763
1787
  results: t.ToolExecuteResult[],
1764
1788
  requestMap: Map<string, t.ToolCallRequest>
@@ -1777,7 +1801,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1777
1801
  if (
1778
1802
  request?.name == null ||
1779
1803
  request.name === '' ||
1780
- (!CODE_EXECUTION_TOOLS.has(request.name) &&
1804
+ (!this.participatesInCodeSession(request.name) &&
1781
1805
  request.name !== Constants.SKILL_TOOL)
1782
1806
  ) {
1783
1807
  continue;
@@ -1830,7 +1854,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1830
1854
  continue;
1831
1855
  }
1832
1856
 
1833
- if (this.sessions && CODE_EXECUTION_TOOLS.has(call.name)) {
1857
+ if (this.sessions && this.participatesInCodeSession(call.name)) {
1834
1858
  const artifact = toolMessage.artifact as
1835
1859
  | t.CodeExecutionArtifact
1836
1860
  | undefined;
@@ -2463,7 +2487,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2463
2487
  const plan = buildToolExecutionRequestPlan({
2464
2488
  toolCalls: approvedEntries.map((entry) => {
2465
2489
  const codeSessionContext =
2466
- CODE_EXECUTION_TOOLS.has(entry.call.name) ||
2490
+ this.participatesInCodeSession(entry.call.name) ||
2467
2491
  entry.call.name === Constants.SKILL_TOOL ||
2468
2492
  entry.call.name === Constants.READ_FILE
2469
2493
  ? this.getCodeSessionContext()
@@ -140,6 +140,52 @@ describe('ToolNode code execution session management', () => {
140
140
  expect(capturedConfigs[0]._injected_files).toBeUndefined();
141
141
  });
142
142
 
143
+ it('injects the existing session into declared host tools on the direct path', async () => {
144
+ const capturedConfigs: Record<string, unknown>[] = [];
145
+ const sessions: t.ToolSessionMap = new Map();
146
+ sessions.set(Constants.EXECUTE_CODE, {
147
+ session_id: 'host-session',
148
+ files: [
149
+ { id: 'g1', name: 'gen.png', storage_session_id: 'host-session' },
150
+ ],
151
+ lastUpdated: Date.now(),
152
+ } satisfies t.CodeSessionContext);
153
+
154
+ const cfTool = tool(
155
+ async (_input, config) => {
156
+ capturedConfigs.push({ ...(config.toolCall ?? {}) });
157
+ return 'Created /mnt/data/x.py';
158
+ },
159
+ {
160
+ name: 'create_file',
161
+ description: 'write a file',
162
+ schema: z.object({ path: z.string(), content: z.string() }),
163
+ }
164
+ ) as unknown as StructuredToolInterface;
165
+
166
+ const toolNode = new ToolNode({
167
+ tools: [cfTool],
168
+ sessions,
169
+ codeSessionToolNames: ['create_file', 'edit_file'],
170
+ });
171
+
172
+ const aiMsg = new AIMessage({
173
+ content: '',
174
+ tool_calls: [
175
+ {
176
+ id: 'call_cf',
177
+ name: 'create_file',
178
+ args: { path: '/mnt/data/x.py', content: 'print(1)' },
179
+ },
180
+ ],
181
+ });
182
+ await toolNode.invoke({ messages: [aiMsg] });
183
+
184
+ // Declared host tool writes into the current sandbox, not a fresh one.
185
+ expect(capturedConfigs).toHaveLength(1);
186
+ expect(capturedConfigs[0].session_id).toBe('host-session');
187
+ });
188
+
143
189
  it('preserves per-file storage_session_id for multi-session files', async () => {
144
190
  const capturedConfigs: Record<string, unknown>[] = [];
145
191
  const sessions: t.ToolSessionMap = new Map();
@@ -774,6 +820,78 @@ describe('ToolNode code execution session management', () => {
774
820
  expect(sessions.has(Constants.EXECUTE_CODE)).toBe(false);
775
821
  });
776
822
 
823
+ it('stores the exec session for host tools declared in codeSessionToolNames', () => {
824
+ const sessions: t.ToolSessionMap = new Map();
825
+ const toolNode = new ToolNode({
826
+ tools: [createMockCodeTool({ capturedConfigs: [] })],
827
+ sessions,
828
+ eventDrivenMode: true,
829
+ codeSessionToolNames: ['create_file', 'edit_file'],
830
+ });
831
+
832
+ const storeMethod = (
833
+ toolNode as unknown as {
834
+ storeCodeSessionFromResults: (
835
+ results: t.ToolExecuteResult[],
836
+ requestMap: Map<string, t.ToolCallRequest>
837
+ ) => void;
838
+ }
839
+ ).storeCodeSessionFromResults.bind(toolNode);
840
+
841
+ storeMethod(
842
+ [
843
+ {
844
+ toolCallId: 'tc-cf',
845
+ content: 'Created /mnt/data/x.py (10 chars).',
846
+ artifact: { session_id: 'authoring-session' },
847
+ status: 'success',
848
+ },
849
+ ],
850
+ new Map([['tc-cf', { id: 'tc-cf', name: 'create_file', args: {} }]])
851
+ );
852
+
853
+ // create_file's exec session is folded into the shared code session, so a
854
+ // later bash_tool/execute_code call reuses the same sandbox.
855
+ const stored = sessions.get(Constants.EXECUTE_CODE) as
856
+ | t.CodeSessionContext
857
+ | undefined;
858
+ expect(stored?.session_id).toBe('authoring-session');
859
+ });
860
+
861
+ it('does not store a host authoring tool session when not declared', () => {
862
+ const sessions: t.ToolSessionMap = new Map();
863
+ const toolNode = new ToolNode({
864
+ tools: [createMockCodeTool({ capturedConfigs: [] })],
865
+ sessions,
866
+ eventDrivenMode: true,
867
+ // codeSessionToolNames omitted — create_file must NOT be treated as a
868
+ // code-session participant.
869
+ });
870
+
871
+ const storeMethod = (
872
+ toolNode as unknown as {
873
+ storeCodeSessionFromResults: (
874
+ results: t.ToolExecuteResult[],
875
+ requestMap: Map<string, t.ToolCallRequest>
876
+ ) => void;
877
+ }
878
+ ).storeCodeSessionFromResults.bind(toolNode);
879
+
880
+ storeMethod(
881
+ [
882
+ {
883
+ toolCallId: 'tc-cf2',
884
+ content: 'Created /mnt/data/x.py (10 chars).',
885
+ artifact: { session_id: 'authoring-session' },
886
+ status: 'success',
887
+ },
888
+ ],
889
+ new Map([['tc-cf2', { id: 'tc-cf2', name: 'create_file', args: {} }]])
890
+ );
891
+
892
+ expect(sessions.has(Constants.EXECUTE_CODE)).toBe(false);
893
+ });
894
+
777
895
  it('ignores error results', () => {
778
896
  const sessions: t.ToolSessionMap = new Map();
779
897
 
@@ -1102,5 +1220,47 @@ describe('ToolNode code execution session management', () => {
1102
1220
  expect(capturedRequests[0].name).toBe('web_search');
1103
1221
  expect(capturedRequests[0].codeSessionContext).toBeUndefined();
1104
1222
  });
1223
+
1224
+ it('attaches codeSessionContext to declared host tools so they write into the current sandbox', async () => {
1225
+ const sessions: t.ToolSessionMap = new Map();
1226
+ sessions.set(Constants.EXECUTE_CODE, {
1227
+ session_id: 'cf-session',
1228
+ files: [
1229
+ { id: 'g1', name: 'gen.png', storage_session_id: 'cf-session' },
1230
+ ],
1231
+ lastUpdated: Date.now(),
1232
+ } satisfies t.CodeSessionContext);
1233
+
1234
+ const { capturedRequests } = captureBatchRequests();
1235
+
1236
+ const toolNode = new ToolNode({
1237
+ tools: [createDummyTool('create_file')],
1238
+ sessions,
1239
+ eventDrivenMode: true,
1240
+ codeSessionToolNames: ['create_file', 'edit_file'],
1241
+ toolCallStepIds: new Map([['call_cf', 'step_cf']]),
1242
+ });
1243
+
1244
+ const aiMsg = new AIMessage({
1245
+ content: '',
1246
+ tool_calls: [
1247
+ {
1248
+ id: 'call_cf',
1249
+ name: 'create_file',
1250
+ args: { path: '/mnt/data/x.py', content: 'print(1)' },
1251
+ },
1252
+ ],
1253
+ });
1254
+
1255
+ await toolNode.invoke({ messages: [aiMsg] });
1256
+
1257
+ expect(capturedRequests).toHaveLength(1);
1258
+ expect(capturedRequests[0].name).toBe('create_file');
1259
+ // Existing session is injected so create_file writes into the current
1260
+ // sandbox instead of spawning a separate one.
1261
+ expect(capturedRequests[0].codeSessionContext?.session_id).toBe(
1262
+ 'cf-session'
1263
+ );
1264
+ });
1105
1265
  });
1106
1266
  });
package/src/types/run.ts CHANGED
@@ -184,6 +184,15 @@ export type RunConfig = {
184
184
  * eager dispatch unsafe.
185
185
  */
186
186
  eagerEventToolExecution?: EagerEventToolExecutionConfig;
187
+ /**
188
+ * Names of host tools that write to the code-execution sandbox but are not
189
+ * built-in `CODE_EXECUTION_TOOLS` (e.g. LibreChat's create_file/edit_file).
190
+ * Their successful results fold the returned exec `session_id` into the
191
+ * shared code session, so a file such a tool writes is visible to later
192
+ * bash_tool/execute_code calls running in the same sandbox. Host-declared so
193
+ * the SDK stays name-agnostic.
194
+ */
195
+ codeSessionToolNames?: string[];
187
196
  /**
188
197
  * Selects the execution backend for built-in code tools. Omit this to keep
189
198
  * the remote LibreChat Code API sandbox. Set `{ engine: 'local' }` to run
@@ -110,6 +110,12 @@ export type ToolNodeOptions = {
110
110
  directToolNames?: Set<string>;
111
111
  /** Opt-in eager execution for event-driven tool calls. */
112
112
  eagerEventToolExecution?: EagerEventToolExecutionConfig;
113
+ /**
114
+ * Host tool names that write to the code-execution sandbox but are not
115
+ * built-in `CODE_EXECUTION_TOOLS`. Their exec `session_id` is folded into the
116
+ * shared code session so later bash_tool/execute_code calls see written files.
117
+ */
118
+ codeSessionToolNames?: string[];
113
119
  /** Shared per-run eager execution registry populated by the stream handler. */
114
120
  eagerEventToolExecutions?: Map<string, EagerEventToolExecution>;
115
121
  /** Shared per-run per-tool turn counter used by eager and normal event dispatch. */