@librechat/agents 3.0.35 → 3.0.40

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 (68) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +71 -2
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +2 -0
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/events.cjs +3 -0
  6. package/dist/cjs/events.cjs.map +1 -1
  7. package/dist/cjs/graphs/Graph.cjs +7 -2
  8. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  9. package/dist/cjs/instrumentation.cjs +1 -1
  10. package/dist/cjs/instrumentation.cjs.map +1 -1
  11. package/dist/cjs/main.cjs +12 -0
  12. package/dist/cjs/main.cjs.map +1 -1
  13. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +329 -0
  14. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -0
  15. package/dist/cjs/tools/ToolNode.cjs +34 -3
  16. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  17. package/dist/cjs/tools/ToolSearchRegex.cjs +455 -0
  18. package/dist/cjs/tools/ToolSearchRegex.cjs.map +1 -0
  19. package/dist/esm/agents/AgentContext.mjs +71 -2
  20. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  21. package/dist/esm/common/enum.mjs +2 -0
  22. package/dist/esm/common/enum.mjs.map +1 -1
  23. package/dist/esm/events.mjs +4 -1
  24. package/dist/esm/events.mjs.map +1 -1
  25. package/dist/esm/graphs/Graph.mjs +7 -2
  26. package/dist/esm/graphs/Graph.mjs.map +1 -1
  27. package/dist/esm/instrumentation.mjs +1 -1
  28. package/dist/esm/instrumentation.mjs.map +1 -1
  29. package/dist/esm/main.mjs +2 -0
  30. package/dist/esm/main.mjs.map +1 -1
  31. package/dist/esm/tools/ProgrammaticToolCalling.mjs +324 -0
  32. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -0
  33. package/dist/esm/tools/ToolNode.mjs +34 -3
  34. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  35. package/dist/esm/tools/ToolSearchRegex.mjs +448 -0
  36. package/dist/esm/tools/ToolSearchRegex.mjs.map +1 -0
  37. package/dist/types/agents/AgentContext.d.ts +25 -1
  38. package/dist/types/common/enum.d.ts +2 -0
  39. package/dist/types/graphs/Graph.d.ts +2 -1
  40. package/dist/types/index.d.ts +2 -0
  41. package/dist/types/test/mockTools.d.ts +28 -0
  42. package/dist/types/tools/ProgrammaticToolCalling.d.ts +86 -0
  43. package/dist/types/tools/ToolNode.d.ts +7 -1
  44. package/dist/types/tools/ToolSearchRegex.d.ts +80 -0
  45. package/dist/types/types/graph.d.ts +7 -1
  46. package/dist/types/types/tools.d.ts +136 -0
  47. package/package.json +5 -1
  48. package/src/agents/AgentContext.ts +86 -0
  49. package/src/common/enum.ts +2 -0
  50. package/src/events.ts +5 -1
  51. package/src/graphs/Graph.ts +8 -1
  52. package/src/index.ts +2 -0
  53. package/src/instrumentation.ts +1 -1
  54. package/src/llm/google/llm.spec.ts +3 -1
  55. package/src/scripts/code_exec_ptc.ts +277 -0
  56. package/src/scripts/programmatic_exec.ts +396 -0
  57. package/src/scripts/programmatic_exec_agent.ts +231 -0
  58. package/src/scripts/tool_search_regex.ts +162 -0
  59. package/src/test/mockTools.ts +366 -0
  60. package/src/tools/ProgrammaticToolCalling.ts +423 -0
  61. package/src/tools/ToolNode.ts +38 -4
  62. package/src/tools/ToolSearchRegex.ts +535 -0
  63. package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +318 -0
  64. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +613 -0
  65. package/src/tools/__tests__/ToolSearchRegex.integration.test.ts +161 -0
  66. package/src/tools/__tests__/ToolSearchRegex.test.ts +232 -0
  67. package/src/types/graph.ts +7 -1
  68. package/src/types/tools.ts +166 -0
