@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.
- package/dist/cjs/agents/AgentContext.cjs +7 -2
- 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/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 +2 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +23 -21
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +23 -22
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +4 -1
- 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 +7 -2
- 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/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 +2 -2
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +23 -22
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +23 -23
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +4 -1
- 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/common/enum.d.ts +2 -1
- package/dist/types/tools/BashProgrammaticToolCalling.d.ts +4 -36
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +4 -36
- package/dist/types/tools/ptcTimeout.d.ts +25 -0
- package/dist/types/types/tools.d.ts +2 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +7 -2
- package/src/agents/__tests__/AgentContext.test.ts +254 -5
- package/src/common/enum.ts +1 -0
- package/src/graphs/MultiAgentGraph.ts +3 -2
- package/src/graphs/__tests__/composition.smoke.test.ts +84 -2
- package/src/tools/BashProgrammaticToolCalling.ts +31 -22
- package/src/tools/ProgrammaticToolCalling.ts +31 -23
- package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +103 -0
- package/src/tools/local/LocalProgrammaticToolCalling.ts +94 -13
- package/src/tools/ptcTimeout.ts +89 -0
- 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('
|
|
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('
|
|
405
|
-
expect(
|
|
406
|
-
expect(result[3].
|
|
407
|
-
expect(result[4].
|
|
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: {
|
package/src/common/enum.ts
CHANGED
|
@@ -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: [
|
|
1079
|
+
messages: [promptMessage],
|
|
1079
1080
|
agentMessages: messagesStateReducer(filteredMessages, [
|
|
1080
|
-
|
|
1081
|
+
promptMessage,
|
|
1081
1082
|
]),
|
|
1082
1083
|
};
|
|
1083
1084
|
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import { HumanMessage } from '@langchain/core/messages';
|
|
2
|
-
import type {
|
|
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
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
113
|
-
}
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
89
|
-
}
|
|
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
|
|
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:
|
|
884
|
+
schema: createProgrammaticToolCallingSchema(maxRunTimeoutMs),
|
|
877
885
|
responseFormat: Constants.CONTENT_AND_ARTIFACT,
|
|
878
886
|
}
|
|
879
887
|
);
|