@librechat/agents 3.1.30 → 3.1.32
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/messages/prune.cjs +1 -1
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +36 -53
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +5 -7
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +49 -27
- package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
- package/dist/esm/messages/prune.mjs +1 -1
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +36 -53
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +5 -7
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +49 -27
- package/dist/esm/tools/ToolSearch.mjs.map +1 -1
- package/dist/types/tools/ToolSearch.d.ts +6 -4
- package/dist/types/types/tools.d.ts +4 -0
- package/package.json +1 -1
- package/src/messages/prune.ts +1 -1
- package/src/specs/thinking-prune.test.ts +172 -51
- package/src/tools/ProgrammaticToolCalling.ts +40 -52
- package/src/tools/ToolNode.ts +4 -5
- package/src/tools/ToolSearch.ts +64 -30
- package/src/tools/__tests__/ToolSearch.test.ts +16 -3
- package/src/types/tools.ts +5 -0
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// src/specs/thinking-prune.test.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
HumanMessage,
|
|
4
|
+
AIMessage,
|
|
5
|
+
SystemMessage,
|
|
6
|
+
BaseMessage,
|
|
7
|
+
ToolMessage,
|
|
8
|
+
} from '@langchain/core/messages';
|
|
3
9
|
import type * as t from '@/types';
|
|
4
10
|
import { createPruneMessages } from '@/messages/prune';
|
|
5
11
|
|
|
@@ -7,7 +13,10 @@ import { createPruneMessages } from '@/messages/prune';
|
|
|
7
13
|
const createTestTokenCounter = (): t.TokenCounter => {
|
|
8
14
|
return (message: BaseMessage): number => {
|
|
9
15
|
// Use type assertion to help TypeScript understand the type
|
|
10
|
-
const content = message.content as
|
|
16
|
+
const content = message.content as
|
|
17
|
+
| string
|
|
18
|
+
| Array<t.MessageContentComplex | string>
|
|
19
|
+
| undefined;
|
|
11
20
|
|
|
12
21
|
// Handle string content
|
|
13
22
|
if (typeof content === 'string') {
|
|
@@ -61,7 +70,8 @@ describe('Prune Messages with Thinking Mode Tests', () => {
|
|
|
61
70
|
content: [
|
|
62
71
|
{
|
|
63
72
|
type: 'thinking',
|
|
64
|
-
thinking:
|
|
73
|
+
thinking:
|
|
74
|
+
'The user is asking me to read a file located at `/home/danny/LibreChat/gistfile1.txt` in chunks of 200 lines at a time, mentioning that the file has 5000 lines total. They want me to continue reading through the entire file without stopping.\n\nI\'ll need to use the text editor tool to view the file in chunks of 200 lines each. Since the file has 5000 lines, I\'ll need to view it in 25 chunks (5000 ÷ 200 = 25).\n\nI\'ll need to make multiple calls to the text editor with the `view` command, specifying different line ranges for each call.\n\nLet me plan out the approach:\n1. Start with lines 1-200\n2. Then 201-400\n3. Then 401-600\n4. And so on until I reach 4801-5000\n\nFor each call, I\'ll use the `view` command with the specific line range in the `view_range` parameter. I\'ll continue until I\'ve shown all 5000 lines as requested.',
|
|
65
75
|
},
|
|
66
76
|
{
|
|
67
77
|
type: 'text',
|
|
@@ -71,7 +81,8 @@ describe('Prune Messages with Thinking Mode Tests', () => {
|
|
|
71
81
|
type: 'tool_use',
|
|
72
82
|
id: 'toolu_01YApWuFsEQCuBFDgYXmcmeZ',
|
|
73
83
|
name: 'text_editor_mcp_textEditor',
|
|
74
|
-
input:
|
|
84
|
+
input:
|
|
85
|
+
'{"command": "view", "path": "/home/danny/LibreChat/gistfile1.txt", "description": "Viewing lines 1-200 of the file", "view_range": [1,200]}',
|
|
75
86
|
},
|
|
76
87
|
],
|
|
77
88
|
});
|
|
@@ -97,7 +108,8 @@ describe('Prune Messages with Thinking Mode Tests', () => {
|
|
|
97
108
|
type: 'tool_use',
|
|
98
109
|
id: 'toolu_01VnyMQ4CvEd6zLDxxtTd6d4',
|
|
99
110
|
name: 'text_editor_mcp_textEditor',
|
|
100
|
-
input:
|
|
111
|
+
input:
|
|
112
|
+
'{"command": "view", "path": "/home/danny/LibreChat/gistfile1.txt", "description": "Viewing lines 201-400 of the file", "view_range": [201,400]}',
|
|
101
113
|
},
|
|
102
114
|
],
|
|
103
115
|
});
|
|
@@ -123,7 +135,8 @@ describe('Prune Messages with Thinking Mode Tests', () => {
|
|
|
123
135
|
type: 'tool_use',
|
|
124
136
|
id: 'toolu_01TZKs4nnBc58BYXKz1Mw4fp',
|
|
125
137
|
name: 'text_editor_mcp_textEditor',
|
|
126
|
-
input:
|
|
138
|
+
input:
|
|
139
|
+
'{"command": "view", "path": "/home/danny/LibreChat/gistfile1.txt", "description": "Viewing lines 401-600 of the file", "view_range": [401,600]}',
|
|
127
140
|
},
|
|
128
141
|
],
|
|
129
142
|
});
|
|
@@ -149,7 +162,8 @@ describe('Prune Messages with Thinking Mode Tests', () => {
|
|
|
149
162
|
type: 'tool_use',
|
|
150
163
|
id: 'toolu_01TZgBacNxjx1QNUpJg9hca5',
|
|
151
164
|
name: 'text_editor_mcp_textEditor',
|
|
152
|
-
input:
|
|
165
|
+
input:
|
|
166
|
+
'{"command": "view", "path": "/home/danny/LibreChat/gistfile1.txt", "description": "Viewing lines 601-800 of the file", "view_range": [601,800]}',
|
|
153
167
|
},
|
|
154
168
|
],
|
|
155
169
|
});
|
|
@@ -180,7 +194,7 @@ describe('Prune Messages with Thinking Mode Tests', () => {
|
|
|
180
194
|
// Create indexTokenCountMap based on the example provided
|
|
181
195
|
const indexTokenCountMap = {
|
|
182
196
|
'0': 617, // userMessage
|
|
183
|
-
'1': 52,
|
|
197
|
+
'1': 52, // assistantMessageWithThinking
|
|
184
198
|
'2': 4995, // toolResponseMessage1
|
|
185
199
|
'3': 307, // assistantMessage2
|
|
186
200
|
'4': 9359, // toolResponseMessage2
|
|
@@ -218,22 +232,34 @@ describe('Prune Messages with Thinking Mode Tests', () => {
|
|
|
218
232
|
expect(result.context.length).toBeGreaterThan(0);
|
|
219
233
|
|
|
220
234
|
// Find the first assistant message in the pruned context
|
|
221
|
-
const firstAssistantIndex = result.context.findIndex(
|
|
235
|
+
const firstAssistantIndex = result.context.findIndex(
|
|
236
|
+
(msg) => msg.getType() === 'ai'
|
|
237
|
+
);
|
|
222
238
|
expect(firstAssistantIndex).toBe(0);
|
|
223
239
|
|
|
224
240
|
const firstAssistantMsg = result.context[firstAssistantIndex];
|
|
225
241
|
expect(Array.isArray(firstAssistantMsg.content)).toBe(true);
|
|
226
242
|
|
|
227
243
|
// Verify that the first assistant message has a thinking block
|
|
228
|
-
const hasThinkingBlock = (
|
|
229
|
-
|
|
244
|
+
const hasThinkingBlock = (
|
|
245
|
+
firstAssistantMsg.content as t.MessageContentComplex[]
|
|
246
|
+
).some(
|
|
247
|
+
(item: t.MessageContentComplex) =>
|
|
248
|
+
typeof item === 'object' && item.type === 'thinking'
|
|
249
|
+
);
|
|
230
250
|
expect(hasThinkingBlock).toBe(true);
|
|
231
251
|
|
|
232
252
|
// Verify that the thinking block is from the original assistant message
|
|
233
|
-
const thinkingBlock = (
|
|
234
|
-
|
|
253
|
+
const thinkingBlock = (
|
|
254
|
+
firstAssistantMsg.content as t.MessageContentComplex[]
|
|
255
|
+
).find(
|
|
256
|
+
(item: t.MessageContentComplex) =>
|
|
257
|
+
typeof item === 'object' && item.type === 'thinking'
|
|
258
|
+
);
|
|
235
259
|
expect(thinkingBlock).toBeDefined();
|
|
236
|
-
expect((thinkingBlock as t.ThinkingContentText).thinking).toContain(
|
|
260
|
+
expect((thinkingBlock as t.ThinkingContentText).thinking).toContain(
|
|
261
|
+
'The user is asking me to read a file'
|
|
262
|
+
);
|
|
237
263
|
});
|
|
238
264
|
|
|
239
265
|
it('should handle token recalculation when inserting thinking blocks', () => {
|
|
@@ -451,35 +477,49 @@ describe('Prune Messages with Thinking Mode Tests', () => {
|
|
|
451
477
|
const result = pruneMessages({ messages });
|
|
452
478
|
|
|
453
479
|
// Find assistant message with tool call and its corresponding tool message in the pruned context
|
|
454
|
-
const assistantIndex = result.context.findIndex(
|
|
455
|
-
msg
|
|
456
|
-
|
|
457
|
-
|
|
480
|
+
const assistantIndex = result.context.findIndex(
|
|
481
|
+
(msg) =>
|
|
482
|
+
msg.getType() === 'ai' &&
|
|
483
|
+
Array.isArray(msg.content) &&
|
|
484
|
+
msg.content.some(
|
|
485
|
+
(item) =>
|
|
486
|
+
typeof item === 'object' &&
|
|
487
|
+
item.type === 'tool_use' &&
|
|
488
|
+
item.id === 'tool123'
|
|
489
|
+
)
|
|
458
490
|
);
|
|
459
491
|
|
|
460
492
|
// If the assistant message with tool call is in the context, its corresponding tool message should also be there
|
|
461
493
|
if (assistantIndex !== -1) {
|
|
462
|
-
const toolIndex = result.context.findIndex(
|
|
463
|
-
msg
|
|
464
|
-
|
|
465
|
-
|
|
494
|
+
const toolIndex = result.context.findIndex(
|
|
495
|
+
(msg) =>
|
|
496
|
+
msg.getType() === 'tool' &&
|
|
497
|
+
'tool_call_id' in msg &&
|
|
498
|
+
msg.tool_call_id === 'tool123'
|
|
466
499
|
);
|
|
467
500
|
|
|
468
501
|
expect(toolIndex).not.toBe(-1);
|
|
469
502
|
}
|
|
470
503
|
|
|
471
504
|
// If the tool message is in the context, its corresponding assistant message should also be there
|
|
472
|
-
const toolIndex = result.context.findIndex(
|
|
473
|
-
msg
|
|
474
|
-
|
|
475
|
-
|
|
505
|
+
const toolIndex = result.context.findIndex(
|
|
506
|
+
(msg) =>
|
|
507
|
+
msg.getType() === 'tool' &&
|
|
508
|
+
'tool_call_id' in msg &&
|
|
509
|
+
msg.tool_call_id === 'tool123'
|
|
476
510
|
);
|
|
477
511
|
|
|
478
512
|
if (toolIndex !== -1) {
|
|
479
|
-
const assistantWithToolIndex = result.context.findIndex(
|
|
480
|
-
msg
|
|
481
|
-
|
|
482
|
-
|
|
513
|
+
const assistantWithToolIndex = result.context.findIndex(
|
|
514
|
+
(msg) =>
|
|
515
|
+
msg.getType() === 'ai' &&
|
|
516
|
+
Array.isArray(msg.content) &&
|
|
517
|
+
msg.content.some(
|
|
518
|
+
(item) =>
|
|
519
|
+
typeof item === 'object' &&
|
|
520
|
+
item.type === 'tool_use' &&
|
|
521
|
+
item.id === 'tool123'
|
|
522
|
+
)
|
|
483
523
|
);
|
|
484
524
|
|
|
485
525
|
expect(assistantWithToolIndex).not.toBe(-1);
|
|
@@ -544,7 +584,9 @@ describe('Prune Messages with Thinking Mode Tests', () => {
|
|
|
544
584
|
// Calculate token counts for each message
|
|
545
585
|
const indexTokenCountMapWithoutSystem: Record<string, number> = {};
|
|
546
586
|
for (let i = 0; i < messagesWithoutSystem.length; i++) {
|
|
547
|
-
indexTokenCountMapWithoutSystem[i] = tokenCounter(
|
|
587
|
+
indexTokenCountMapWithoutSystem[i] = tokenCounter(
|
|
588
|
+
messagesWithoutSystem[i]
|
|
589
|
+
);
|
|
548
590
|
}
|
|
549
591
|
|
|
550
592
|
// Create pruneMessages function with thinking mode enabled
|
|
@@ -557,24 +599,33 @@ describe('Prune Messages with Thinking Mode Tests', () => {
|
|
|
557
599
|
});
|
|
558
600
|
|
|
559
601
|
// Prune messages
|
|
560
|
-
const resultWithoutSystem = pruneMessagesWithoutSystem({
|
|
602
|
+
const resultWithoutSystem = pruneMessagesWithoutSystem({
|
|
603
|
+
messages: messagesWithoutSystem,
|
|
604
|
+
});
|
|
561
605
|
|
|
562
606
|
// Verify that the pruned context contains at least one message
|
|
563
607
|
expect(resultWithoutSystem.context.length).toBeGreaterThan(0);
|
|
564
608
|
|
|
565
609
|
// Find all assistant messages in the latest sequence (after the last human message)
|
|
566
|
-
const lastHumanIndex = resultWithoutSystem.context
|
|
567
|
-
|
|
568
|
-
.
|
|
610
|
+
const lastHumanIndex = resultWithoutSystem.context
|
|
611
|
+
.map((msg) => msg.getType())
|
|
612
|
+
.lastIndexOf('human');
|
|
613
|
+
const assistantMessagesAfterLastHuman = resultWithoutSystem.context
|
|
614
|
+
.slice(lastHumanIndex + 1)
|
|
615
|
+
.filter((msg) => msg.getType() === 'ai');
|
|
569
616
|
|
|
570
617
|
// Verify that at least one assistant message exists in the latest sequence
|
|
571
618
|
expect(assistantMessagesAfterLastHuman.length).toBeGreaterThan(0);
|
|
572
619
|
|
|
573
620
|
// Verify that at least one of these assistant messages has a thinking block
|
|
574
|
-
const hasThinkingBlock = assistantMessagesAfterLastHuman.some(msg => {
|
|
621
|
+
const hasThinkingBlock = assistantMessagesAfterLastHuman.some((msg) => {
|
|
575
622
|
const content = msg.content as t.MessageContentComplex[];
|
|
576
|
-
return
|
|
577
|
-
|
|
623
|
+
return (
|
|
624
|
+
Array.isArray(content) &&
|
|
625
|
+
content.some(
|
|
626
|
+
(item) => typeof item === 'object' && item.type === 'thinking'
|
|
627
|
+
)
|
|
628
|
+
);
|
|
578
629
|
});
|
|
579
630
|
expect(hasThinkingBlock).toBe(true);
|
|
580
631
|
|
|
@@ -604,26 +655,36 @@ describe('Prune Messages with Thinking Mode Tests', () => {
|
|
|
604
655
|
});
|
|
605
656
|
|
|
606
657
|
// Prune messages
|
|
607
|
-
const resultWithSystem = pruneMessagesWithSystem({
|
|
658
|
+
const resultWithSystem = pruneMessagesWithSystem({
|
|
659
|
+
messages: messagesWithSystem,
|
|
660
|
+
});
|
|
608
661
|
|
|
609
662
|
// Verify that the system message remains first
|
|
610
663
|
expect(resultWithSystem.context.length).toBeGreaterThan(1);
|
|
611
664
|
expect(resultWithSystem.context[0].getType()).toBe('system');
|
|
612
665
|
|
|
613
666
|
// Find all assistant messages in the latest sequence (after the last human message)
|
|
614
|
-
const lastHumanIndexWithSystem = resultWithSystem.context
|
|
615
|
-
|
|
616
|
-
.
|
|
667
|
+
const lastHumanIndexWithSystem = resultWithSystem.context
|
|
668
|
+
.map((msg) => msg.getType())
|
|
669
|
+
.lastIndexOf('human');
|
|
670
|
+
const assistantMessagesAfterLastHumanWithSystem = resultWithSystem.context
|
|
671
|
+
.slice(lastHumanIndexWithSystem + 1)
|
|
672
|
+
.filter((msg) => msg.getType() === 'ai');
|
|
617
673
|
|
|
618
674
|
// Verify that at least one assistant message exists in the latest sequence
|
|
619
675
|
expect(assistantMessagesAfterLastHumanWithSystem.length).toBeGreaterThan(0);
|
|
620
676
|
|
|
621
677
|
// Verify that at least one of these assistant messages has a thinking block
|
|
622
|
-
const hasThinkingBlockWithSystem =
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
678
|
+
const hasThinkingBlockWithSystem =
|
|
679
|
+
assistantMessagesAfterLastHumanWithSystem.some((msg) => {
|
|
680
|
+
const content = msg.content as t.MessageContentComplex[];
|
|
681
|
+
return (
|
|
682
|
+
Array.isArray(content) &&
|
|
683
|
+
content.some(
|
|
684
|
+
(item) => typeof item === 'object' && item.type === 'thinking'
|
|
685
|
+
)
|
|
686
|
+
);
|
|
687
|
+
});
|
|
627
688
|
expect(hasThinkingBlockWithSystem).toBe(true);
|
|
628
689
|
});
|
|
629
690
|
|
|
@@ -686,18 +747,78 @@ describe('Prune Messages with Thinking Mode Tests', () => {
|
|
|
686
747
|
const result = pruneMessages({ messages });
|
|
687
748
|
|
|
688
749
|
// Find the first assistant message in the pruned context
|
|
689
|
-
const firstAssistantIndex = result.context.findIndex(
|
|
750
|
+
const firstAssistantIndex = result.context.findIndex(
|
|
751
|
+
(msg) => msg.getType() === 'ai'
|
|
752
|
+
);
|
|
690
753
|
expect(firstAssistantIndex).not.toBe(-1);
|
|
691
754
|
|
|
692
755
|
const firstAssistantMsg = result.context[firstAssistantIndex];
|
|
693
756
|
expect(Array.isArray(firstAssistantMsg.content)).toBe(true);
|
|
694
757
|
|
|
695
758
|
// Verify that the first assistant message has a thinking block
|
|
696
|
-
const thinkingBlock = (
|
|
697
|
-
|
|
759
|
+
const thinkingBlock = (
|
|
760
|
+
firstAssistantMsg.content as t.MessageContentComplex[]
|
|
761
|
+
).find((item) => typeof item === 'object' && item.type === 'thinking');
|
|
698
762
|
expect(thinkingBlock).toBeDefined();
|
|
699
763
|
|
|
700
764
|
// Verify that it's the newer thinking block
|
|
701
|
-
expect((thinkingBlock as t.ThinkingContentText).thinking).toContain(
|
|
765
|
+
expect((thinkingBlock as t.ThinkingContentText).thinking).toContain(
|
|
766
|
+
'newer thinking block'
|
|
767
|
+
);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it('should throw descriptive error when aggressive pruning removes all AI messages', () => {
|
|
771
|
+
const tokenCounter = createTestTokenCounter();
|
|
772
|
+
|
|
773
|
+
const assistantMessageWithThinking = new AIMessage({
|
|
774
|
+
content: [
|
|
775
|
+
{
|
|
776
|
+
type: 'thinking',
|
|
777
|
+
thinking: 'This is a thinking block that will be pruned',
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
type: 'text',
|
|
781
|
+
text: 'Response with thinking',
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
type: 'tool_use',
|
|
785
|
+
id: 'tool123',
|
|
786
|
+
name: 'large_tool',
|
|
787
|
+
input: '{"query": "test"}',
|
|
788
|
+
},
|
|
789
|
+
],
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
const largeToolResponse = new ToolMessage({
|
|
793
|
+
content: 'A'.repeat(10000),
|
|
794
|
+
tool_call_id: 'tool123',
|
|
795
|
+
name: 'large_tool',
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
const messages = [
|
|
799
|
+
new SystemMessage('System instruction'),
|
|
800
|
+
new HumanMessage('Hello'),
|
|
801
|
+
assistantMessageWithThinking,
|
|
802
|
+
largeToolResponse,
|
|
803
|
+
];
|
|
804
|
+
|
|
805
|
+
const indexTokenCountMap: Record<string, number> = {
|
|
806
|
+
'0': 17,
|
|
807
|
+
'1': 5,
|
|
808
|
+
'2': 100,
|
|
809
|
+
'3': 10000,
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
const pruneMessages = createPruneMessages({
|
|
813
|
+
maxTokens: 50,
|
|
814
|
+
startIndex: 0,
|
|
815
|
+
tokenCounter,
|
|
816
|
+
indexTokenCountMap: { ...indexTokenCountMap },
|
|
817
|
+
thinkingEnabled: true,
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
expect(() => pruneMessages({ messages })).toThrow(
|
|
821
|
+
/Context window exceeded/
|
|
822
|
+
);
|
|
702
823
|
});
|
|
703
824
|
});
|
|
@@ -29,25 +29,25 @@ const DEFAULT_MAX_ROUND_TRIPS = 20;
|
|
|
29
29
|
const DEFAULT_TIMEOUT = 60000;
|
|
30
30
|
|
|
31
31
|
// ============================================================================
|
|
32
|
-
//
|
|
32
|
+
// Description Components (Single Source of Truth)
|
|
33
33
|
// ============================================================================
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
type: 'object',
|
|
37
|
-
properties: {
|
|
38
|
-
code: {
|
|
39
|
-
type: 'string',
|
|
40
|
-
minLength: 1,
|
|
41
|
-
description: `Python code that calls tools programmatically. Tools are available as async functions.
|
|
42
|
-
|
|
43
|
-
CRITICAL - STATELESS EXECUTION:
|
|
35
|
+
const STATELESS_WARNING = `CRITICAL - STATELESS EXECUTION:
|
|
44
36
|
Each call is a fresh Python interpreter. Variables, imports, and data do NOT persist between calls.
|
|
45
37
|
You MUST complete your entire workflow in ONE code block: query → process → output.
|
|
46
|
-
DO NOT split work across multiple calls expecting to reuse variables
|
|
38
|
+
DO NOT split work across multiple calls expecting to reuse variables.`;
|
|
47
39
|
|
|
48
|
-
|
|
40
|
+
const CORE_RULES = `Rules:
|
|
41
|
+
- EVERYTHING in one call—no state persists between executions
|
|
42
|
+
- Just write code with await—auto-wrapped in async context
|
|
43
|
+
- DO NOT define async def main() or call asyncio.run()
|
|
44
|
+
- Tools are pre-defined—DO NOT write function definitions
|
|
45
|
+
- Only print() output returns to the model`;
|
|
46
|
+
|
|
47
|
+
const ADDITIONAL_RULES = `- Generated files are automatically available in /mnt/data/ for subsequent executions
|
|
48
|
+
- Tool names normalized: hyphens→underscores, keywords get \`_tool\` suffix`;
|
|
49
49
|
|
|
50
|
-
Example (Complete workflow in one call):
|
|
50
|
+
const EXAMPLES = `Example (Complete workflow in one call):
|
|
51
51
|
# Query data
|
|
52
52
|
data = await query_database(sql="SELECT * FROM users")
|
|
53
53
|
# Process it
|
|
@@ -59,14 +59,29 @@ Example (Complete workflow in one call):
|
|
|
59
59
|
|
|
60
60
|
Example (Parallel calls):
|
|
61
61
|
sf, ny = await asyncio.gather(get_weather(city="SF"), get_weather(city="NY"))
|
|
62
|
-
print(f"SF: {sf}, NY: {ny}")
|
|
62
|
+
print(f"SF: {sf}, NY: {ny}")`;
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Schema
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
const CODE_PARAM_DESCRIPTION = `Python code that calls tools programmatically. Tools are available as async functions.
|
|
69
|
+
|
|
70
|
+
${STATELESS_WARNING}
|
|
71
|
+
|
|
72
|
+
Your code is auto-wrapped in async context. Just write logic with await—no boilerplate needed.
|
|
73
|
+
|
|
74
|
+
${EXAMPLES}
|
|
75
|
+
|
|
76
|
+
${CORE_RULES}`;
|
|
77
|
+
|
|
78
|
+
export const ProgrammaticToolCallingSchema = {
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: {
|
|
81
|
+
code: {
|
|
82
|
+
type: 'string',
|
|
83
|
+
minLength: 1,
|
|
84
|
+
description: CODE_PARAM_DESCRIPTION,
|
|
70
85
|
},
|
|
71
86
|
timeout: {
|
|
72
87
|
type: 'integer',
|
|
@@ -85,21 +100,14 @@ export const ProgrammaticToolCallingName = Constants.PROGRAMMATIC_TOOL_CALLING;
|
|
|
85
100
|
export const ProgrammaticToolCallingDescription = `
|
|
86
101
|
Run tools via Python code. Auto-wrapped in async context—just use \`await\` directly.
|
|
87
102
|
|
|
88
|
-
|
|
89
|
-
Complete your ENTIRE workflow in ONE call: fetch → process → save. No splitting across calls.
|
|
103
|
+
${STATELESS_WARNING}
|
|
90
104
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
- Do NOT define \`async def main()\` or call \`asyncio.run()\`—just write code with await
|
|
94
|
-
- Tools are pre-defined—DO NOT write function definitions
|
|
95
|
-
- Only \`print()\` output returns; tool results are raw dicts/lists/strings
|
|
96
|
-
- Generated files are automatically available in /mnt/data/ for subsequent executions
|
|
97
|
-
- Tool names normalized: hyphens→underscores, keywords get \`_tool\` suffix
|
|
105
|
+
${CORE_RULES}
|
|
106
|
+
${ADDITIONAL_RULES}
|
|
98
107
|
|
|
99
108
|
When to use: loops, conditionals, parallel (\`asyncio.gather\`), multi-step pipelines.
|
|
100
109
|
|
|
101
|
-
|
|
102
|
-
data = await query_db(sql="..."); df = process(data); await save_to_sheet(data=df); print("Done")
|
|
110
|
+
${EXAMPLES}
|
|
103
111
|
`.trim();
|
|
104
112
|
|
|
105
113
|
export const ProgrammaticToolCallingDefinition = {
|
|
@@ -624,26 +632,6 @@ export function createProgrammaticToolCallingTool(
|
|
|
624
632
|
const debug = initParams.debug ?? process.env.PTC_DEBUG === 'true';
|
|
625
633
|
const EXEC_ENDPOINT = `${baseUrl}/exec/programmatic`;
|
|
626
634
|
|
|
627
|
-
const description = `
|
|
628
|
-
Run tools via Python code. Auto-wrapped in async context—just use \`await\` directly.
|
|
629
|
-
|
|
630
|
-
CRITICAL - STATELESS: Each call is a fresh interpreter. Variables/imports do NOT persist.
|
|
631
|
-
Complete your ENTIRE workflow in ONE call: fetch → process → save. No splitting across calls.
|
|
632
|
-
|
|
633
|
-
Rules:
|
|
634
|
-
- Everything in ONE code block—no state carries over between executions
|
|
635
|
-
- Do NOT define \`async def main()\` or call \`asyncio.run()\`—just write code with await
|
|
636
|
-
- Tools are pre-defined—DO NOT write function definitions
|
|
637
|
-
- Only \`print()\` output returns; tool results are raw dicts/lists/strings
|
|
638
|
-
- Generated files are automatically available in /mnt/data/ for subsequent executions
|
|
639
|
-
- Tool names normalized: hyphens→underscores, keywords get \`_tool\` suffix
|
|
640
|
-
|
|
641
|
-
When to use: loops, conditionals, parallel (\`asyncio.gather\`), multi-step pipelines.
|
|
642
|
-
|
|
643
|
-
Example (complete pipeline):
|
|
644
|
-
data = await query_db(sql="..."); df = process(data); await save_to_sheet(data=df); print("Done")
|
|
645
|
-
`.trim();
|
|
646
|
-
|
|
647
635
|
return tool(
|
|
648
636
|
async (rawParams, config) => {
|
|
649
637
|
const params = rawParams as { code: string; timeout?: number };
|
|
@@ -777,7 +765,7 @@ Example (complete pipeline):
|
|
|
777
765
|
},
|
|
778
766
|
{
|
|
779
767
|
name: Constants.PROGRAMMATIC_TOOL_CALLING,
|
|
780
|
-
description,
|
|
768
|
+
description: ProgrammaticToolCallingDescription,
|
|
781
769
|
schema: ProgrammaticToolCallingSchema,
|
|
782
770
|
responseFormat: Constants.CONTENT_AND_ARTIFACT,
|
|
783
771
|
}
|
package/src/tools/ToolNode.ts
CHANGED
|
@@ -585,17 +585,16 @@ export function toolsCondition<T extends string>(
|
|
|
585
585
|
toolNode: T,
|
|
586
586
|
invokedToolIds?: Set<string>
|
|
587
587
|
): T | typeof END {
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
: state.messages[state.messages.length - 1];
|
|
588
|
+
const messages = Array.isArray(state) ? state : state.messages;
|
|
589
|
+
const message = messages[messages.length - 1] as AIMessage | undefined;
|
|
591
590
|
|
|
592
591
|
if (
|
|
592
|
+
message &&
|
|
593
593
|
'tool_calls' in message &&
|
|
594
594
|
(message.tool_calls?.length ?? 0) > 0 &&
|
|
595
595
|
!areToolCallsInvoked(message, invokedToolIds)
|
|
596
596
|
) {
|
|
597
597
|
return toolNode;
|
|
598
|
-
} else {
|
|
599
|
-
return END;
|
|
600
598
|
}
|
|
599
|
+
return END;
|
|
601
600
|
}
|