@librechat/agents 3.1.37 → 3.1.39

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 (41) 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 +1 -1
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/messages/cache.cjs +2 -2
  6. package/dist/cjs/messages/cache.cjs.map +1 -1
  7. package/dist/cjs/stream.cjs +2 -1
  8. package/dist/cjs/stream.cjs.map +1 -1
  9. package/dist/cjs/tools/CodeExecutor.cjs +1 -0
  10. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  11. package/dist/cjs/tools/handlers.cjs +25 -8
  12. package/dist/cjs/tools/handlers.cjs.map +1 -1
  13. package/dist/esm/agents/AgentContext.mjs +3 -0
  14. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  15. package/dist/esm/graphs/Graph.mjs +1 -1
  16. package/dist/esm/graphs/Graph.mjs.map +1 -1
  17. package/dist/esm/messages/cache.mjs +2 -2
  18. package/dist/esm/messages/cache.mjs.map +1 -1
  19. package/dist/esm/stream.mjs +2 -1
  20. package/dist/esm/stream.mjs.map +1 -1
  21. package/dist/esm/tools/CodeExecutor.mjs +1 -0
  22. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  23. package/dist/esm/tools/handlers.mjs +25 -8
  24. package/dist/esm/tools/handlers.mjs.map +1 -1
  25. package/dist/types/agents/AgentContext.d.ts +2 -0
  26. package/dist/types/tools/CodeExecutor.d.ts +2 -2
  27. package/dist/types/types/tools.d.ts +1 -0
  28. package/package.json +1 -1
  29. package/src/agents/AgentContext.ts +3 -0
  30. package/src/graphs/Graph.ts +1 -1
  31. package/src/messages/cache.test.ts +41 -0
  32. package/src/messages/cache.ts +2 -2
  33. package/src/scripts/bedrock-content-aggregation-test.ts +265 -0
  34. package/src/scripts/bedrock-parallel-tools-test.ts +203 -0
  35. package/src/scripts/tools.ts +3 -12
  36. package/src/stream.ts +2 -1
  37. package/src/tools/CodeExecutor.ts +1 -0
  38. package/src/tools/__tests__/ToolNode.session.test.ts +465 -0
  39. package/src/tools/__tests__/handlers.test.ts +994 -0
  40. package/src/tools/handlers.ts +32 -13
  41. package/src/types/tools.ts +1 -0
@@ -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
+ });