@@ -0,0 +1,423 @@
1
+ // src/tools/ProgrammaticToolCalling.ts
2
+ import { z } from 'zod';
3
+ import { config } from 'dotenv';
4
+ import fetch, { RequestInit } from 'node-fetch';
5
+ import { HttpsProxyAgent } from 'https-proxy-agent';
6
+ import { getEnvironmentVariable } from '@langchain/core/utils/env';
7
+ import { tool, DynamicStructuredTool } from '@langchain/core/tools';
8
+ import type * as t from '@/types';
9
+ import { imageExtRegex, getCodeBaseURL } from './CodeExecutor';
10
+ import { EnvVar, Constants } from '@/common';
11
+
12
+ config();
13
+
14
+ // ============================================================================
15
+ // Constants
16
+ // ============================================================================
17
+
18
+ const imageMessage = 'Image is already displayed to the user';
19
+ const otherMessage = 'File is already downloaded by the user';
20
+ const accessMessage =
21
+ 'Note: Files are READ-ONLY. Save changes to NEW filenames. To access these files in future executions, provide the `session_id` as a parameter (not in your code).';
22
+ const emptyOutputMessage =
23
+ 'stdout: Empty. Ensure you\'re writing output explicitly.\n';
24
+
25
+ /** Default max round-trips to prevent infinite loops */
26
+ const DEFAULT_MAX_ROUND_TRIPS = 20;
27
+
28
+ /** Default execution timeout in milliseconds */
29
+ const DEFAULT_TIMEOUT = 60000;
30
+
31
+ // ============================================================================
32
+ // Schema
33
+ // ============================================================================
34
+
35
+ const ProgrammaticToolCallingSchema = z.object({
36
+ code: z
37
+ .string()
38
+ .min(1)
39
+ .describe(
40
+ `Python code that calls tools programmatically. Tools are automatically available as async Python functions - DO NOT define them yourself.
41
+
42
+ The Code API generates async function stubs from the tool definitions. Just call them directly:
43
+
44
+ Example (Simple call):
45
+ result = await get_weather(city="San Francisco")
46
+ print(result)
47
+
48
+ Example (Parallel - Fastest):
49
+ results = await asyncio.gather(
50
+ get_weather(city="SF"),
51
+ get_weather(city="NYC"),
52
+ get_weather(city="London")
53
+ )
54
+ for city, weather in zip(["SF", "NYC", "London"], results):
55
+ print(f"{city}: {weather['temperature']}°F")
56
+
57
+ Example (Loop with processing):
58
+ team = await get_team_members()
59
+ for member in team:
60
+ expenses = await get_expenses(user_id=member['id'])
61
+ total = sum(e['amount'] for e in expenses)
62
+ print(f"{member['name']}: \${total:.2f}")
63
+
64
+ Example (Conditional logic):
65
+ data = await fetch_data(source="primary")
66
+ if not data:
67
+ data = await fetch_data(source="backup")
68
+ print(f"Got {len(data)} records")
69
+
70
+ Requirements:
71
+ - Tools are pre-defined as async functions - DO NOT write function definitions
72
+ - Use await for all tool calls
73
+ - Use asyncio.gather() for parallel execution of independent calls
74
+ - Only print() output flows back to the context window
75
+ - Tool results from programmatic calls do NOT consume context tokens`
76
+ ),
77
+ tools: z
78
+ .array(
79
+ z.object({
80
+ name: z.string(),
81
+ description: z.string().optional(),
82
+ parameters: z.any(), // JsonSchemaType
83
+ })
84
+ )
85
+ .optional()
86
+ .describe(
87
+ 'Optional array of tool definitions that can be called from the code. If not provided, uses programmatic tools configured in the agent context. Tool names must match tools available in the toolMap.'
88
+ ),
89
+ session_id: z
90
+ .string()
91
+ .optional()
92
+ .describe(
93
+ 'Session ID for file access (same as regular code execution). Files load into /mnt/data/ and are READ-ONLY.'
94
+ ),
95
+ timeout: z
96
+ .number()
97
+ .int()
98
+ .min(1000)
99
+ .max(300000)
100
+ .optional()
101
+ .default(DEFAULT_TIMEOUT)
102
+ .describe(
103
+ 'Maximum execution time in milliseconds. Default: 60 seconds. Max: 5 minutes.'
104
+ ),
105
+ });
106
+
107
+ // ============================================================================
108
+ // Helper Functions
109
+ // ============================================================================
110
+
111
+ /**
112
+ * Makes an HTTP request to the Code API.
113
+ * @param endpoint - The API endpoint URL
114
+ * @param apiKey - The API key for authentication
115
+ * @param body - The request body
116
+ * @param proxy - Optional HTTP proxy URL
117
+ * @returns The parsed API response
118
+ */
119
+ export async function makeRequest(
120
+ endpoint: string,
121
+ apiKey: string,
122
+ body: Record<string, unknown>,
123
+ proxy?: string
124
+ ): Promise<t.ProgrammaticExecutionResponse> {
125
+ const fetchOptions: RequestInit = {
126
+ method: 'POST',
127
+ headers: {
128
+ 'Content-Type': 'application/json',
129
+ 'User-Agent': 'LibreChat/1.0',
130
+ 'X-API-Key': apiKey,
131
+ },
132
+ body: JSON.stringify(body),
133
+ };
134
+
135
+ if (proxy != null && proxy !== '') {
136
+ fetchOptions.agent = new HttpsProxyAgent(proxy);
137
+ }
138
+
139
+ const response = await fetch(endpoint, fetchOptions);
140
+
141
+ if (!response.ok) {
142
+ const errorText = await response.text();
143
+ throw new Error(
144
+ `HTTP error! status: ${response.status}, body: ${errorText}`
145
+ );
146
+ }
147
+
148
+ return (await response.json()) as t.ProgrammaticExecutionResponse;
149
+ }
150
+
151
+ /**
152
+ * Executes tools in parallel when requested by the API.
153
+ * Uses Promise.all for parallel execution, catching individual errors.
154
+ * @param toolCalls - Array of tool calls from the API
155
+ * @param toolMap - Map of tool names to executable tools
156
+ * @returns Array of tool results
157
+ */
158
+ export async function executeTools(
159
+ toolCalls: t.PTCToolCall[],
160
+ toolMap: t.ToolMap
161
+ ): Promise<t.PTCToolResult[]> {
162
+ const executions = toolCalls.map(async (call): Promise<t.PTCToolResult> => {
163
+ const tool = toolMap.get(call.name);
164
+
165
+ if (!tool) {
166
+ return {
167
+ call_id: call.id,
168
+ result: null,
169
+ is_error: true,
170
+ error_message: `Tool '${call.name}' not found. Available tools: ${Array.from(toolMap.keys()).join(', ')}`,
171
+ };
172
+ }
173
+
174
+ try {
175
+ const result = await tool.invoke(call.input, {
176
+ metadata: { [Constants.PROGRAMMATIC_TOOL_CALLING]: true },
177
+ });
178
+ return {
179
+ call_id: call.id,
180
+ result,
181
+ is_error: false,
182
+ };
183
+ } catch (error) {
184
+ return {
185
+ call_id: call.id,
186
+ result: null,
187
+ is_error: true,
188
+ error_message: (error as Error).message || 'Tool execution failed',
189
+ };
190
+ }
191
+ });
192
+
193
+ return await Promise.all(executions);
194
+ }
195
+
196
+ /**
197
+ * Formats the completed response for the agent.
198
+ * @param response - The completed API response
199
+ * @returns Tuple of [formatted string, artifact]
200
+ */
201
+ export function formatCompletedResponse(
202
+ response: t.ProgrammaticExecutionResponse
203
+ ): [string, t.ProgrammaticExecutionArtifact] {
204
+ let formatted = '';
205
+
206
+ if (response.stdout != null && response.stdout !== '') {
207
+ formatted += `stdout:\n${response.stdout}\n`;
208
+ } else {
209
+ formatted += emptyOutputMessage;
210
+ }
211
+
212
+ if (response.stderr != null && response.stderr !== '') {
213
+ formatted += `stderr:\n${response.stderr}\n`;
214
+ }
215
+
216
+ if (response.files && response.files.length > 0) {
217
+ formatted += 'Generated files:\n';
218
+
219
+ const fileCount = response.files.length;
220
+ for (let i = 0; i < fileCount; i++) {
221
+ const file = response.files[i];
222
+ const isImage = imageExtRegex.test(file.name);
223
+ formatted += `- /mnt/data/${file.name} | ${isImage ? imageMessage : otherMessage}`;
224
+
225
+ if (i < fileCount - 1) {
226
+ formatted += fileCount <= 3 ? ', ' : ',\n';
227
+ }
228
+ }
229
+
230
+ formatted += `\nsession_id: ${response.session_id}\n\n${accessMessage}`;
231
+ }
232
+
233
+ return [
234
+ formatted.trim(),
235
+ {
236
+ session_id: response.session_id,
237
+ files: response.files,
238
+ },
239
+ ];
240
+ }
241
+
242
+ // ============================================================================
243
+ // Tool Factory
244
+ // ============================================================================
245
+
246
+ /**
247
+ * Creates a Programmatic Tool Calling tool for complex multi-tool workflows.
248
+ *
249
+ * This tool enables AI agents to write Python code that orchestrates multiple
250
+ * tool calls programmatically, reducing LLM round-trips and token usage.
251
+ *
252
+ * The tool map must be provided at runtime via config.configurable.toolMap.
253
+ *
254
+ * @param params - Configuration parameters (apiKey, baseUrl, maxRoundTrips, proxy)
255
+ * @returns A LangChain DynamicStructuredTool for programmatic tool calling
256
+ *
257
+ * @example
258
+ * const ptcTool = createProgrammaticToolCallingTool({
259
+ * apiKey: process.env.CODE_API_KEY,
260
+ * maxRoundTrips: 20
261
+ * });
262
+ *
263
+ * const [output, artifact] = await ptcTool.invoke(
264
+ * { code, tools },
265
+ * { configurable: { toolMap } }
266
+ * );
267
+ */
268
+ export function createProgrammaticToolCallingTool(
269
+ initParams: t.ProgrammaticToolCallingParams = {}
270
+ ): DynamicStructuredTool<typeof ProgrammaticToolCallingSchema> {
271
+ const apiKey =
272
+ (initParams[EnvVar.CODE_API_KEY] as string | undefined) ??
273
+ initParams.apiKey ??
274
+ getEnvironmentVariable(EnvVar.CODE_API_KEY) ??
275
+ '';
276
+
277
+ if (!apiKey) {
278
+ throw new Error(
279
+ 'No API key provided for programmatic tool calling. ' +
280
+ 'Set CODE_API_KEY environment variable or pass apiKey in initParams.'
281
+ );
282
+ }
283
+
284
+ const baseUrl = initParams.baseUrl ?? getCodeBaseURL();
285
+ const maxRoundTrips = initParams.maxRoundTrips ?? DEFAULT_MAX_ROUND_TRIPS;
286
+ const proxy = initParams.proxy ?? process.env.PROXY;
287
+ const EXEC_ENDPOINT = `${baseUrl}/exec/programmatic`;
288
+
289
+ const description = `
290
+ Executes Python code with programmatic tool calling. Tools are automatically generated as async Python functions from the tool definitions - DO NOT define them in your code.
291
+
292
+ Usage:
293
+ - Write Python code that calls tools using await: result = await get_data()
294
+ - Tools are pre-defined as async functions - just call them
295
+ - Use asyncio.gather() for parallel execution (single round-trip!)
296
+ - Only print() output flows through the context window
297
+ - Tool results from programmatic calls do NOT consume context tokens
298
+
299
+ When to use:
300
+ - Processing multiple records with tool calls (10+ items)
301
+ - Loops, conditionals, or aggregation based on tool results
302
+ - Any workflow requiring 3+ sequential tool calls
303
+ - Parallel execution of independent tool calls
304
+ - Filtering/summarizing large data before returning to context
305
+
306
+ Patterns:
307
+ - Simple: result = await get_data()
308
+ - Loop: for item in items: data = await fetch(item)
309
+ - Parallel: results = await asyncio.gather(t1(), t2(), t3())
310
+ - Conditional: if x: await tool_a() else: await tool_b()
311
+ `.trim();
312
+
313
+ return tool<typeof ProgrammaticToolCallingSchema>(
314
+ async (params, config) => {
315
+ const { code, tools, session_id, timeout = DEFAULT_TIMEOUT } = params;
316
+
317
+ // Extra params injected by ToolNode (follows web_search pattern)
318
+ const { toolMap, programmaticToolDefs } = config.toolCall ?? {};
319
+
320
+ if (toolMap == null || toolMap.size === 0) {
321
+ throw new Error(
322
+ 'No toolMap provided. ' +
323
+ 'ToolNode should inject this from AgentContext when invoked through the graph.'
324
+ );
325
+ }
326
+
327
+ // Use provided tools or fall back to programmaticToolDefs from ToolNode
328
+ const effectiveTools = tools ?? programmaticToolDefs;
329
+
330
+ if (effectiveTools == null || effectiveTools.length === 0) {
331
+ throw new Error(
332
+ 'No tool definitions provided. ' +
333
+ 'Either pass tools in the input or ensure ToolNode injects programmaticToolDefs.'
334
+ );
335
+ }
336
+
337
+ let roundTrip = 0;
338
+
339
+ try {
340
+ // ====================================================================
341
+ // Phase 1: Initial request
342
+ // ====================================================================
343
+
344
+ let response = await makeRequest(
345
+ EXEC_ENDPOINT,
346
+ apiKey,
347
+ {
348
+ code,
349
+ tools: effectiveTools,
350
+ session_id,
351
+ timeout,
352
+ },
353
+ proxy
354
+ );
355
+
356
+ // ====================================================================
357
+ // Phase 2: Handle response loop
358
+ // ====================================================================
359
+
360
+ while (response.status === 'tool_call_required') {
361
+ roundTrip++;
362
+
363
+ if (roundTrip > maxRoundTrips) {
364
+ throw new Error(
365
+ `Exceeded maximum round trips (${maxRoundTrips}). ` +
366
+ 'This may indicate an infinite loop, excessive tool calls, ' +
367
+ 'or a logic error in your code.'
368
+ );
369
+ }
370
+
371
+ // eslint-disable-next-line no-console
372
+ console.log(
373
+ `[PTC] Round trip ${roundTrip}: ${response.tool_calls?.length ?? 0} tool(s) to execute`
374
+ );
375
+
376
+ const toolResults = await executeTools(
377
+ response.tool_calls ?? [],
378
+ toolMap
379
+ );
380
+
381
+ response = await makeRequest(
382
+ EXEC_ENDPOINT,
383
+ apiKey,
384
+ {
385
+ continuation_token: response.continuation_token,
386
+ tool_results: toolResults,
387
+ },
388
+ proxy
389
+ );
390
+ }
391
+
392
+ // ====================================================================
393
+ // Phase 3: Handle final state
394
+ // ====================================================================
395
+
396
+ if (response.status === 'completed') {
397
+ return formatCompletedResponse(response);
398
+ }
399
+
400
+ if (response.status === 'error') {
401
+ throw new Error(
402
+ `Execution error: ${response.error}` +
403
+ (response.stderr != null && response.stderr !== ''
404
+ ? `\n\nStderr:\n${response.stderr}`
405
+ : '')
406
+ );
407
+ }
408
+
409
+ throw new Error(`Unexpected response status: ${response.status}`);
410
+ } catch (error) {
411
+ throw new Error(
412
+ `Programmatic execution failed: ${(error as Error).message}`
413
+ );
414
+ }
415
+ },
416
+ {
417
+ name: Constants.PROGRAMMATIC_TOOL_CALLING,
418
+ description,
419
+ schema: ProgrammaticToolCallingSchema,
420
+ responseFormat: Constants.CONTENT_AND_ARTIFACT,
421
+ }
422
+ );
423
+ }
@@ -20,6 +20,7 @@ import type { BaseMessage, AIMessage } from '@langchain/core/messages';
20
20
  import type { StructuredToolInterface } from '@langchain/core/tools';
