@librechat/agents 3.1.36 → 3.1.38

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 (35) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +3 -0
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +38 -29
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/stream.cjs +2 -1
  6. package/dist/cjs/stream.cjs.map +1 -1
  7. package/dist/cjs/tools/ToolNode.cjs +90 -14
  8. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  9. package/dist/cjs/tools/handlers.cjs +25 -8
  10. package/dist/cjs/tools/handlers.cjs.map +1 -1
  11. package/dist/esm/agents/AgentContext.mjs +3 -0
  12. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  13. package/dist/esm/graphs/Graph.mjs +38 -29
  14. package/dist/esm/graphs/Graph.mjs.map +1 -1
  15. package/dist/esm/stream.mjs +2 -1
  16. package/dist/esm/stream.mjs.map +1 -1
  17. package/dist/esm/tools/ToolNode.mjs +90 -14
  18. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  19. package/dist/esm/tools/handlers.mjs +25 -8
  20. package/dist/esm/tools/handlers.mjs.map +1 -1
  21. package/dist/types/agents/AgentContext.d.ts +2 -0
  22. package/dist/types/tools/ToolNode.d.ts +10 -0
  23. package/dist/types/types/tools.d.ts +7 -1
  24. package/package.json +1 -1
  25. package/src/agents/AgentContext.ts +3 -0
  26. package/src/graphs/Graph.ts +41 -36
  27. package/src/scripts/bedrock-content-aggregation-test.ts +265 -0
  28. package/src/scripts/bedrock-parallel-tools-test.ts +203 -0
  29. package/src/scripts/tools.ts +3 -12
  30. package/src/stream.ts +2 -1
  31. package/src/tools/ToolNode.ts +120 -14
  32. package/src/tools/__tests__/ToolNode.session.test.ts +465 -0
  33. package/src/tools/__tests__/handlers.test.ts +994 -0
  34. package/src/tools/handlers.ts +32 -13
  35. package/src/types/tools.ts +7 -1
@@ -161,6 +161,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
161
161
  * Each file uses its own session_id (supporting multi-session file tracking).
162
162
  * Both session_id and _injected_files are injected directly to invokeParams
163
163
  * (not inside args) so they bypass Zod schema validation and reach config.toolCall.
164
+ *
165
+ * session_id is always injected when available (even without tracked files)
166
+ * so the CodeExecutor can fall back to the /files endpoint for session continuity.
164
167
  */
165
168
  if (
166
169
  call.name === Constants.EXECUTE_CODE ||
@@ -169,23 +172,20 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
169
172
  const codeSession = this.sessions?.get(Constants.EXECUTE_CODE) as
170
173
  | t.CodeSessionContext
171
174
  | undefined;
172
- if (codeSession?.files != null && codeSession.files.length > 0) {
173
- /**
174
- * Convert tracked files to CodeEnvFile format for the API.
175
- * Each file uses its own session_id (set when file was created).
176
- * This supports files from multiple parallel/sequential executions.
177
- */
178
- const fileRefs: t.CodeEnvFile[] = codeSession.files.map((file) => ({
179
- session_id: file.session_id ?? codeSession.session_id,
180
- id: file.id,
181
- name: file.name,
182
- }));
183
- /** Inject latest session_id and files - bypasses Zod, reaches config.toolCall */
175
+ if (codeSession?.session_id != null && codeSession.session_id !== '') {
184
176
  invokeParams = {
185
177
  ...invokeParams,
186
178
  session_id: codeSession.session_id,
187
- _injected_files: fileRefs,
188
179
  };
180
+
181
+ if (codeSession.files != null && codeSession.files.length > 0) {
182
+ const fileRefs: t.CodeEnvFile[] = codeSession.files.map((file) => ({
183
+ session_id: file.session_id ?? codeSession.session_id,
184
+ id: file.id,
185
+ name: file.name,
186
+ }));
187
+ invokeParams._injected_files = fileRefs;
188
+ }
189
189
  }
190
190
  }
191
191
 
@@ -256,6 +256,100 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
256
256
  }
