@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.
- package/dist/cjs/agents/AgentContext.cjs +3 -0
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +38 -29
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/stream.cjs +2 -1
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +90 -14
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/handlers.cjs +25 -8
- package/dist/cjs/tools/handlers.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +3 -0
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +38 -29
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/stream.mjs +2 -1
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +90 -14
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/handlers.mjs +25 -8
- package/dist/esm/tools/handlers.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +2 -0
- package/dist/types/tools/ToolNode.d.ts +10 -0
- package/dist/types/types/tools.d.ts +7 -1
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +3 -0
- package/src/graphs/Graph.ts +41 -36
- package/src/scripts/bedrock-content-aggregation-test.ts +265 -0
- package/src/scripts/bedrock-parallel-tools-test.ts +203 -0
- package/src/scripts/tools.ts +3 -12
- package/src/stream.ts +2 -1
- package/src/tools/ToolNode.ts +120 -14
- package/src/tools/__tests__/ToolNode.session.test.ts +465 -0
- package/src/tools/__tests__/handlers.test.ts +994 -0
- package/src/tools/handlers.ts +32 -13
- package/src/types/tools.ts +7 -1
package/src/tools/ToolNode.ts
CHANGED
|
@@ -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?.
|
|
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
|
-
|
|
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
|
+
});
|