@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.
Files changed (75) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +26 -3
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +1 -0
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/events.cjs +2 -1
  6. package/dist/cjs/events.cjs.map +1 -1
  7. package/dist/cjs/graphs/Graph.cjs +5 -1
  8. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  9. package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -2
  10. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  11. package/dist/cjs/main.cjs +4 -0
  12. package/dist/cjs/main.cjs.map +1 -1
  13. package/dist/cjs/tools/BashExecutor.cjs +5 -2
  14. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  15. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +26 -24
  16. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  17. package/dist/cjs/tools/CodeExecutor.cjs +28 -2
  18. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  19. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +130 -56
  20. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  21. package/dist/cjs/tools/ToolNode.cjs +7 -5
  22. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  23. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +52 -13
  24. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -1
  25. package/dist/cjs/tools/ptcTimeout.cjs +56 -0
  26. package/dist/cjs/tools/ptcTimeout.cjs.map +1 -0
  27. package/dist/esm/agents/AgentContext.mjs +27 -4
  28. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  29. package/dist/esm/common/enum.mjs +1 -0
  30. package/dist/esm/common/enum.mjs.map +1 -1
  31. package/dist/esm/events.mjs +2 -1
  32. package/dist/esm/events.mjs.map +1 -1
  33. package/dist/esm/graphs/Graph.mjs +5 -1
  34. package/dist/esm/graphs/Graph.mjs.map +1 -1
  35. package/dist/esm/graphs/MultiAgentGraph.mjs +3 -2
  36. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  37. package/dist/esm/main.mjs +3 -3
  38. package/dist/esm/tools/BashExecutor.mjs +6 -3
  39. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  40. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +26 -25
  41. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  42. package/dist/esm/tools/CodeExecutor.mjs +27 -3
  43. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  44. package/dist/esm/tools/ProgrammaticToolCalling.mjs +131 -58
  45. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  46. package/dist/esm/tools/ToolNode.mjs +7 -5
  47. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  48. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +54 -15
  49. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -1
  50. package/dist/esm/tools/ptcTimeout.mjs +50 -0
  51. package/dist/esm/tools/ptcTimeout.mjs.map +1 -0
  52. package/dist/types/agents/AgentContext.d.ts +3 -1
  53. package/dist/types/common/enum.d.ts +2 -1
  54. package/dist/types/tools/BashProgrammaticToolCalling.d.ts +4 -36
  55. package/dist/types/tools/CodeExecutor.d.ts +5 -0
  56. package/dist/types/tools/ProgrammaticToolCalling.d.ts +18 -39
  57. package/dist/types/tools/ptcTimeout.d.ts +25 -0
  58. package/dist/types/types/tools.d.ts +8 -0
  59. package/package.json +1 -1
  60. package/src/agents/AgentContext.ts +32 -3
  61. package/src/agents/__tests__/AgentContext.test.ts +36 -3
  62. package/src/common/enum.ts +1 -0
  63. package/src/events.ts +4 -1
  64. package/src/graphs/MultiAgentGraph.ts +3 -2
  65. package/src/graphs/__tests__/composition.smoke.test.ts +84 -2
  66. package/src/tools/BashExecutor.ts +14 -3
  67. package/src/tools/BashProgrammaticToolCalling.ts +37 -25
  68. package/src/tools/CodeExecutor.ts +36 -2
  69. package/src/tools/ProgrammaticToolCalling.ts +206 -53
  70. package/src/tools/ToolNode.ts +3 -4
  71. package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +424 -0
  72. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +31 -1
  73. package/src/tools/local/LocalProgrammaticToolCalling.ts +94 -13
  74. package/src/tools/ptcTimeout.ts +89 -0
  75. 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 LocalProgrammaticToolCallingSchema = {
34
- ...ProgrammaticToolCallingSchema,
35
- properties: {
36
- ...ProgrammaticToolCallingSchema.properties,
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
- 'Local engine runtime for orchestration code. Defaults to bash; use py/python for Python orchestration.',
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
- } as const;
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: LocalProgrammaticToolCallingSchema,
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: BashProgrammaticToolCallingSchema,
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
+ }