257
257
  }
258
258
 
259
+ /**
260
+ * Builds code session context for injection into event-driven tool calls.
261
+ * Mirrors the session injection logic in runTool() for direct execution.
262
+ */
263
+ private getCodeSessionContext(): t.ToolCallRequest['codeSessionContext'] {
264
+ if (!this.sessions) {
265
+ return undefined;
266
+ }
267
+
268
+ const codeSession = this.sessions.get(Constants.EXECUTE_CODE) as
269
+ | t.CodeSessionContext
270
+ | undefined;
271
+ if (!codeSession) {
272
+ return undefined;
273
+ }
274
+
275
+ const context: NonNullable<t.ToolCallRequest['codeSessionContext']> = {
276
+ session_id: codeSession.session_id,
277
+ };
278
+
279
+ if (codeSession.files && codeSession.files.length > 0) {
280
+ context.files = codeSession.files.map((file) => ({
281
+ session_id: file.session_id ?? codeSession.session_id,
282
+ id: file.id,
283
+ name: file.name,
284
+ }));
285
+ }
286
+
287
+ return context;
288
+ }
289
+
290
+ /**
291
+ * Extracts code execution session context from tool results and stores in Graph.sessions.
292
+ * Mirrors the session storage logic in Graph.handleToolCallCompleted() for direct execution.
293
+ */
294
+ private storeCodeSessionFromResults(
295
+ results: t.ToolExecuteResult[],
296
+ requests: t.ToolCallRequest[]
297
+ ): void {
298
+ if (!this.sessions) {
299
+ return;
300
+ }
301
+
302
+ for (let i = 0; i < results.length; i++) {
303
+ const result = results[i];
304
+ if (result.status !== 'success' || result.artifact == null) {
305
+ continue;
306
+ }
307
+
308
+ const request = requests.find((r) => r.id === result.toolCallId);
309
+ if (
310
+ request?.name !== Constants.EXECUTE_CODE &&
311
+ request?.name !== Constants.PROGRAMMATIC_TOOL_CALLING
312
+ ) {
313
+ continue;
314
+ }
315
+
316
+ const artifact = result.artifact as t.CodeExecutionArtifact | undefined;
317
+ if (artifact?.session_id == null || artifact.session_id === '') {
318
+ continue;
319
+ }
320
+
321
+ const newFiles = artifact.files ?? [];
322
+ const existingSession = this.sessions.get(Constants.EXECUTE_CODE) as
323
+ | t.CodeSessionContext
324
+ | undefined;
325
+ const existingFiles = existingSession?.files ?? [];
326
+
327
+ if (newFiles.length > 0) {
328
+ const filesWithSession: t.FileRefs = newFiles.map((file) => ({
329
+ ...file,
330
+ session_id: artifact.session_id,
331
+ }));
332
+
333
+ const newFileNames = new Set(filesWithSession.map((f) => f.name));
334
+ const filteredExisting = existingFiles.filter(
335
+ (f) => !newFileNames.has(f.name)
336
+ );
337
+
338
+ this.sessions.set(Constants.EXECUTE_CODE, {
339
+ session_id: artifact.session_id,
340
+ files: [...filteredExisting, ...filesWithSession],
341
+ lastUpdated: Date.now(),
342
+ });
343
+ } else {
344
+ this.sessions.set(Constants.EXECUTE_CODE, {
345
+ session_id: artifact.session_id,
346
+ files: existingFiles,
347
+ lastUpdated: Date.now(),
348
+ });
349
+ }
350
+ }
351
+ }
352
+
259
353
  /**
260
354
  * Dispatches tool calls to the host via ON_TOOL_EXECUTE event and returns raw ToolMessages.
261
355
  * Core logic for event-driven execution, separated from output shaping.
@@ -267,13 +361,23 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
267
361
  const requests: t.ToolCallRequest[] = toolCalls.map((call) => {
268
362
  const turn = this.toolUsageCount.get(call.name) ?? 0;
269
363
  this.toolUsageCount.set(call.name, turn + 1);
270
- return {
364
+
365
+ const request: t.ToolCallRequest = {
271
366
  id: call.id!,
272
367
  name: call.name,
273
368
  args: call.args as Record<string, unknown>,
274
369
  stepId: this.toolCallStepIds?.get(call.id!),
275
370
  turn,
276
371
  };
372
+
373
+ if (
374
+ call.name === Constants.EXECUTE_CODE ||
375
+ call.name === Constants.PROGRAMMATIC_TOOL_CALLING
376
+ ) {
377
+ request.codeSessionContext = this.getCodeSessionContext();
378
+ }
379
+
380
+ return request;
277
381
  });
278
382
 
279
383
  const results = await new Promise<t.ToolExecuteResult[]>(
@@ -294,6 +398,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
294
398
  }
295
399
  );
296
400
 
401
+ this.storeCodeSessionFromResults(results, requests);
402
+
297
403
  return results.map((result) => {
298
404
  const request = requests.find((r) => r.id === result.toolCallId);
299
405
  const toolName = request?.name ?? 'unknown';
@@ -0,0 +1,465 @@
1
+ import { z } from 'zod';
2
+ import { tool } from '@langchain/core/tools';
3
+ import { AIMessage } from '@langchain/core/messages';
4
+ import { describe, it, expect } from '@jest/globals';
5
+ import type { StructuredToolInterface } from '@langchain/core/tools';
6
+ import type * as t from '@/types';
7
+ import { ToolNode } from '../ToolNode';
8
+ import { Constants } from '@/common';
9
+
10
+ /**
11
+ * Creates a mock execute_code tool that captures the toolCall config it receives.
12
+ * Returns a content_and_artifact response with configurable session/files.
13
+ */
14
+ function createMockCodeTool(options: {
15
+ capturedConfigs: Record<string, unknown>[];
16
+ artifact?: t.CodeExecutionArtifact;
17
+ }): StructuredToolInterface {
18
+ const { capturedConfigs, artifact } = options;
19
+ const defaultArtifact: t.CodeExecutionArtifact = {
20
+ session_id: 'new-session-123',
21
+ files: [],
22
+ };
23
+
24
+ return tool(
25
+ async (_input, config) => {
26
+ capturedConfigs.push({ ...(config.toolCall ?? {}) });
27
+ return ['stdout:\nhello world\n', artifact ?? defaultArtifact];
28
+ },
29
+ {
30
+ name: Constants.EXECUTE_CODE,
31
+ description: 'Execute code in a sandbox',
32
+ schema: z.object({ lang: z.string(), code: z.string() }),
33
+ responseFormat: Constants.CONTENT_AND_ARTIFACT,
34
+ }
35
+ ) as unknown as StructuredToolInterface;
36
+ }
37
+
38
+ function createAIMessageWithCodeCall(callId: string): AIMessage {
39
+ return new AIMessage({
40
+ content: '',
41
+ tool_calls: [
42
+ {
43
+ id: callId,
44
+ name: Constants.EXECUTE_CODE,
45
+ args: { lang: 'python', code: 'print("hello")' },
46
+ },
47
+ ],
48
+ });
49
+ }
50
+
51
+ describe('ToolNode code execution session management', () => {
52
+ describe('session injection via runTool (direct execution)', () => {
53
+ it('injects session_id and _injected_files when session has files', async () => {
54
+ const capturedConfigs: Record<string, unknown>[] = [];
55
+ const sessions: t.ToolSessionMap = new Map();
56
+ sessions.set(Constants.EXECUTE_CODE, {
57
+ session_id: 'prev-session-abc',
58
+ files: [
59
+ { id: 'file1', name: 'data.csv', session_id: 'prev-session-abc' },
60
+ { id: 'file2', name: 'chart.png', session_id: 'prev-session-abc' },
61
+ ],
62
+ lastUpdated: Date.now(),
63
+ } satisfies t.CodeSessionContext);
64
+
65
+ const mockTool = createMockCodeTool({ capturedConfigs });
66
+ const toolNode = new ToolNode({ tools: [mockTool], sessions });
67
+
68
+ const aiMsg = createAIMessageWithCodeCall('call_1');
69
+ await toolNode.invoke({ messages: [aiMsg] });
70
+
71
+ expect(capturedConfigs).toHaveLength(1);
72
+ expect(capturedConfigs[0].session_id).toBe('prev-session-abc');
73
+ expect(capturedConfigs[0]._injected_files).toEqual([
74
+ { session_id: 'prev-session-abc', id: 'file1', name: 'data.csv' },
75
+ { session_id: 'prev-session-abc', id: 'file2', name: 'chart.png' },
76
+ ]);
77
+ });
78
+
79
+ it('injects session_id even when session has no tracked files', async () => {
80
+ const capturedConfigs: Record<string, unknown>[] = [];
81
+ const sessions: t.ToolSessionMap = new Map();
82
+ sessions.set(Constants.EXECUTE_CODE, {
83
+ session_id: 'prev-session-no-files',
84
+ files: [],
85
+ lastUpdated: Date.now(),
86
+ } satisfies t.CodeSessionContext);
87
+
88
+ const mockTool = createMockCodeTool({ capturedConfigs });
89
+ const toolNode = new ToolNode({ tools: [mockTool], sessions });
90
+
91
+ const aiMsg = createAIMessageWithCodeCall('call_2');
92
+ await toolNode.invoke({ messages: [aiMsg] });
93
+
94
+ expect(capturedConfigs).toHaveLength(1);
95
+ expect(capturedConfigs[0].session_id).toBe('prev-session-no-files');
96
+ expect(capturedConfigs[0]._injected_files).toBeUndefined();
97
+ });
98
+
99
+ it('does not inject session context when no session exists', async () => {
100
+ const capturedConfigs: Record<string, unknown>[] = [];
101
+ const sessions: t.ToolSessionMap = new Map();
102
+
103
+ const mockTool = createMockCodeTool({ capturedConfigs });
104
+ const toolNode = new ToolNode({ tools: [mockTool], sessions });
105
+
106
+ const aiMsg = createAIMessageWithCodeCall('call_3');
107
+ await toolNode.invoke({ messages: [aiMsg] });
108
+
109
+ expect(capturedConfigs).toHaveLength(1);
110
+ expect(capturedConfigs[0].session_id).toBeUndefined();
111
+ expect(capturedConfigs[0]._injected_files).toBeUndefined();
112
+ });
113
+
114
+ it('preserves per-file session_id for multi-session files', async () => {
115
+ const capturedConfigs: Record<string, unknown>[] = [];
116
+ const sessions: t.ToolSessionMap = new Map();
117
+ sessions.set(Constants.EXECUTE_CODE, {
118
+ session_id: 'session-B',
119
+ files: [
120
+ { id: 'f1', name: 'old.csv', session_id: 'session-A' },
121
+ { id: 'f2', name: 'new.png', session_id: 'session-B' },
122
+ ],
123
+ lastUpdated: Date.now(),
124
+ } satisfies t.CodeSessionContext);
125
+
126
+ const mockTool = createMockCodeTool({ capturedConfigs });
127
+ const toolNode = new ToolNode({ tools: [mockTool], sessions });
128
+
129
+ const aiMsg = createAIMessageWithCodeCall('call_4');
130
+ await toolNode.invoke({ messages: [aiMsg] });
131
+
132
+ const files = capturedConfigs[0]._injected_files as t.CodeEnvFile[];
133
+ expect(files[0].session_id).toBe('session-A');
134
+ expect(files[1].session_id).toBe('session-B');
135
+ });
136
+ });
137
+
138
+ describe('getCodeSessionContext (via dispatchToolEvents request building)', () => {
139
+ it('builds session context with files for event-driven requests', () => {
140
+ const sessions: t.ToolSessionMap = new Map();
141
+ sessions.set(Constants.EXECUTE_CODE, {
142
+ session_id: 'evt-session',
143
+ files: [{ id: 'ef1', name: 'out.parquet', session_id: 'evt-session' }],
144
+ lastUpdated: Date.now(),
145
+ } satisfies t.CodeSessionContext);
146
+
147
+ const mockTool = createMockCodeTool({ capturedConfigs: [] });
148
+ const toolNode = new ToolNode({
149
+ tools: [mockTool],
150
+ sessions,
151
+ eventDrivenMode: true,
152
+ });
153
+
154
+ const context = (
155
+ toolNode as unknown as { getCodeSessionContext: () => unknown }
156
+ ).getCodeSessionContext();
157
+
158
+ expect(context).toEqual({
159
+ session_id: 'evt-session',
160
+ files: [{ session_id: 'evt-session', id: 'ef1', name: 'out.parquet' }],
161
+ });
162
+ });
163
+
164
+ it('builds session context without files when session has no tracked files', () => {
165
+ const sessions: t.ToolSessionMap = new Map();
166
+ sessions.set(Constants.EXECUTE_CODE, {
167
+ session_id: 'evt-session-empty',
168
+ files: [],
169
+ lastUpdated: Date.now(),
170
+ } satisfies t.CodeSessionContext);
171
+
172
+ const mockTool = createMockCodeTool({ capturedConfigs: [] });
173
+ const toolNode = new ToolNode({
174
+ tools: [mockTool],
175
+ sessions,
176
+ eventDrivenMode: true,
177
+ });
178
+
179
+ const context = (
180
+ toolNode as unknown as { getCodeSessionContext: () => unknown }
181
+ ).getCodeSessionContext();
182
+
183
+ expect(context).toEqual({ session_id: 'evt-session-empty' });
184
+ });
185
+
186
+ it('returns undefined when no session exists', () => {
187
+ const sessions: t.ToolSessionMap = new Map();
188
+
189
+ const mockTool = createMockCodeTool({ capturedConfigs: [] });
190
+ const toolNode = new ToolNode({
191
+ tools: [mockTool],
192
+ sessions,
193
+ eventDrivenMode: true,
194
+ });
195
+
196
+ const context = (
197
+ toolNode as unknown as { getCodeSessionContext: () => unknown }
198
+ ).getCodeSessionContext();
199
+
200
+ expect(context).toBeUndefined();
201
+ });
202
+ });
203
+
204
+ describe('storeCodeSessionFromResults (session storage from artifacts)', () => {
205
+ it('stores session with files from code execution results', () => {
206
+ const sessions: t.ToolSessionMap = new Map();
207
+
208
+ const mockTool = createMockCodeTool({ capturedConfigs: [] });
209
+ const toolNode = new ToolNode({
210
+ tools: [mockTool],
211
+ sessions,
212
+ eventDrivenMode: true,
213
+ });
214
+
215
+ const storeMethod = (
216
+ toolNode as unknown as {
217
+ storeCodeSessionFromResults: (
218
+ results: t.ToolExecuteResult[],
219
+ requests: t.ToolCallRequest[]
220
+ ) => void;
221
+ }
222
+ ).storeCodeSessionFromResults.bind(toolNode);
223
+
224
+ storeMethod(
225
+ [
226
+ {
227
+ toolCallId: 'tc1',
228
+ content: 'output',
229
+ artifact: {
230
+ session_id: 'new-sess',
231
+ files: [{ id: 'f1', name: 'result.csv' }],
232
+ },
233
+ status: 'success',
234
+ },
235
+ ],
236
+ [{ id: 'tc1', name: Constants.EXECUTE_CODE, args: {} }]
237
+ );
238
+
239
+ const stored = sessions.get(
240
+ Constants.EXECUTE_CODE
241
+ ) as t.CodeSessionContext;
242
+ expect(stored).toBeDefined();
243
+ expect(stored.session_id).toBe('new-sess');
244
+ expect(stored.files).toHaveLength(1);
245
+ expect(stored.files![0]).toEqual(
246
+ expect.objectContaining({
247
+ id: 'f1',
248
+ name: 'result.csv',
249
+ session_id: 'new-sess',
250
+ })
251
+ );
252
+ });
253
+
254
+ it('stores session_id even when Code API returns no files', () => {
255
+ const sessions: t.ToolSessionMap = new Map();
256
+
257
+ const mockTool = createMockCodeTool({ capturedConfigs: [] });
258
+ const toolNode = new ToolNode({
259
+ tools: [mockTool],
260
+ sessions,
261
+ eventDrivenMode: true,
262
+ });
263
+
264
+ const storeMethod = (
265
+ toolNode as unknown as {
266
+ storeCodeSessionFromResults: (
267
+ results: t.ToolExecuteResult[],
268
+ requests: t.ToolCallRequest[]
269
+ ) => void;
270
+ }
271
+ ).storeCodeSessionFromResults.bind(toolNode);
272
+
273
+ storeMethod(
274
+ [
275
+ {
276
+ toolCallId: 'tc2',
277
+ content: 'stdout:\nSaved parquet\n',
278
+ artifact: { session_id: 'parquet-session', files: [] },
279
+ status: 'success',
280
+ },
281
+ ],
282
+ [{ id: 'tc2', name: Constants.EXECUTE_CODE, args: {} }]
283
+ );
284
+
285
+ const stored = sessions.get(
286
+ Constants.EXECUTE_CODE
287
+ ) as t.CodeSessionContext;
288
+ expect(stored).toBeDefined();
289
+ expect(stored.session_id).toBe('parquet-session');
290
+ expect(stored.files).toEqual([]);
291
+ });
292
+
293
+ it('merges new files with existing session, replacing same-name files', () => {
294
+ const sessions: t.ToolSessionMap = new Map();
295
+ sessions.set(Constants.EXECUTE_CODE, {
296
+ session_id: 'old-sess',
297
+ files: [
298
+ { id: 'f1', name: 'data.csv', session_id: 'old-sess' },
299
+ { id: 'f2', name: 'chart.png', session_id: 'old-sess' },
300
+ ],
301
+ lastUpdated: Date.now(),
302
+ } satisfies t.CodeSessionContext);
303
+
304
+ const mockTool = createMockCodeTool({ capturedConfigs: [] });
305
+ const toolNode = new ToolNode({
306
+ tools: [mockTool],
307
+ sessions,
308
+ eventDrivenMode: true,
309
+ });
310
+
311
+ const storeMethod = (
312
+ toolNode as unknown as {
313
+ storeCodeSessionFromResults: (
314
+ results: t.ToolExecuteResult[],
315
+ requests: t.ToolCallRequest[]
316
+ ) => void;
317
+ }
318
+ ).storeCodeSessionFromResults.bind(toolNode);
319
+
320
+ storeMethod(
321
+ [
322
+ {
323
+ toolCallId: 'tc3',
324
+ content: 'output',
325
+ artifact: {
326
+ session_id: 'new-sess',
327
+ files: [{ id: 'f3', name: 'chart.png' }],
328
+ },
329
+ status: 'success',
330
+ },
331
+ ],
332
+ [{ id: 'tc3', name: Constants.EXECUTE_CODE, args: {} }]
333
+ );
334
+
335
+ const stored = sessions.get(
336
+ Constants.EXECUTE_CODE
337
+ ) as t.CodeSessionContext;
338
+ expect(stored.session_id).toBe('new-sess');
339
+ expect(stored.files).toHaveLength(2);
340
+
341
+ const csvFile = stored.files!.find((f) => f.name === 'data.csv');
342
+ expect(csvFile!.session_id).toBe('old-sess');
343
+
344
+ const chartFile = stored.files!.find((f) => f.name === 'chart.png');
345
+ expect(chartFile!.id).toBe('f3');
346
+ expect(chartFile!.session_id).toBe('new-sess');
347
+ });
348
+
349
+ it('preserves existing files when new execution has no files', () => {
350
+ const sessions: t.ToolSessionMap = new Map();
351
+ sessions.set(Constants.EXECUTE_CODE, {
352
+ session_id: 'old-sess',
353
+ files: [{ id: 'f1', name: 'data.csv', session_id: 'old-sess' }],
354
+ lastUpdated: Date.now(),
355
+ } satisfies t.CodeSessionContext);
356
+
357
+ const mockTool = createMockCodeTool({ capturedConfigs: [] });
358
+ const toolNode = new ToolNode({
359
+ tools: [mockTool],
360
+ sessions,
361
+ eventDrivenMode: true,
362
+ });
363
+
364
+ const storeMethod = (
365
+ toolNode as unknown as {
366
+ storeCodeSessionFromResults: (
367
+ results: t.ToolExecuteResult[],
368
+ requests: t.ToolCallRequest[]
369
+ ) => void;
370
+ }
371
+ ).storeCodeSessionFromResults.bind(toolNode);
372
+
373
+ storeMethod(
374
+ [
375
+ {
376
+ toolCallId: 'tc4',
377
+ content: 'stdout:\nno files generated\n',
378
+ artifact: { session_id: 'new-sess', files: [] },
379
+ status: 'success',
380
+ },
381
+ ],
382
+ [{ id: 'tc4', name: Constants.EXECUTE_CODE, args: {} }]
383
+ );
384
+
385
+ const stored = sessions.get(
386
+ Constants.EXECUTE_CODE
387
+ ) as t.CodeSessionContext;
388
+ expect(stored.session_id).toBe('new-sess');
389
+ expect(stored.files).toHaveLength(1);
390
+ expect(stored.files![0].name).toBe('data.csv');
391
+ });
392
+
393
+ it('ignores non-code-execution tool results', () => {
394
+ const sessions: t.ToolSessionMap = new Map();
395
+
396
+ const mockTool = createMockCodeTool({ capturedConfigs: [] });
397
+ const toolNode = new ToolNode({
398
+ tools: [mockTool],
399
+ sessions,
400
+ eventDrivenMode: true,
401
+ });
402
+
403
+ const storeMethod = (
404
+ toolNode as unknown as {
405
+ storeCodeSessionFromResults: (
406
+ results: t.ToolExecuteResult[],
407
+ requests: t.ToolCallRequest[]
408
+ ) => void;
409
+ }
410
+ ).storeCodeSessionFromResults.bind(toolNode);
411
+
412
+ storeMethod(
413
+ [
414
+ {
415
+ toolCallId: 'tc5',
416
+ content: 'search results',
417
+ artifact: { session_id: 'should-not-store' },
418
+ status: 'success',
419
+ },
420
+ ],
421
+ [{ id: 'tc5', name: 'web_search', args: {} }]
422
+ );
423
+
424
+ expect(sessions.has(Constants.EXECUTE_CODE)).toBe(false);
425
+ });
426
+
427
+ it('ignores error results', () => {
428
+ const sessions: t.ToolSessionMap = new Map();
429
+
430
+ const mockTool = createMockCodeTool({ capturedConfigs: [] });
431
+ const toolNode = new ToolNode({
432
+ tools: [mockTool],
433
+ sessions,
434
+ eventDrivenMode: true,
435
+ });
436
+
437
+ const storeMethod = (
438
+ toolNode as unknown as {
439
+ storeCodeSessionFromResults: (
440
+ results: t.ToolExecuteResult[],
441
+ requests: t.ToolCallRequest[]
442
+ ) => void;
443
+ }
444
+ ).storeCodeSessionFromResults.bind(toolNode);
445
+
446
+ storeMethod(
447
+ [
448
+ {
449
+ toolCallId: 'tc6',
450
+ content: '',
451
+ artifact: {
452
+ session_id: 'error-session',
453
+ files: [{ id: 'f1', name: 'x' }],
454
+ },
455
+ status: 'error',
456
+ errorMessage: 'execution failed',
457
+ },
458
+ ],
459
+ [{ id: 'tc6', name: Constants.EXECUTE_CODE, args: {} }]
460
+ );
461
+
462
+ expect(sessions.has(Constants.EXECUTE_CODE)).toBe(false);
463
+ });
464
+ });
465
+ });