@librechat/agents 3.1.83 → 3.1.85
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 +26 -3
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +1 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/events.cjs +2 -1
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +5 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -2
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/main.cjs +4 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +5 -2
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +26 -24
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +28 -2
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +130 -56
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +7 -5
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +52 -13
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ptcTimeout.cjs +56 -0
- package/dist/cjs/tools/ptcTimeout.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +27 -4
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +1 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/events.mjs +2 -1
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +5 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +3 -2
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/main.mjs +3 -3
- package/dist/esm/tools/BashExecutor.mjs +6 -3
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +26 -25
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +27 -3
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +131 -58
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +7 -5
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +54 -15
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ptcTimeout.mjs +50 -0
- package/dist/esm/tools/ptcTimeout.mjs.map +1 -0
- package/dist/types/agents/AgentContext.d.ts +3 -1
- package/dist/types/common/enum.d.ts +2 -1
- package/dist/types/tools/BashProgrammaticToolCalling.d.ts +4 -36
- package/dist/types/tools/CodeExecutor.d.ts +5 -0
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +18 -39
- package/dist/types/tools/ptcTimeout.d.ts +25 -0
- package/dist/types/types/tools.d.ts +8 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +32 -3
- package/src/agents/__tests__/AgentContext.test.ts +36 -3
- package/src/common/enum.ts +1 -0
- package/src/events.ts +4 -1
- package/src/graphs/MultiAgentGraph.ts +3 -2
- package/src/graphs/__tests__/composition.smoke.test.ts +84 -2
- package/src/tools/BashExecutor.ts +14 -3
- package/src/tools/BashProgrammaticToolCalling.ts +37 -25
- package/src/tools/CodeExecutor.ts +36 -2
- package/src/tools/ProgrammaticToolCalling.ts +206 -53
- package/src/tools/ToolNode.ts +3 -4
- package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +424 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +31 -1
- package/src/tools/local/LocalProgrammaticToolCalling.ts +94 -13
- package/src/tools/ptcTimeout.ts +89 -0
- package/src/types/tools.ts +12 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
|
|
3
|
+
import type { RequestInit } from 'node-fetch';
|
|
4
|
+
import type * as t from '@/types';
|
|
5
|
+
import {
|
|
6
|
+
createCodeExecutionTool,
|
|
7
|
+
resolveCodeApiAuthHeaders,
|
|
8
|
+
} from '../CodeExecutor';
|
|
9
|
+
import { createBashExecutionTool } from '../BashExecutor';
|
|
10
|
+
import {
|
|
11
|
+
createProgrammaticToolCallingTool,
|
|
12
|
+
fetchSessionFiles,
|
|
13
|
+
makeRequest,
|
|
14
|
+
} from '../ProgrammaticToolCalling';
|
|
15
|
+
import { createBashProgrammaticToolCallingTool } from '../BashProgrammaticToolCalling';
|
|
16
|
+
import {
|
|
17
|
+
clampCodeApiRunTimeoutMs,
|
|
18
|
+
createCodeApiRunTimeoutSchema,
|
|
19
|
+
} from '../ptcTimeout';
|
|
20
|
+
import {
|
|
21
|
+
createLocalProgrammaticToolCallingTool,
|
|
22
|
+
createLocalBashProgrammaticToolCallingTool,
|
|
23
|
+
} from '../local/LocalProgrammaticToolCalling';
|
|
24
|
+
|
|
25
|
+
jest.mock('node-fetch', () => ({
|
|
26
|
+
__esModule: true,
|
|
27
|
+
default: jest.fn(),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
type FetchMock = jest.MockedFunction<
|
|
31
|
+
(url: unknown, init?: unknown) => Promise<unknown>
|
|
32
|
+
>;
|
|
33
|
+
|
|
34
|
+
type CodeApiRequestBody = {
|
|
35
|
+
timeout?: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type TimeoutSchemaForTest = {
|
|
39
|
+
default: number;
|
|
40
|
+
maximum: number;
|
|
41
|
+
description: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type ToolSchemaForTest = {
|
|
45
|
+
properties: {
|
|
46
|
+
timeout: TimeoutSchemaForTest;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const fetchMock = fetch as unknown as FetchMock;
|
|
51
|
+
|
|
52
|
+
function requestBodyAt(callIndex: number): CodeApiRequestBody {
|
|
53
|
+
const init = fetchMock.mock.calls[callIndex]?.[1] as RequestInit;
|
|
54
|
+
return JSON.parse(init.body as string) as CodeApiRequestBody;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function timeoutSchemaForTest(toolSchema: unknown): TimeoutSchemaForTest {
|
|
58
|
+
return (toolSchema as ToolSchemaForTest).properties.timeout;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function jsonResponse(body: unknown): unknown {
|
|
62
|
+
return {
|
|
63
|
+
ok: true,
|
|
64
|
+
json: jest.fn(async () => body),
|
|
65
|
+
text: jest.fn(async () => JSON.stringify(body)),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function completedResponse(stdout = 'ok'): unknown {
|
|
70
|
+
return jsonResponse({
|
|
71
|
+
status: 'completed',
|
|
72
|
+
session_id: 'session_123',
|
|
73
|
+
stdout,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function errorResponse(status: number, body: string): unknown {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
status,
|
|
81
|
+
text: jest.fn(async () => body),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const toolDefs = [
|
|
86
|
+
{
|
|
87
|
+
name: 'lookup_user',
|
|
88
|
+
description: 'Lookup a user',
|
|
89
|
+
parameters: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
] as unknown as t.LCTool[];
|
|
95
|
+
|
|
96
|
+
function toolMap(): t.ToolMap {
|
|
97
|
+
return new Map([
|
|
98
|
+
[
|
|
99
|
+
'lookup_user',
|
|
100
|
+
{
|
|
101
|
+
name: 'lookup_user',
|
|
102
|
+
invoke: jest.fn(async () => ({ id: 'user_123' })),
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
]) as unknown as t.ToolMap;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
describe('CodeAPI auth header injection', () => {
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
fetchMock.mockReset();
|
|
111
|
+
fetchMock.mockResolvedValue(completedResponse());
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('resolves static and dynamic auth header params', async () => {
|
|
115
|
+
await expect(
|
|
116
|
+
resolveCodeApiAuthHeaders({ Authorization: 'Bearer static' })
|
|
117
|
+
).resolves.toEqual({
|
|
118
|
+
Authorization: 'Bearer static',
|
|
119
|
+
});
|
|
120
|
+
await expect(
|
|
121
|
+
resolveCodeApiAuthHeaders(async () => ({
|
|
122
|
+
Authorization: 'Bearer dynamic',
|
|
123
|
+
}))
|
|
124
|
+
).resolves.toEqual({
|
|
125
|
+
Authorization: 'Bearer dynamic',
|
|
126
|
+
});
|
|
127
|
+
await expect(resolveCodeApiAuthHeaders()).resolves.toEqual({});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('keeps the no-auth request path unchanged', async () => {
|
|
131
|
+
await makeRequest('https://code.example.com/exec/programmatic', {
|
|
132
|
+
code: 'print(1)',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
136
|
+
'https://code.example.com/exec/programmatic',
|
|
137
|
+
expect.objectContaining({
|
|
138
|
+
headers: expect.not.objectContaining({
|
|
139
|
+
Authorization: expect.any(String),
|
|
140
|
+
}),
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('forwards Authorization for direct code execution', async () => {
|
|
146
|
+
fetchMock.mockResolvedValueOnce(
|
|
147
|
+
jsonResponse({ session_id: 'session_123', stdout: '1\n' })
|
|
148
|
+
);
|
|
149
|
+
const tool = createCodeExecutionTool({
|
|
150
|
+
authHeaders: async () => ({ Authorization: 'Bearer code-token' }),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await tool.invoke({ lang: 'py', code: 'print(1)' });
|
|
154
|
+
|
|
155
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
156
|
+
expect.any(String),
|
|
157
|
+
expect.objectContaining({
|
|
158
|
+
headers: expect.objectContaining({
|
|
159
|
+
Authorization: 'Bearer code-token',
|
|
160
|
+
}),
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
expect(
|
|
164
|
+
JSON.parse((fetchMock.mock.calls[0]?.[1] as RequestInit).body as string)
|
|
165
|
+
).not.toHaveProperty('authHeaders');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('forwards Authorization for bash execution', async () => {
|
|
169
|
+
fetchMock.mockResolvedValueOnce(
|
|
170
|
+
jsonResponse({ session_id: 'session_123', stdout: '1\n' })
|
|
171
|
+
);
|
|
172
|
+
const tool = createBashExecutionTool({
|
|
173
|
+
authHeaders: { Authorization: 'Bearer bash-token' },
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await tool.invoke({ command: 'echo 1' });
|
|
177
|
+
|
|
178
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
179
|
+
expect.any(String),
|
|
180
|
+
expect.objectContaining({
|
|
181
|
+
headers: expect.objectContaining({
|
|
182
|
+
Authorization: 'Bearer bash-token',
|
|
183
|
+
}),
|
|
184
|
+
})
|
|
185
|
+
);
|
|
186
|
+
expect(
|
|
187
|
+
JSON.parse((fetchMock.mock.calls[0]?.[1] as RequestInit).body as string)
|
|
188
|
+
).not.toHaveProperty('authHeaders');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('includes the CodeAPI endpoint and response body on direct execution failures', async () => {
|
|
192
|
+
fetchMock.mockResolvedValueOnce(errorResponse(404, 'Cannot POST /exec'));
|
|
193
|
+
const tool = createBashExecutionTool();
|
|
194
|
+
|
|
195
|
+
await expect(tool.invoke({ command: 'echo 1' })).rejects.toThrow(
|
|
196
|
+
/CodeAPI request failed: POST .*\/exec returned 404, body: Cannot POST \/exec/
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('forwards Authorization on programmatic initial and continuation requests', async () => {
|
|
201
|
+
fetchMock
|
|
202
|
+
.mockResolvedValueOnce(
|
|
203
|
+
jsonResponse({
|
|
204
|
+
status: 'tool_call_required',
|
|
205
|
+
continuation_token: 'continue_123',
|
|
206
|
+
tool_calls: [{ id: 'call_1', name: 'lookup_user', input: {} }],
|
|
207
|
+
})
|
|
208
|
+
)
|
|
209
|
+
.mockResolvedValueOnce(completedResponse('done'));
|
|
210
|
+
|
|
211
|
+
const tool = createProgrammaticToolCallingTool({
|
|
212
|
+
authHeaders: () => ({ Authorization: 'Bearer ptc-token' }),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
await tool.invoke(
|
|
216
|
+
{ code: 'result = await lookup_user()\nprint(result)' },
|
|
217
|
+
{
|
|
218
|
+
toolCall: {
|
|
219
|
+
name: 'programmatic_code_execution',
|
|
220
|
+
args: {},
|
|
221
|
+
toolMap: toolMap(),
|
|
222
|
+
toolDefs,
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
228
|
+
for (const call of fetchMock.mock.calls) {
|
|
229
|
+
expect(call[1]).toEqual(
|
|
230
|
+
expect.objectContaining({
|
|
231
|
+
headers: expect.objectContaining({
|
|
232
|
+
Authorization: 'Bearer ptc-token',
|
|
233
|
+
}),
|
|
234
|
+
})
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('defaults programmatic timeout to the configured CodeAPI run cap', async () => {
|
|
240
|
+
const tool = createProgrammaticToolCallingTool({
|
|
241
|
+
runTimeoutMs: 15000,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
await tool.invoke(
|
|
245
|
+
{ code: 'result = await lookup_user()\nprint(result)' },
|
|
246
|
+
{
|
|
247
|
+
toolCall: {
|
|
248
|
+
name: 'programmatic_code_execution',
|
|
249
|
+
args: {},
|
|
250
|
+
toolMap: toolMap(),
|
|
251
|
+
toolDefs,
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
expect(requestBodyAt(0).timeout).toBe(15000);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('defaults bash programmatic timeout to the configured CodeAPI run cap', async () => {
|
|
260
|
+
const tool = createBashProgrammaticToolCallingTool({
|
|
261
|
+
runTimeoutMs: 15000,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await tool.invoke(
|
|
265
|
+
{ code: 'lookup_user "{}"' },
|
|
266
|
+
{
|
|
267
|
+
toolCall: {
|
|
268
|
+
name: 'bash_programmatic_code_execution',
|
|
269
|
+
args: {},
|
|
270
|
+
toolMap: toolMap(),
|
|
271
|
+
toolDefs,
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
expect(requestBodyAt(0).timeout).toBe(15000);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('describes the PTC timeout as a single sandbox run cap', () => {
|
|
280
|
+
const schema = createCodeApiRunTimeoutSchema(15000);
|
|
281
|
+
|
|
282
|
+
expect(clampCodeApiRunTimeoutMs(60000, 15000)).toBe(15000);
|
|
283
|
+
expect(schema.default).toBe(15000);
|
|
284
|
+
expect(schema.maximum).toBe(15000);
|
|
285
|
+
expect(schema.description).toContain('one sandbox run');
|
|
286
|
+
expect(schema.description).toContain('not the total multi-round-trip');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('keeps local programmatic timeout schemas aligned with local execution defaults', () => {
|
|
290
|
+
const pythonTimeout = timeoutSchemaForTest(
|
|
291
|
+
createLocalProgrammaticToolCallingTool().schema
|
|
292
|
+
);
|
|
293
|
+
const bashTimeout = timeoutSchemaForTest(
|
|
294
|
+
createLocalBashProgrammaticToolCallingTool().schema
|
|
295
|
+
);
|
|
296
|
+
const configuredTimeout = timeoutSchemaForTest(
|
|
297
|
+
createLocalProgrammaticToolCallingTool({ timeoutMs: 120000 }).schema
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
expect(pythonTimeout.default).toBe(60000);
|
|
301
|
+
expect(pythonTimeout.maximum).toBe(300000);
|
|
302
|
+
expect(pythonTimeout.description).toContain('local execution time');
|
|
303
|
+
expect(bashTimeout.default).toBe(60000);
|
|
304
|
+
expect(bashTimeout.maximum).toBe(300000);
|
|
305
|
+
expect(configuredTimeout.default).toBe(120000);
|
|
306
|
+
expect(configuredTimeout.maximum).toBe(300000);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('forwards Authorization for bash programmatic requests', async () => {
|
|
310
|
+
const tool = createBashProgrammaticToolCallingTool({
|
|
311
|
+
authHeaders: { Authorization: 'Bearer bash-ptc-token' },
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
await tool.invoke(
|
|
315
|
+
{ code: 'lookup_user "{}"' },
|
|
316
|
+
{
|
|
317
|
+
toolCall: {
|
|
318
|
+
name: 'bash_programmatic_code_execution',
|
|
319
|
+
args: {},
|
|
320
|
+
toolMap: toolMap(),
|
|
321
|
+
toolDefs,
|
|
322
|
+
},
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
327
|
+
expect.any(String),
|
|
328
|
+
expect.objectContaining({
|
|
329
|
+
headers: expect.objectContaining({
|
|
330
|
+
Authorization: 'Bearer bash-ptc-token',
|
|
331
|
+
}),
|
|
332
|
+
})
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('fetches session files with the CodeAPI resource scope and auth headers', async () => {
|
|
337
|
+
fetchMock.mockResolvedValueOnce(
|
|
338
|
+
jsonResponse([
|
|
339
|
+
{
|
|
340
|
+
id: 'file-1',
|
|
341
|
+
resource_id: 'skill-1',
|
|
342
|
+
storage_session_id: 'session_123',
|
|
343
|
+
name: 'skill/file.txt',
|
|
344
|
+
kind: 'skill',
|
|
345
|
+
version: 7,
|
|
346
|
+
},
|
|
347
|
+
])
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const files = await fetchSessionFiles(
|
|
351
|
+
'https://code.example.com',
|
|
352
|
+
'session_123',
|
|
353
|
+
{ kind: 'skill', id: 'skill-1', version: 7 },
|
|
354
|
+
undefined,
|
|
355
|
+
{ Authorization: 'Bearer files-token' }
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
expect(files).toHaveLength(1);
|
|
359
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
360
|
+
'https://code.example.com/files/session_123?detail=full&kind=skill&id=skill-1&version=7',
|
|
361
|
+
expect.objectContaining({
|
|
362
|
+
headers: expect.objectContaining({
|
|
363
|
+
Authorization: 'Bearer files-token',
|
|
364
|
+
}),
|
|
365
|
+
})
|
|
366
|
+
);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('fetches scoped session files with auth headers and no proxy placeholder', async () => {
|
|
370
|
+
fetchMock.mockResolvedValueOnce(jsonResponse([]));
|
|
371
|
+
|
|
372
|
+
await fetchSessionFiles(
|
|
373
|
+
'https://code.example.com',
|
|
374
|
+
'session_123',
|
|
375
|
+
{ kind: 'skill', id: 'skill-1', version: 7 },
|
|
376
|
+
{ Authorization: 'Bearer scoped-files-token' }
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
380
|
+
'https://code.example.com/files/session_123?detail=full&kind=skill&id=skill-1&version=7',
|
|
381
|
+
expect.objectContaining({
|
|
382
|
+
headers: expect.objectContaining({
|
|
383
|
+
Authorization: 'Bearer scoped-files-token',
|
|
384
|
+
}),
|
|
385
|
+
})
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('preserves the legacy fetchSessionFiles proxy/auth argument order', async () => {
|
|
390
|
+
fetchMock.mockResolvedValueOnce(
|
|
391
|
+
jsonResponse([
|
|
392
|
+
{
|
|
393
|
+
name: 'session_123/file-1.txt',
|
|
394
|
+
metadata: { 'original-filename': 'file.txt' },
|
|
395
|
+
},
|
|
396
|
+
])
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
const files = await fetchSessionFiles(
|
|
400
|
+
'https://code.example.com',
|
|
401
|
+
'session_123',
|
|
402
|
+
'',
|
|
403
|
+
{ Authorization: 'Bearer legacy-files-token' }
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
expect(files).toEqual([
|
|
407
|
+
{
|
|
408
|
+
storage_session_id: 'session_123',
|
|
409
|
+
kind: 'user',
|
|
410
|
+
id: 'file-1',
|
|
411
|
+
resource_id: 'file-1',
|
|
412
|
+
name: 'file.txt',
|
|
413
|
+
},
|
|
414
|
+
]);
|
|
415
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
416
|
+
'https://code.example.com/files/session_123?detail=full',
|
|
417
|
+
expect.objectContaining({
|
|
418
|
+
headers: expect.objectContaining({
|
|
419
|
+
Authorization: 'Bearer legacy-files-token',
|
|
420
|
+
}),
|
|
421
|
+
})
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* Unit tests for Programmatic Tool Calling.
|
|
4
4
|
* Tests manual invocation with mock tools and Code API responses.
|
|
5
5
|
*/
|
|
6
|
-
import { describe, it, expect, beforeEach } from '@jest/globals';
|
|
6
|
+
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
|
|
7
7
|
import type * as t from '@/types';
|
|
8
|
+
import { Constants } from '@/common';
|
|
8
9
|
import {
|
|
9
10
|
createProgrammaticToolCallingTool,
|
|
10
11
|
formatCompletedResponse,
|
|
@@ -56,6 +57,35 @@ describe('ProgrammaticToolCalling', () => {
|
|
|
56
57
|
});
|
|
57
58
|
});
|
|
58
59
|
|
|
60
|
+
it('marks bash PTC inner tool invocations with bash metadata', async () => {
|
|
61
|
+
const invoke = jest.fn<
|
|
62
|
+
(_input: unknown, _config: unknown) => Promise<{ ok: boolean }>
|
|
63
|
+
>(async () => ({ ok: true }));
|
|
64
|
+
const customTool = {
|
|
65
|
+
name: 'custom_tool',
|
|
66
|
+
invoke,
|
|
67
|
+
} as unknown as t.GenericTool;
|
|
68
|
+
const customToolMap: t.ToolMap = new Map([['custom_tool', customTool]]);
|
|
69
|
+
const toolCalls: t.PTCToolCall[] = [
|
|
70
|
+
{
|
|
71
|
+
id: 'call_001',
|
|
72
|
+
name: 'custom_tool',
|
|
73
|
+
input: { value: 1 },
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
await executeTools(
|
|
78
|
+
toolCalls,
|
|
79
|
+
customToolMap,
|
|
80
|
+
Constants.BASH_PROGRAMMATIC_TOOL_CALLING
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(invoke).toHaveBeenCalledWith(
|
|
84
|
+
{ value: 1 },
|
|
85
|
+
{ metadata: { [Constants.BASH_PROGRAMMATIC_TOOL_CALLING]: true } }
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
59
89
|
it('executes multiple tools in parallel', async () => {
|
|
60
90
|
const toolCalls: t.PTCToolCall[] = [
|
|
61
91
|
{
|
|
@@ -30,19 +30,100 @@ import {
|
|
|
30
30
|
import { Constants } from '@/common';
|
|
31
31
|
|
|
32
32
|
const DEFAULT_TIMEOUT = 60000;
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
const LOCAL_MIN_TIMEOUT = 1000;
|
|
34
|
+
const LOCAL_MAX_TIMEOUT = 300000;
|
|
35
|
+
|
|
36
|
+
type LocalTimeoutSchema = {
|
|
37
|
+
type: 'integer';
|
|
38
|
+
minimum: number;
|
|
39
|
+
maximum: number;
|
|
40
|
+
default: number;
|
|
41
|
+
description: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type LocalProgrammaticToolCallingJsonSchema = {
|
|
45
|
+
type: 'object';
|
|
46
|
+
properties: typeof ProgrammaticToolCallingSchema.properties & {
|
|
47
|
+
timeout: LocalTimeoutSchema;
|
|
37
48
|
lang: {
|
|
38
|
-
type: 'string'
|
|
39
|
-
enum: ['py', 'python', 'bash', 'sh']
|
|
40
|
-
default: 'bash'
|
|
41
|
-
description:
|
|
42
|
-
|
|
49
|
+
type: 'string';
|
|
50
|
+
enum: readonly ['py', 'python', 'bash', 'sh'];
|
|
51
|
+
default: 'bash';
|
|
52
|
+
description: string;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
required: readonly ['code'];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type LocalBashProgrammaticToolCallingJsonSchema = {
|
|
59
|
+
type: 'object';
|
|
60
|
+
properties: typeof BashProgrammaticToolCallingSchema.properties & {
|
|
61
|
+
timeout: LocalTimeoutSchema;
|
|
62
|
+
};
|
|
63
|
+
required: readonly ['code'];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function normalizeLocalTimeout(timeoutMs: number | undefined): number {
|
|
67
|
+
if (timeoutMs == null || !Number.isFinite(timeoutMs)) {
|
|
68
|
+
return DEFAULT_TIMEOUT;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return Math.max(LOCAL_MIN_TIMEOUT, Math.floor(timeoutMs));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatLocalTimeout(timeoutMs: number): string {
|
|
75
|
+
return timeoutMs % 1000 === 0
|
|
76
|
+
? `${timeoutMs / 1000} seconds`
|
|
77
|
+
: `${timeoutMs} milliseconds`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createLocalTimeoutSchema(timeoutMs?: number): LocalTimeoutSchema {
|
|
81
|
+
const defaultTimeout = normalizeLocalTimeout(timeoutMs);
|
|
82
|
+
const maxTimeout = Math.max(LOCAL_MAX_TIMEOUT, defaultTimeout);
|
|
83
|
+
const formattedDefault = formatLocalTimeout(defaultTimeout);
|
|
84
|
+
const formattedMax = formatLocalTimeout(maxTimeout);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
type: 'integer',
|
|
88
|
+
minimum: LOCAL_MIN_TIMEOUT,
|
|
89
|
+
maximum: maxTimeout,
|
|
90
|
+
default: defaultTimeout,
|
|
91
|
+
description:
|
|
92
|
+
'Maximum local execution time in milliseconds. ' +
|
|
93
|
+
`Default: ${formattedDefault}. Max: ${formattedMax}.`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function createLocalProgrammaticToolCallingSchema(
|
|
98
|
+
localConfig: t.LocalExecutionConfig = {}
|
|
99
|
+
): LocalProgrammaticToolCallingJsonSchema {
|
|
100
|
+
return {
|
|
101
|
+
...ProgrammaticToolCallingSchema,
|
|
102
|
+
properties: {
|
|
103
|
+
...ProgrammaticToolCallingSchema.properties,
|
|
104
|
+
timeout: createLocalTimeoutSchema(localConfig.timeoutMs),
|
|
105
|
+
lang: {
|
|
106
|
+
type: 'string',
|
|
107
|
+
enum: ['py', 'python', 'bash', 'sh'],
|
|
108
|
+
default: 'bash',
|
|
109
|
+
description:
|
|
110
|
+
'Local engine runtime for orchestration code. Defaults to bash; use py/python for Python orchestration.',
|
|
111
|
+
},
|
|
43
112
|
},
|
|
44
|
-
}
|
|
45
|
-
}
|
|
113
|
+
} as const;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function createLocalBashProgrammaticToolCallingSchema(
|
|
117
|
+
localConfig: t.LocalExecutionConfig = {}
|
|
118
|
+
): LocalBashProgrammaticToolCallingJsonSchema {
|
|
119
|
+
return {
|
|
120
|
+
...BashProgrammaticToolCallingSchema,
|
|
121
|
+
properties: {
|
|
122
|
+
...BashProgrammaticToolCallingSchema.properties,
|
|
123
|
+
timeout: createLocalTimeoutSchema(localConfig.timeoutMs),
|
|
124
|
+
},
|
|
125
|
+
} as const;
|
|
126
|
+
}
|
|
46
127
|
|
|
47
128
|
type ToolBridge = {
|
|
48
129
|
url: string;
|
|
@@ -582,7 +663,7 @@ export function createLocalProgrammaticToolCallingTool(
|
|
|
582
663
|
{
|
|
583
664
|
name: ProgrammaticToolCallingName,
|
|
584
665
|
description: `${ProgrammaticToolCallingDescription}\n\nLocal engine: runs bash by default, or Python when \`lang\` is \`py\` or \`python\`, on the host machine and calls tools through an in-process localhost bridge.`,
|
|
585
|
-
schema:
|
|
666
|
+
schema: createLocalProgrammaticToolCallingSchema(localConfig),
|
|
586
667
|
responseFormat: Constants.CONTENT_AND_ARTIFACT,
|
|
587
668
|
}
|
|
588
669
|
);
|
|
@@ -604,7 +685,7 @@ export function createLocalBashProgrammaticToolCallingTool(
|
|
|
604
685
|
{
|
|
605
686
|
name: Constants.BASH_PROGRAMMATIC_TOOL_CALLING,
|
|
606
687
|
description: `${BashProgrammaticToolCallingDescription}\n\nLocal engine: runs this bash orchestration code on the host machine and calls tools through an in-process localhost bridge.`,
|
|
607
|
-
schema:
|
|
688
|
+
schema: createLocalBashProgrammaticToolCallingSchema(localConfig),
|
|
608
689
|
responseFormat: Constants.CONTENT_AND_ARTIFACT,
|
|
609
690
|
}
|
|
610
691
|
);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { EnvVar } from '@/common';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_CODE_API_RUN_TIMEOUT_MS = 15_000;
|
|
4
|
+
export const MIN_CODE_API_RUN_TIMEOUT_MS = 1_000;
|
|
5
|
+
|
|
6
|
+
type TimeoutSchema = {
|
|
7
|
+
type: 'integer';
|
|
8
|
+
minimum: number;
|
|
9
|
+
maximum: number;
|
|
10
|
+
default: number;
|
|
11
|
+
description: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type ProgrammaticToolCallingJsonSchema = {
|
|
15
|
+
type: 'object';
|
|
16
|
+
properties: {
|
|
17
|
+
code: {
|
|
18
|
+
type: 'string';
|
|
19
|
+
minLength: number;
|
|
20
|
+
description: string;
|
|
21
|
+
};
|
|
22
|
+
timeout: TimeoutSchema;
|
|
23
|
+
};
|
|
24
|
+
required: readonly ['code'];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function normalizeTimeoutMs(value: number | undefined): number | undefined {
|
|
28
|
+
if (value == null || !Number.isFinite(value)) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return Math.max(MIN_CODE_API_RUN_TIMEOUT_MS, Math.floor(value));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseTimeoutMs(value: string | undefined): number | undefined {
|
|
36
|
+
if (value == null || value.trim() === '') {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return normalizeTimeoutMs(Number(value));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatTimeout(timeoutMs: number): string {
|
|
44
|
+
return timeoutMs % 1000 === 0
|
|
45
|
+
? `${timeoutMs / 1000} seconds`
|
|
46
|
+
: `${timeoutMs} milliseconds`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resolveCodeApiRunTimeoutMs(override?: number): number {
|
|
50
|
+
return (
|
|
51
|
+
normalizeTimeoutMs(override) ??
|
|
52
|
+
parseTimeoutMs(process.env[EnvVar.CODE_API_RUN_TIMEOUT_MS]) ??
|
|
53
|
+
DEFAULT_CODE_API_RUN_TIMEOUT_MS
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function clampCodeApiRunTimeoutMs(
|
|
58
|
+
timeoutMs: number | undefined,
|
|
59
|
+
maxRunTimeoutMs = resolveCodeApiRunTimeoutMs()
|
|
60
|
+
): number {
|
|
61
|
+
const normalizedMaxRunTimeoutMs =
|
|
62
|
+
normalizeTimeoutMs(maxRunTimeoutMs) ?? DEFAULT_CODE_API_RUN_TIMEOUT_MS;
|
|
63
|
+
const normalizedTimeoutMs = normalizeTimeoutMs(timeoutMs);
|
|
64
|
+
|
|
65
|
+
if (normalizedTimeoutMs == null) {
|
|
66
|
+
return normalizedMaxRunTimeoutMs;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return Math.min(normalizedTimeoutMs, normalizedMaxRunTimeoutMs);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createCodeApiRunTimeoutSchema(
|
|
73
|
+
maxRunTimeoutMs = resolveCodeApiRunTimeoutMs()
|
|
74
|
+
): TimeoutSchema {
|
|
75
|
+
const normalizedMaxRunTimeoutMs =
|
|
76
|
+
normalizeTimeoutMs(maxRunTimeoutMs) ?? DEFAULT_CODE_API_RUN_TIMEOUT_MS;
|
|
77
|
+
const formattedTimeout = formatTimeout(normalizedMaxRunTimeoutMs);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
type: 'integer',
|
|
81
|
+
minimum: MIN_CODE_API_RUN_TIMEOUT_MS,
|
|
82
|
+
maximum: normalizedMaxRunTimeoutMs,
|
|
83
|
+
default: normalizedMaxRunTimeoutMs,
|
|
84
|
+
description:
|
|
85
|
+
'Maximum wall-clock time in milliseconds for one sandbox run or replay iteration. ' +
|
|
86
|
+
'This is not the total multi-round-trip task budget. ' +
|
|
87
|
+
`Default: ${formattedTimeout}. Max: ${formattedTimeout}.`,
|
|
88
|
+
};
|
|
89
|
+
}
|