@librechat/agents 3.1.84 → 3.1.86

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 (56) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +7 -2
  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/graphs/Graph.cjs +5 -1
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -2
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  9. package/dist/cjs/main.cjs +2 -0
  10. package/dist/cjs/main.cjs.map +1 -1
  11. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +23 -21
  12. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  13. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +23 -22
  14. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  15. package/dist/cjs/tools/ToolNode.cjs +4 -1
  16. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  17. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +52 -13
  18. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -1
  19. package/dist/cjs/tools/ptcTimeout.cjs +56 -0
  20. package/dist/cjs/tools/ptcTimeout.cjs.map +1 -0
  21. package/dist/esm/agents/AgentContext.mjs +7 -2
  22. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  23. package/dist/esm/common/enum.mjs +1 -0
  24. package/dist/esm/common/enum.mjs.map +1 -1
  25. package/dist/esm/graphs/Graph.mjs +5 -1
  26. package/dist/esm/graphs/Graph.mjs.map +1 -1
  27. package/dist/esm/graphs/MultiAgentGraph.mjs +3 -2
  28. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  29. package/dist/esm/main.mjs +2 -2
  30. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +23 -22
  31. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  32. package/dist/esm/tools/ProgrammaticToolCalling.mjs +23 -23
  33. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  34. package/dist/esm/tools/ToolNode.mjs +4 -1
  35. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  36. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +54 -15
  37. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -1
  38. package/dist/esm/tools/ptcTimeout.mjs +50 -0
  39. package/dist/esm/tools/ptcTimeout.mjs.map +1 -0
  40. package/dist/types/common/enum.d.ts +2 -1
  41. package/dist/types/tools/BashProgrammaticToolCalling.d.ts +4 -36
  42. package/dist/types/tools/ProgrammaticToolCalling.d.ts +4 -36
  43. package/dist/types/tools/ptcTimeout.d.ts +25 -0
  44. package/dist/types/types/tools.d.ts +2 -0
  45. package/package.json +1 -1
  46. package/src/agents/AgentContext.ts +7 -2
  47. package/src/agents/__tests__/AgentContext.test.ts +254 -5
  48. package/src/common/enum.ts +1 -0
  49. package/src/graphs/MultiAgentGraph.ts +3 -2
  50. package/src/graphs/__tests__/composition.smoke.test.ts +84 -2
  51. package/src/tools/BashProgrammaticToolCalling.ts +31 -22
  52. package/src/tools/ProgrammaticToolCalling.ts +31 -23
  53. package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +103 -0
  54. package/src/tools/local/LocalProgrammaticToolCalling.ts +94 -13
  55. package/src/tools/ptcTimeout.ts +89 -0
  56. package/src/types/tools.ts +2 -0
@@ -368,7 +368,7 @@ describe('AgentContext', () => {
368
368
  expect(result[4].content).toBe('Latest');
369
369
  });
370
370
 