21
21
  import type * as t from '@/types';
22
22
  import { RunnableCallable } from '@/utils';
23
+ import { Constants } from '@/common';
23
24
 
24
25
  /**
25
26
  * Helper to check if a value is a Send object
@@ -38,6 +39,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
38
39
  toolCallStepIds?: Map<string, string>;
39
40
  errorHandler?: t.ToolNodeConstructorParams['errorHandler'];
40
41
  private toolUsageCount: Map<string, number>;
42
+ /** Tools available for programmatic code execution */
43
+ private programmaticToolMap?: t.ToolMap;
44
+ /** Tool definitions for programmatic code execution (sent to Code API) */
45
+ private programmaticToolDefs?: t.LCTool[];
46
+ /** Tool registry for tool search (deferred tools) */
47
+ private toolRegistry?: t.LCToolRegistry;
41
48
 
42
49
  constructor({
43
50
  tools,
@@ -48,6 +55,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
48
55
  toolCallStepIds,
49
56
  handleToolErrors,
50
57
  loadRuntimeTools,
58
+ programmaticToolMap,
59
+ programmaticToolDefs,
60
+ toolRegistry,
51
61
  }: t.ToolNodeConstructorParams) {
52
62
  super({ name, tags, func: (input, config) => this.run(input, config) });
53
63
  this.tools = tools;
@@ -57,6 +67,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
57
67
  this.loadRuntimeTools = loadRuntimeTools;
58
68
  this.errorHandler = errorHandler;
59
69
  this.toolUsageCount = new Map<string, number>();
70
+ this.programmaticToolMap = programmaticToolMap;
71
+ this.programmaticToolDefs = programmaticToolDefs;
72
+ this.toolRegistry = toolRegistry;
60
73
  }
61
74
 
62
75
  /**
@@ -83,10 +96,31 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
83
96
  this.toolUsageCount.set(call.name, turn + 1);
84
97
  const args = call.args;
85
98
  const stepId = this.toolCallStepIds?.get(call.id!);
86
- const output = await tool.invoke(
87
- { ...call, args, type: 'tool_call', stepId, turn },
88
- config
89
- );
99
+
100
+ // Build invoke params - LangChain extracts non-schema fields to config.toolCall
101
+ let invokeParams: Record<string, unknown> = {
102
+ ...call,
103
+ args,
104
+ type: 'tool_call',
105
+ stepId,
106
+ turn,
107
+ };
108
+
109
+ // Inject runtime data for special tools (becomes available at config.toolCall)
110
+ if (call.name === Constants.PROGRAMMATIC_TOOL_CALLING) {
111
+ invokeParams = {
112
+ ...invokeParams,
113
+ toolMap: this.programmaticToolMap,
114
+ programmaticToolDefs: this.programmaticToolDefs,
115
+ };
116
+ } else if (call.name === Constants.TOOL_SEARCH_REGEX) {
117
+ invokeParams = {
118
+ ...invokeParams,
119
+ toolRegistry: this.toolRegistry,
120
+ };
121
+ }
122
+
123
+ const output = await tool.invoke(invokeParams, config);
90
124
  if (
91
125
  (isBaseMessage(output) && output._getType() === 'tool') ||
92
126
  isCommand(output)