371
- it('does not place Anthropic dynamic instructions between tool calls and results', async () => {
371
+ it('keeps Anthropic dynamic instructions attached to the latest user turn during tool follow-up', async () => {
372
372
  const ctx = createBasicContext({
373
373
  agentConfig: {
374
374
  provider: Providers.ANTHROPIC,
@@ -401,10 +401,115 @@ describe('AgentContext', () => {
401
401
  }),
402
402
  ]);
403
403
 
404
- expect(result[1].content).toBe('Use the tool');
405
- expect((result[2] as AIMessage).tool_calls?.[0]?.id).toBe('call_1');
406
- expect(result[3].getType()).toBe('tool');
407
- expect(result[4].content).toBe('Dynamic instructions');
404
+ expect(result[1].content).toBe('Dynamic instructions');
405
+ expect(result[2].content).toBe('Use the tool');
406
+ expect((result[3] as AIMessage).tool_calls?.[0]?.id).toBe('call_1');
407
+ expect(result[4].getType()).toBe('tool');
408
+ });
409
+
410
+ it('keeps Anthropic stable history cacheable before dynamic tool-follow-up context', async () => {
411
+ const ctx = createBasicContext({
412
+ agentConfig: {
413
+ provider: Providers.ANTHROPIC,
414
+ clientOptions: {
415
+ model: 'claude-3-5-sonnet',
416
+ promptCache: true,
417
+ },
418
+ instructions: 'Stable instructions',
419
+ additional_instructions: 'Dynamic instructions',
420
+ },
421
+ });
422
+
423
+ const result = await ctx.systemRunnable!.invoke([
424
+ new HumanMessage('Earlier'),
425
+ new AIMessage('Earlier assistant response'),
426
+ new HumanMessage('Use the tool'),
427
+ new AIMessage({
428
+ content: '',
429
+ tool_calls: [
430
+ {
431
+ id: 'call_1',
432
+ name: 'calculator',
433
+ args: { expression: '2+2' },
434
+ type: 'tool_call',
435
+ },
436
+ ],
437
+ }),
438
+ new ToolMessage({
439
+ content: '4',
440
+ name: 'calculator',
441
+ tool_call_id: 'call_1',
442
+ }),
443
+ ]);
444
+ const stableAssistant = result[2].content as TestSystemContentBlock[];
445
+
446
+ expect(result[1].content).toBe('Earlier');
447
+ expect(stableAssistant[0]).toMatchObject({
448
+ type: 'text',
449
+ text: 'Earlier assistant response',
450
+ cache_control: { type: 'ephemeral' },
451
+ });
452
+ expect(result[3].content).toBe('Dynamic instructions');
453
+ expect(result[4].content).toBe('Use the tool');
454
+ expect((result[5] as AIMessage).tool_calls?.[0]?.id).toBe('call_1');
455
+ expect(result[6].getType()).toBe('tool');
456
+ });
457
+
458
+ it('keeps Anthropic dynamic context on latest no-tool turn after mixed prior turns', async () => {
459
+ const ctx = createBasicContext({
460
+ agentConfig: {
461
+ provider: Providers.ANTHROPIC,
462
+ clientOptions: {
463
+ model: 'claude-3-5-sonnet',
464
+ promptCache: true,
465
+ },
466
+ instructions: 'Stable instructions',
467
+ additional_instructions: 'Dynamic instructions',
468
+ },
469
+ });
470
+
471
+ const result = await ctx.systemRunnable!.invoke([
472
+ new HumanMessage('First turn, no tools'),
473
+ new AIMessage('First assistant response'),
474
+ new HumanMessage('Use the tool'),
475
+ new AIMessage({
476
+ content: '',
477
+ tool_calls: [
478
+ {
479
+ id: 'call_1',
480
+ name: 'calculator',
481
+ args: { expression: '2+2' },
482
+ type: 'tool_call',
483
+ },
484
+ ],
485
+ }),
486
+ new ToolMessage({
487
+ content: '4',
488
+ name: 'calculator',
489
+ tool_call_id: 'call_1',
490
+ }),
491
+ new AIMessage('4'),
492
+ new HumanMessage('Now answer without tools'),
493
+ ]);
494
+ const firstAssistant = result[2].content as TestSystemContentBlock[];
495
+ const toolAnswer = result[6].content as TestSystemContentBlock[];
496
+
497
+ expect(result[1].content).toBe('First turn, no tools');
498
+ expect(firstAssistant[0]).toMatchObject({
499
+ type: 'text',
500
+ text: 'First assistant response',
501
+ cache_control: { type: 'ephemeral' },
502
+ });
503
+ expect(result[3].content).toBe('Use the tool');
504
+ expect((result[4] as AIMessage).tool_calls?.[0]?.id).toBe('call_1');
505
+ expect(result[5].getType()).toBe('tool');
506
+ expect(toolAnswer[0]).toMatchObject({
507
+ type: 'text',
508
+ text: '4',
509
+ cache_control: { type: 'ephemeral' },
510
+ });
511
+ expect(result[7].content).toBe('Dynamic instructions');
512
+ expect(result[8].content).toBe('Now answer without tools');
408
513
  });
409
514
 
410
515
  it('caches stable OpenRouter history before dynamic instructions', async () => {
@@ -437,6 +542,150 @@ describe('AgentContext', () => {
437
542
  expect(result[4].content).toBe('Latest');
438
543
  });
439
544
 
545
+ it('keeps OpenRouter opening user message before dynamic tool-follow-up context', async () => {
546
+ const ctx = createBasicContext({
547
+ agentConfig: {
548
+ provider: Providers.OPENROUTER,
549
+ clientOptions: {
550
+ model: 'anthropic/claude-haiku-4.5',
551
+ promptCache: true,
552
+ },
553
+ instructions: 'Stable instructions',
554
+ additional_instructions: 'Dynamic instructions',
555
+ },
556
+ });
557
+
558
+ const result = await ctx.systemRunnable!.invoke([
559
+ new HumanMessage('Use the tool'),
560
+ new AIMessage({
561
+ content: '',
562
+ tool_calls: [
563
+ {
564
+ id: 'call_1',
565
+ name: 'calculator',
566
+ args: { expression: '2+2' },
567
+ type: 'tool_call',
568
+ },
569
+ ],
570
+ }),
571
+ new ToolMessage({
572
+ content: '4',
573
+ name: 'calculator',
574
+ tool_call_id: 'call_1',
575
+ }),
576
+ ]);
577
+
578
+ expect(result[1].content).toBe('Use the tool');
579
+ expect(result[2].content).toBe('Dynamic instructions');
580
+ expect((result[3] as AIMessage).tool_calls?.[0]?.id).toBe('call_1');
581
+ expect(result[4].getType()).toBe('tool');
582
+ });
583
+
584
+ it('keeps OpenRouter stable history cacheable before dynamic tool-follow-up context', async () => {
585
+ const ctx = createBasicContext({
586
+ agentConfig: {
587
+ provider: Providers.OPENROUTER,
588
+ clientOptions: {
589
+ model: 'anthropic/claude-haiku-4.5',
590
+ promptCache: true,
591
+ },
592
+ instructions: 'Stable instructions',
593
+ additional_instructions: 'Dynamic instructions',
594
+ },
595
+ });
596
+
597
+ const result = await ctx.systemRunnable!.invoke([
598
+ new HumanMessage('Earlier'),
599
+ new AIMessage('Earlier assistant response'),
600
+ new HumanMessage('Use the tool'),
601
+ new AIMessage({
602
+ content: '',
603
+ tool_calls: [
604
+ {
605
+ id: 'call_1',
606
+ name: 'calculator',
607
+ args: { expression: '2+2' },
608
+ type: 'tool_call',
609
+ },
610
+ ],
611
+ }),
612
+ new ToolMessage({
613
+ content: '4',
614
+ name: 'calculator',
615
+ tool_call_id: 'call_1',
616
+ }),
617
+ ]);
618
+ const stableAssistant = result[2].content as TestSystemContentBlock[];
619
+
620
+ expect(result[1].content).toBe('Earlier');
621
+ expect(stableAssistant[0]).toMatchObject({
622
+ type: 'text',
623
+ text: 'Earlier assistant response',
624
+ cache_control: { type: 'ephemeral' },
625
+ });
626
+ expect(result[3].content).toBe('Dynamic instructions');
627
+ expect(result[4].content).toBe('Use the tool');
628
+ expect((result[5] as AIMessage).tool_calls?.[0]?.id).toBe('call_1');
629
+ expect(result[6].getType()).toBe('tool');
630
+ });
631
+
632
+ it('keeps OpenRouter dynamic context on latest no-tool turn after mixed prior turns', async () => {
633
+ const ctx = createBasicContext({
634
+ agentConfig: {
635
+ provider: Providers.OPENROUTER,
636
+ clientOptions: {
637
+ model: 'anthropic/claude-haiku-4.5',
638
+ promptCache: true,
639
+ },
640
+ instructions: 'Stable instructions',
641
+ additional_instructions: 'Dynamic instructions',
642
+ },
643
+ });
644
+
645
+ const result = await ctx.systemRunnable!.invoke([
646
+ new HumanMessage('First turn, no tools'),
647
+ new AIMessage('First assistant response'),
648
+ new HumanMessage('Use the tool'),
649
+ new AIMessage({
650
+ content: '',
651
+ tool_calls: [
652
+ {
653
+ id: 'call_1',
654
+ name: 'calculator',
655
+ args: { expression: '2+2' },
656
+ type: 'tool_call',
657
+ },
658
+ ],
659
+ }),
660
+ new ToolMessage({
661
+ content: '4',
662
+ name: 'calculator',
663
+ tool_call_id: 'call_1',
664
+ }),
665
+ new AIMessage('4'),
666
+ new HumanMessage('Now answer without tools'),
667
+ ]);
668
+ const firstAssistant = result[2].content as TestSystemContentBlock[];
669
+ const toolAnswer = result[6].content as TestSystemContentBlock[];
670
+
671
+ expect(result[1].content).toBe('First turn, no tools');
672
+ expect(firstAssistant[0]).toMatchObject({
673
+ type: 'text',
674
+ text: 'First assistant response',
675
+ cache_control: { type: 'ephemeral' },
676
+ });
677
+ expect(result[3].content).toBe('Use the tool');
678
+ expect((result[4] as AIMessage).tool_calls?.[0]?.id).toBe('call_1');
679
+ expect(result[5].getType()).toBe('tool');
680
+ expect(toolAnswer[0]).toMatchObject({
681
+ type: 'text',
682
+ text: '4',
683
+ cache_control: { type: 'ephemeral' },
684
+ });
685
+ expect(result[7].content).toBe('Dynamic instructions');
686
+ expect(result[8].content).toBe('Now answer without tools');
687
+ });
688
+
440
689
  it('adds OpenRouter body cache points when there is no dynamic tail', async () => {
441
690
  const ctx = createBasicContext({
442
691
  agentConfig: {
@@ -261,4 +261,5 @@ export enum TitleMethod {
261
261
 
262
262
  export enum EnvVar {
263
263
  CODE_BASEURL = 'LIBRECHAT_CODE_BASEURL',
264
+ CODE_API_RUN_TIMEOUT_MS = 'CODE_API_RUN_TIMEOUT_MS',
264
265
  }
@@ -1074,10 +1074,11 @@ export class MultiAgentGraph extends StandardGraph {
1074
1074
  * to pass filtered messages + prompt to the destination agent
1075
1075
  */
1076
1076
  const filteredMessages = state.messages.slice(0, this.startIndex);
1077
+ const promptMessage = new HumanMessage(promptText);
1077
1078
  return {
1078
- messages: [new HumanMessage(promptText)],
1079
+ messages: [promptMessage],
1079
1080
  agentMessages: messagesStateReducer(filteredMessages, [
1080
- new HumanMessage(promptText),
1081
+ promptMessage,
1081
1082
  ]),
1082
1083
  };
1083
1084
  }
@@ -1,11 +1,17 @@
1
- import { HumanMessage } from '@langchain/core/messages';
2
- import type { ToolCall } from '@langchain/core/messages/tool';
1
+ import { HumanMessage, getBufferString } from '@langchain/core/messages';
2
+ import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
3
3
  import type { RunnableConfig } from '@langchain/core/runnables';
4
+ import type { ChatGenerationChunk } from '@langchain/core/outputs';
5
+ import type { ToolCall } from '@langchain/core/messages/tool';
6
+ import type { BaseMessage } from '@langchain/core/messages';
4
7
  import type * as t from '@/types';
5
8
  import { MultiAgentGraph } from '../MultiAgentGraph';
6
9
  import { Constants, Providers } from '@/common';
10
+ import { FakeChatModel } from '@/llm/fake';
7
11
  import { StandardGraph } from '../Graph';
8
12
 
13
+ const CHAIN_PROMPT_PREFIX = 'Previous context:\n';
14
+
9
15
  const makeAgent = (agentId: string): t.AgentInputs => ({
10
16
  agentId,
11
17
  provider: Providers.OPENAI,
@@ -29,6 +35,36 @@ const getAiContents = (messages: t.BaseGraphState['messages']): string[] =>
29
35
  .map((message) => message.content)
30
36
  .filter((content): content is string => typeof content === 'string');
31
37
 
38
+ const getChainPromptContent = (messages: BaseMessage[]): string => {
39
+ const promptMessage = messages.find(
40
+ (message) =>
41
+ message.getType() === 'human' &&
42
+ typeof message.content === 'string' &&
43
+ message.content.startsWith(CHAIN_PROMPT_PREFIX)
44
+ );
45
+ if (promptMessage == null || typeof promptMessage.content !== 'string') {
46
+ throw new Error('Expected chain prompt message');
47
+ }
48
+ return promptMessage.content;
49
+ };
50
+
51
+ class CapturingChatModel extends FakeChatModel {
52
+ readonly invocations: BaseMessage[][] = [];
53
+
54
+ constructor(responses: string[]) {
55
+ super({ responses });
56
+ }
57
+
58
+ override async *_streamResponseChunks(
59
+ messages: BaseMessage[],
60
+ options: this['ParsedCallOptions'],
61
+ runManager?: CallbackManagerForLLMRun
62
+ ): AsyncGenerator<ChatGenerationChunk> {
63
+ this.invocations.push(messages);
64
+ yield* super._streamResponseChunks(messages, options, runManager);
65
+ }
66
+ }
67
+
32
68
  const expectCompiledWorkflow = (
33
69
  workflow: t.CompiledWorkflow | t.CompiledMultiAgentWorkflow
34
70
  ): void => {
@@ -119,6 +155,52 @@ describe('LangGraph composition smoke tests', () => {
119
155
  expect(getAiContents(result.messages)).toEqual(['from A', 'from B']);
120
156
  });
121
157
 
158
+ it('does not duplicate excludeResults chain prompt history for downstream agents', async () => {
159
+ const model = new CapturingChatModel(['from A', 'from B', 'from C']);
160
+ const prompt = (messages: BaseMessage[], startIndex: number): string =>
161
+ `${CHAIN_PROMPT_PREFIX}${getBufferString(messages.slice(startIndex))}`;
162
+ const graph = new MultiAgentGraph({
163
+ runId: 'exclude-results-chain-smoke',
164
+ agents: [makeAgent('A'), makeAgent('B'), makeAgent('C')],
165
+ edges: [
166
+ {
167
+ from: 'A',
168
+ to: 'B',
169
+ edgeType: 'direct',
170
+ prompt,
171
+ excludeResults: true,
172
+ },
173
+ {
174
+ from: 'B',
175
+ to: 'C',
176
+ edgeType: 'direct',
177
+ prompt,
178
+ excludeResults: true,
179
+ },
180
+ ],
181
+ });
182
+ graph.overrideModel = model;
183
+
184
+ const result = await graph
185
+ .createWorkflow()
186
+ .invoke(
187
+ { messages: [new HumanMessage('start')] },
188
+ makeConfig('exclude-results-chain-smoke')
189
+ );
190
+
191
+ expect(getAiContents(result.messages)).toEqual([
192
+ 'from A',
193
+ 'from B',
194
+ 'from C',
195
+ ]);
196
+ expect(model.invocations).toHaveLength(3);
197
+
198
+ const downstreamPrompt = getChainPromptContent(model.invocations[2]);
199
+ const previousPromptCount =
200
+ downstreamPrompt.match(/Human: Previous context:/g)?.length ?? 0;
201
+ expect(previousPromptCount).toBe(1);
202
+ });
203
+
122
204
  it('compiles and invokes a handoff edge using graph-managed transfer tools', async () => {
123
205
  const transferToolCall: ToolCall = {
124
206
  id: 'call_transfer_to_B',
@@ -1,6 +1,7 @@
1
1
  import { config } from 'dotenv';
2
2
  import { tool, DynamicStructuredTool } from '@langchain/core/tools';
3
3
  import type { ToolCall } from '@langchain/core/messages/tool';
4
+ import type { ProgrammaticToolCallingJsonSchema } from './ptcTimeout';
4
5
  import type * as t from '@/types';
5
6
  import {
6
7
  makeRequest,
@@ -8,6 +9,11 @@ import {
8
9
  formatCompletedResponse,
9
10
  } from './ProgrammaticToolCalling';
10
11
  import { getCodeBaseURL } from './CodeExecutor';
12
+ import {
13
+ clampCodeApiRunTimeoutMs,
14
+ createCodeApiRunTimeoutSchema,
15
+ resolveCodeApiRunTimeoutMs,
16
+ } from './ptcTimeout';
11
17
  import { Constants } from '@/common';
12
18
 
13
19
  config();
@@ -17,7 +23,7 @@ config();
17
23
  // ============================================================================
18
24
 
19
25
  const DEFAULT_MAX_ROUND_TRIPS = 20;
20
- const DEFAULT_TIMEOUT = 60000;
26
+ const DEFAULT_RUN_TIMEOUT_MS = resolveCodeApiRunTimeoutMs();
21
27
 
22
28
  /** Bash reserved words that get `_tool` suffix when used as function names */
23
29
  const BASH_RESERVED = new Set([
@@ -60,7 +66,8 @@ const CORE_RULES = `Rules:
60
66
  - Tools are pre-defined as bash functions—DO NOT redefine them
61
67
  - Each tool function accepts a JSON string argument
62
68
  - Only echo/printf output returns to the model
63
- - Generated files are automatically available in /mnt/data/ for subsequent executions`;
69
+ - Generated files are automatically available in /mnt/data/ for subsequent executions
70
+ - timeout caps one sandbox run/replay iteration, not the total multi-round-trip workflow`;
64
71
 
65
72
  const ADDITIONAL_RULES =
66
73
  '- Tool names normalized: hyphens→underscores, reserved words get `_tool` suffix';
@@ -92,25 +99,25 @@ ${CORE_RULES}`;
92
99
  // Schema
93
100
  // ============================================================================
94
101
 
95
- export const BashProgrammaticToolCallingSchema = {
96
- type: 'object',
97
- properties: {
98
- code: {
99
- type: 'string',
100
- minLength: 1,
101
- description: CODE_PARAM_DESCRIPTION,
102
- },
103
- timeout: {
104
- type: 'integer',
105
- minimum: 1000,
106
- maximum: 300000,
107
- default: DEFAULT_TIMEOUT,
108
- description:
109
- 'Maximum execution time in milliseconds. Default: 60 seconds. Max: 5 minutes.',
102
+ export function createBashProgrammaticToolCallingSchema(
103
+ maxRunTimeoutMs = DEFAULT_RUN_TIMEOUT_MS
104
+ ): ProgrammaticToolCallingJsonSchema {
105
+ return {
106
+ type: 'object',
107
+ properties: {
108
+ code: {
109
+ type: 'string',
110
+ minLength: 1,
111
+ description: CODE_PARAM_DESCRIPTION,
112
+ },
113
+ timeout: createCodeApiRunTimeoutSchema(maxRunTimeoutMs),
110
114
  },
111
- },
112
- required: ['code'],
113
- } as const;
115
+ required: ['code'],
116
+ } as const;
117
+ }
118
+
119
+ export const BashProgrammaticToolCallingSchema =
120
+ createBashProgrammaticToolCallingSchema();
114
121
 
115
122
  export const BashProgrammaticToolCallingName =
116
123
  Constants.BASH_PROGRAMMATIC_TOOL_CALLING;
@@ -242,6 +249,7 @@ export function createBashProgrammaticToolCallingTool(
242
249
  ): DynamicStructuredTool {
243
250
  const baseUrl = initParams.baseUrl ?? getCodeBaseURL();
244
251
  const maxRoundTrips = initParams.maxRoundTrips ?? DEFAULT_MAX_ROUND_TRIPS;
252
+ const maxRunTimeoutMs = resolveCodeApiRunTimeoutMs(initParams.runTimeoutMs);
245
253
  const proxy = initParams.proxy ?? process.env.PROXY;
246
254
  const debug = initParams.debug ?? process.env.BASH_PTC_DEBUG === 'true';
247
255
  const EXEC_ENDPOINT = `${baseUrl}/exec/programmatic`;
@@ -249,7 +257,8 @@ export function createBashProgrammaticToolCallingTool(
249
257
  return tool(
250
258
  async (rawParams, config) => {
251
259
  const params = rawParams as { code: string; timeout?: number };
252
- const { code, timeout = DEFAULT_TIMEOUT } = params;
260
+ const { code } = params;
261
+ const timeout = clampCodeApiRunTimeoutMs(params.timeout, maxRunTimeoutMs);
253
262
 
254
263
  const toolCall = (config.toolCall ?? {}) as ToolCall &
255
264
  Partial<t.ProgrammaticCache> & {
@@ -382,7 +391,7 @@ export function createBashProgrammaticToolCallingTool(
382
391
  {
383
392
  name: Constants.BASH_PROGRAMMATIC_TOOL_CALLING,
384
393
  description: BashProgrammaticToolCallingDescription,
385
- schema: BashProgrammaticToolCallingSchema,
394
+ schema: createBashProgrammaticToolCallingSchema(maxRunTimeoutMs),
386
395
  responseFormat: Constants.CONTENT_AND_ARTIFACT,
387
396
  }
388
397
  );
@@ -4,6 +4,7 @@ import fetch, { RequestInit } from 'node-fetch';
4
4
  import { HttpsProxyAgent } from 'https-proxy-agent';
5
5
  import { tool, DynamicStructuredTool } from '@langchain/core/tools';
6
6
  import type { ToolCall } from '@langchain/core/messages/tool';
7
+ import type { ProgrammaticToolCallingJsonSchema } from './ptcTimeout';
7
8
  import type * as t from '@/types';
8
9
  import {
9
10
  buildCodeApiHttpErrorMessage,
@@ -11,6 +12,11 @@ import {
11
12
  getCodeBaseURL,
12
13
  resolveCodeApiAuthHeaders,
13
14
  } from './CodeExecutor';
15
+ import {
16
+ clampCodeApiRunTimeoutMs,
17
+ createCodeApiRunTimeoutSchema,
18
+ resolveCodeApiRunTimeoutMs,
19
+ } from './ptcTimeout';
14
20
  import { Constants } from '@/common';
15
21
 
16
22
  config();
@@ -18,8 +24,7 @@ config();
18
24
  /** Default max round-trips to prevent infinite loops */
19
25
  const DEFAULT_MAX_ROUND_TRIPS = 20;
20
26
 
21
- /** Default execution timeout in milliseconds */
22
- const DEFAULT_TIMEOUT = 60000;
27
+ const DEFAULT_RUN_TIMEOUT_MS = resolveCodeApiRunTimeoutMs();
23
28
 
24
29
  // ============================================================================
25
30
  // Description Components (Single Source of Truth)
@@ -35,7 +40,8 @@ const CORE_RULES = `Rules:
35
40
  - Just write code with await—auto-wrapped in async context
36
41
  - DO NOT define async def main() or call asyncio.run()
37
42
  - Tools are pre-defined—DO NOT write function definitions
38
- - Only print() output returns to the model`;
43
+ - Only print() output returns to the model
44
+ - timeout caps one sandbox run/replay iteration, not the total multi-round-trip workflow`;
39
45
 
40
46
  const ADDITIONAL_RULES = `- Generated files are automatically available in /mnt/data/ for subsequent executions
41
47
  - Tool names normalized: hyphens→underscores, keywords get \`_tool\` suffix`;
@@ -68,25 +74,25 @@ ${EXAMPLES}
68
74
 
69
75
  ${CORE_RULES}`;
70
76
 
71
- export const ProgrammaticToolCallingSchema = {
72
- type: 'object',
73
- properties: {
74
- code: {
75
- type: 'string',
76
- minLength: 1,
77
- description: CODE_PARAM_DESCRIPTION,
78
- },
79
- timeout: {
80
- type: 'integer',
81
- minimum: 1000,
82
- maximum: 300000,
83
- default: DEFAULT_TIMEOUT,
84
- description:
85
- 'Maximum execution time in milliseconds. Default: 60 seconds. Max: 5 minutes.',
77
+ export function createProgrammaticToolCallingSchema(
78
+ maxRunTimeoutMs = DEFAULT_RUN_TIMEOUT_MS
79
+ ): ProgrammaticToolCallingJsonSchema {
80
+ return {
81
+ type: 'object',
82
+ properties: {
83
+ code: {
84
+ type: 'string',
85
+ minLength: 1,
86
+ description: CODE_PARAM_DESCRIPTION,
87
+ },
88
+ timeout: createCodeApiRunTimeoutSchema(maxRunTimeoutMs),
86
89
  },
87
- },
88
- required: ['code'],
89
- } as const;
90
+ required: ['code'],
91
+ } as const;
92
+ }
93
+
94
+ export const ProgrammaticToolCallingSchema =
95
+ createProgrammaticToolCallingSchema();
90
96
 
91
97
  export const ProgrammaticToolCallingName = Constants.PROGRAMMATIC_TOOL_CALLING;
92
98
 
@@ -731,6 +737,7 @@ export function createProgrammaticToolCallingTool(
731
737
  ): DynamicStructuredTool {
732
738
  const baseUrl = initParams.baseUrl ?? getCodeBaseURL();
733
739
  const maxRoundTrips = initParams.maxRoundTrips ?? DEFAULT_MAX_ROUND_TRIPS;
740
+ const maxRunTimeoutMs = resolveCodeApiRunTimeoutMs(initParams.runTimeoutMs);
734
741
  const proxy = initParams.proxy ?? process.env.PROXY;
735
742
  const debug = initParams.debug ?? process.env.PTC_DEBUG === 'true';
736
743
  const EXEC_ENDPOINT = `${baseUrl}/exec/programmatic`;
@@ -738,7 +745,8 @@ export function createProgrammaticToolCallingTool(
738
745
  return tool(
739
746
  async (rawParams, config) => {
740
747
  const params = rawParams as { code: string; timeout?: number };
741
- const { code, timeout = DEFAULT_TIMEOUT } = params;
748
+ const { code } = params;
749
+ const timeout = clampCodeApiRunTimeoutMs(params.timeout, maxRunTimeoutMs);
742
750
 
743
751
  // Extra params injected by ToolNode (follows web_search pattern).
744
752
  const toolCall = (config.toolCall ?? {}) as ToolCall &
@@ -873,7 +881,7 @@ export function createProgrammaticToolCallingTool(
873
881
  {
874
882
  name: Constants.PROGRAMMATIC_TOOL_CALLING,
875
883
  description: ProgrammaticToolCallingDescription,
876
- schema: ProgrammaticToolCallingSchema,
884
+ schema: createProgrammaticToolCallingSchema(maxRunTimeoutMs),
877
885
  responseFormat: Constants.CONTENT_AND_ARTIFACT,
878
886
  }
879
887
  );