@librechat/agents 3.0.42 → 3.0.44

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 (51) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +134 -70
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +1 -1
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +7 -13
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/main.cjs +6 -0
  8. package/dist/cjs/main.cjs.map +1 -1
  9. package/dist/cjs/messages/tools.cjs +85 -0
  10. package/dist/cjs/messages/tools.cjs.map +1 -0
  11. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +143 -34
  12. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  13. package/dist/cjs/tools/ToolNode.cjs +30 -13
  14. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  15. package/dist/esm/agents/AgentContext.mjs +134 -70
  16. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  17. package/dist/esm/common/enum.mjs +1 -1
  18. package/dist/esm/common/enum.mjs.map +1 -1
  19. package/dist/esm/graphs/Graph.mjs +8 -14
  20. package/dist/esm/graphs/Graph.mjs.map +1 -1
  21. package/dist/esm/main.mjs +2 -1
  22. package/dist/esm/main.mjs.map +1 -1
  23. package/dist/esm/messages/tools.mjs +82 -0
  24. package/dist/esm/messages/tools.mjs.map +1 -0
  25. package/dist/esm/tools/ProgrammaticToolCalling.mjs +141 -35
  26. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  27. package/dist/esm/tools/ToolNode.mjs +30 -13
  28. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  29. package/dist/types/agents/AgentContext.d.ts +37 -17
  30. package/dist/types/common/enum.d.ts +1 -1
  31. package/dist/types/messages/index.d.ts +1 -0
  32. package/dist/types/messages/tools.d.ts +17 -0
  33. package/dist/types/tools/ProgrammaticToolCalling.d.ts +28 -23
  34. package/dist/types/tools/ToolNode.d.ts +9 -7
  35. package/dist/types/types/tools.d.ts +7 -5
  36. package/package.json +1 -1
  37. package/src/agents/AgentContext.ts +157 -85
  38. package/src/agents/__tests__/AgentContext.test.ts +805 -0
  39. package/src/common/enum.ts +1 -1
  40. package/src/graphs/Graph.ts +9 -21
  41. package/src/messages/__tests__/tools.test.ts +473 -0
  42. package/src/messages/index.ts +1 -0
  43. package/src/messages/tools.ts +99 -0
  44. package/src/scripts/code_exec_ptc.ts +78 -21
  45. package/src/scripts/programmatic_exec.ts +3 -3
  46. package/src/scripts/programmatic_exec_agent.ts +4 -4
  47. package/src/tools/ProgrammaticToolCalling.ts +178 -43
  48. package/src/tools/ToolNode.ts +33 -14
  49. package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +9 -9
  50. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +245 -5
  51. package/src/types/tools.ts +5 -5
@@ -52,12 +52,12 @@ result = await get_weather(city="San Francisco")
52
52
  print(f"Temperature: {result['temperature']}°F")
53
53
  print(f"Condition: {result['condition']}")
54
54
  `,
55
- tools: toolDefinitions,
56
55
  };
57
56
  const toolCall = {
58
57
  name: 'programmatic_code_execution',
59
58
  args,
60
59
  toolMap,
60
+ toolDefs: toolDefinitions,
61
61
  };
62
62
 
63
63
  const output = await ptcTool.invoke(args, { toolCall });
@@ -85,12 +85,12 @@ for member in team:
85
85
 
86
86
  print(f"Grand total: \${total:.2f}")
87
87
  `,
88
- tools: toolDefinitions,
89
88
  };
90
89
  const toolCall = {
91
90
  name: 'programmatic_code_execution',
92
91
  args,
93
92
  toolMap,
93
+ toolDefs: toolDefinitions,
94
94
  };
95
95
 
96
96
  const output = await ptcTool.invoke(args, { toolCall });
@@ -116,12 +116,12 @@ results = await asyncio.gather(*[
116
116
  for city, weather in zip(cities, results):
117
117
  print(f"{city}: {weather['temperature']}°F, {weather['condition']}")
118
118
  `,
119
- tools: toolDefinitions,
120
119
  };
121
120
  const toolCall = {
122
121
  name: 'programmatic_code_execution',
123
122
  args,
124
123
  toolMap,
124
+ toolDefs: toolDefinitions,
125
125
  };
126
126
 
127
127
  const output = await ptcTool.invoke(args, { toolCall });
@@ -150,12 +150,12 @@ if high_spenders:
150
150
  else:
151
151
  print("No high spenders found.")
152
152
  `,
153
- tools: toolDefinitions,
154
153
  };
155
154
  const toolCall = {
156
155
  name: 'programmatic_code_execution',
157
156
  args,
158
157
  toolMap,
158
+ toolDefs: toolDefinitions,
159
159
  };
160
160
 
161
161
  const output = await ptcTool.invoke(args, { toolCall });
@@ -180,12 +180,12 @@ for member in team:
180
180
  else:
181
181
  print("No equipment expenses found")
182
182
  `,
183
- tools: toolDefinitions,
184
183
  };
185
184
  const toolCall = {
186
185
  name: 'programmatic_code_execution',
187
186
  args,
188
187
  toolMap,
188
+ toolDefs: toolDefinitions,
189
189
  };
190
190
 
191
191
  const output = await ptcTool.invoke(args, { toolCall });
@@ -206,12 +206,12 @@ for city in cities:
206
206
  except Exception as e:
207
207
  print(f"{city}: Error - {e}")
208
208
  `,
209
- tools: toolDefinitions,
210
209
  };
211
210
  const toolCall = {
212
211
  name: 'programmatic_code_execution',
213
212
  args,
214
213
  toolMap,
214
+ toolDefs: toolDefinitions,
215
215
  };
216
216
 
217
217
  const output = await ptcTool.invoke(args, { toolCall });
@@ -230,12 +230,12 @@ result2 = await calculator(expression="(10 + 5) / 3")
230
230
  print(f"2 + 2 * 3 = {result1['result']}")
231
231
  print(f"(10 + 5) / 3 = {result2['result']:.2f}")
232
232
  `,
233
- tools: toolDefinitions,
234
233
  };
235
234
  const toolCall = {
236
235
  name: 'programmatic_code_execution',
237
236
  args,
238
237
  toolMap,
238
+ toolDefs: toolDefinitions,
239
239
  };
240
240
 
241
241
  const output = await ptcTool.invoke(args, { toolCall });
@@ -265,12 +265,12 @@ for member, expenses in zip(team, all_expenses):
265
265
  total = sum(e['amount'] for e in expenses)
266
266
  print(f" {member['name']}: \${total:.2f} ({len(expenses)} items)")
267
267
  `,
268
- tools: toolDefinitions,
269
268
  };
270
269
  const toolCall = {
271
270
  name: 'programmatic_code_execution',
272
271
  args,
273
272
  toolMap,
273
+ toolDefs: toolDefinitions,
274
274
  };
275
275
 
276
276
  const output = await ptcTool.invoke(args, { toolCall });
@@ -302,12 +302,12 @@ print(f"SF: {sf['temperature']}°F vs NYC: {nyc['temperature']}°F")
302
302
  difference = abs(sf['temperature'] - nyc['temperature'])
303
303
  print(f"Temperature difference: {difference}°F")
304
304
  `,
305
- tools: [weatherToolDef!],
306
305
  };
307
306
  const toolCall = {
308
307
  name: 'programmatic_code_execution',
309
308
  args,
310
309
  toolMap,
310
+ toolDefs: [weatherToolDef!],
311
311
  };
312
312
 
313
313
  const output = await ptcTool.invoke(args, { toolCall });
@@ -6,16 +6,19 @@
6
6
  import { describe, it, expect, beforeEach } from '@jest/globals';
7
7
  import type * as t from '@/types';
8
8
  import {
9
- executeTools,
10
- formatCompletedResponse,
11
9
  createProgrammaticToolCallingTool,
10
+ formatCompletedResponse,
11
+ extractUsedToolNames,
12
+ filterToolsByUsage,
13
+ executeTools,
14
+ normalizeToPythonIdentifier,
12
15
  } from '../ProgrammaticToolCalling';
13
16
  import {
17
+ createProgrammaticToolRegistry,
14
18
  createGetTeamMembersTool,
15
19
  createGetExpensesTool,
16
20
  createGetWeatherTool,
17
21
  createCalculatorTool,
18
- createProgrammaticToolRegistry,
19
22
  } from '@/test/mockTools';
20
23
 
21
24
  describe('ProgrammaticToolCalling', () => {
@@ -183,6 +186,243 @@ describe('ProgrammaticToolCalling', () => {
183
186
  });
184
187
  });
185
188
 
189
+ describe('normalizeToPythonIdentifier', () => {
190
+ it('converts hyphens to underscores', () => {
191
+ expect(normalizeToPythonIdentifier('my-tool-name')).toBe('my_tool_name');
192
+ });
193
+
194
+ it('converts spaces to underscores', () => {
195
+ expect(normalizeToPythonIdentifier('my tool name')).toBe('my_tool_name');
196
+ });
197
+
198
+ it('leaves underscores unchanged', () => {
199
+ expect(normalizeToPythonIdentifier('my_tool_name')).toBe('my_tool_name');
200
+ });
201
+
202
+ it('handles mixed hyphens and underscores', () => {
203
+ expect(normalizeToPythonIdentifier('my-tool_name-v2')).toBe(
204
+ 'my_tool_name_v2'
205
+ );
206
+ });
207
+
208
+ it('handles MCP-style names with hyphens', () => {
209
+ expect(
210
+ normalizeToPythonIdentifier('create_spreadsheet_mcp_Google-Workspace')
211
+ ).toBe('create_spreadsheet_mcp_Google_Workspace');
212
+ });
213
+
214
+ it('removes invalid characters', () => {
215
+ expect(normalizeToPythonIdentifier('tool@name!v2')).toBe('toolnamev2');
216
+ expect(normalizeToPythonIdentifier('get.data.v2')).toBe('getdatav2');
217
+ });
218
+
219
+ it('prefixes with underscore if starts with number', () => {
220
+ expect(normalizeToPythonIdentifier('123tool')).toBe('_123tool');
221
+ expect(normalizeToPythonIdentifier('1-tool')).toBe('_1_tool');
222
+ });
223
+
224
+ it('appends _tool suffix for Python keywords', () => {
225
+ expect(normalizeToPythonIdentifier('return')).toBe('return_tool');
226
+ expect(normalizeToPythonIdentifier('async')).toBe('async_tool');
227
+ expect(normalizeToPythonIdentifier('import')).toBe('import_tool');
228
+ });
229
+ });
230
+
231
+ describe('extractUsedToolNames', () => {
232
+ const createToolMap = (names: string[]): Map<string, string> => {
233
+ const map = new Map<string, string>();
234
+ for (const name of names) {
235
+ map.set(normalizeToPythonIdentifier(name), name);
236
+ }
237
+ return map;
238
+ };
239
+
240
+ const availableTools = createToolMap([
241
+ 'get_weather',
242
+ 'get_team_members',
243
+ 'get_expenses',
244
+ 'calculator',
245
+ 'search_docs',
246
+ ]);
247
+
248
+ it('extracts single tool name from simple code', () => {
249
+ const code = `result = await get_weather(city="SF")
250
+ print(result)`;
251
+ const used = extractUsedToolNames(code, availableTools);
252
+
253
+ expect(used.size).toBe(1);
254
+ expect(used.has('get_weather')).toBe(true);
255
+ });
256
+
257
+ it('extracts multiple tool names from code', () => {
258
+ const code = `team = await get_team_members()
259
+ for member in team:
260
+ expenses = await get_expenses(user_id=member['id'])
261
+ print(f"{member['name']}: {sum(e['amount'] for e in expenses)}")`;
262
+
263
+ const used = extractUsedToolNames(code, availableTools);
264
+
265
+ expect(used.size).toBe(2);
266
+ expect(used.has('get_team_members')).toBe(true);
267
+ expect(used.has('get_expenses')).toBe(true);
268
+ });
269
+
270
+ it('extracts tools from asyncio.gather calls', () => {
271
+ const code = `results = await asyncio.gather(
272
+ get_weather(city="SF"),
273
+ get_weather(city="NYC"),
274
+ get_expenses(user_id="u1")
275
+ )`;
276
+ const used = extractUsedToolNames(code, availableTools);
277
+
278
+ expect(used.size).toBe(2);
279
+ expect(used.has('get_weather')).toBe(true);
280
+ expect(used.has('get_expenses')).toBe(true);
281
+ });
282
+
283
+ it('does not match partial tool names', () => {
284
+ const code = `# Using get_weather_data instead
285
+ result = await get_weather_data(city="SF")`;
286
+
287
+ const used = extractUsedToolNames(code, availableTools);
288
+ expect(used.has('get_weather')).toBe(false);
289
+ });
290
+
291
+ it('matches tool names in different contexts', () => {
292
+ const code = `# direct call
293
+ x = await calculator(expression="1+1")
294
+ # in list comprehension
295
+ results = [await get_weather(city=c) for c in cities]
296
+ # conditional
297
+ if condition:
298
+ await get_team_members()`;
299
+
300
+ const used = extractUsedToolNames(code, availableTools);
301
+
302
+ expect(used.size).toBe(3);
303
+ expect(used.has('calculator')).toBe(true);
304
+ expect(used.has('get_weather')).toBe(true);
305
+ expect(used.has('get_team_members')).toBe(true);
306
+ });
307
+
308
+ it('returns empty set when no tools are used', () => {
309
+ const code = `print("Hello, World!")
310
+ x = 1 + 2`;
311
+
312
+ const used = extractUsedToolNames(code, availableTools);
313
+ expect(used.size).toBe(0);
314
+ });
315
+
316
+ it('handles tool names with special characters via normalization', () => {
317
+ const specialTools = createToolMap(['get_data.v2', 'calc+plus']);
318
+ const code = `await get_datav2()
319
+ await calcplus()`;
320
+
321
+ const used = extractUsedToolNames(code, specialTools);
322
+
323
+ expect(used.has('get_data.v2')).toBe(true);
324
+ expect(used.has('calc+plus')).toBe(true);
325
+ });
326
+
327
+ it('matches hyphenated tool names using underscore in code', () => {
328
+ const mcpTools = createToolMap([
329
+ 'create_spreadsheet_mcp_Google-Workspace',
330
+ 'search_gmail_mcp_Google-Workspace',
331
+ ]);
332
+ const code = `result = await create_spreadsheet_mcp_Google_Workspace(title="Test")
333
+ print(result)`;
334
+
335
+ const used = extractUsedToolNames(code, mcpTools);
336
+
337
+ expect(used.size).toBe(1);
338
+ expect(used.has('create_spreadsheet_mcp_Google-Workspace')).toBe(true);
339
+ });
340
+ });
341
+
342
+ describe('filterToolsByUsage', () => {
343
+ const allToolDefs: t.LCTool[] = [
344
+ {
345
+ name: 'get_weather',
346
+ description: 'Get weather for a city',
347
+ parameters: {
348
+ type: 'object',
349
+ properties: { city: { type: 'string' } },
350
+ },
351
+ },
352
+ {
353
+ name: 'get_team_members',
354
+ description: 'Get team members',
355
+ parameters: { type: 'object', properties: {} },
356
+ },
357
+ {
358
+ name: 'get_expenses',
359
+ description: 'Get expenses for a user',
360
+ parameters: {
361
+ type: 'object',
362
+ properties: { user_id: { type: 'string' } },
363
+ },
364
+ },
365
+ {
366
+ name: 'calculator',
367
+ description: 'Evaluate an expression',
368
+ parameters: {
369
+ type: 'object',
370
+ properties: { expression: { type: 'string' } },
371
+ },
372
+ },
373
+ ];
374
+
375
+ it('filters to only used tools', () => {
376
+ const code = `result = await get_weather(city="SF")
377
+ print(result)`;
378
+
379
+ const filtered = filterToolsByUsage(allToolDefs, code);
380
+
381
+ expect(filtered).toHaveLength(1);
382
+ expect(filtered[0].name).toBe('get_weather');
383
+ });
384
+
385
+ it('filters to multiple used tools', () => {
386
+ const code = `team = await get_team_members()
387
+ for member in team:
388
+ expenses = await get_expenses(user_id=member['id'])`;
389
+
390
+ const filtered = filterToolsByUsage(allToolDefs, code);
391
+
392
+ expect(filtered).toHaveLength(2);
393
+ expect(filtered.map((t) => t.name).sort()).toEqual([
394
+ 'get_expenses',
395
+ 'get_team_members',
396
+ ]);
397
+ });
398
+
399
+ it('returns all tools when no tools are detected', () => {
400
+ const code = 'print("Hello, World!")';
401
+
402
+ const filtered = filterToolsByUsage(allToolDefs, code);
403
+
404
+ expect(filtered).toHaveLength(4);
405
+ });
406
+
407
+ it('preserves tool definition structure', () => {
408
+ const code = 'await calculator(expression="2+2")';
409
+
410
+ const filtered = filterToolsByUsage(allToolDefs, code);
411
+
412
+ expect(filtered).toHaveLength(1);
413
+ expect(filtered[0]).toEqual(allToolDefs[3]);
414
+ expect(filtered[0].parameters).toBeDefined();
415
+ expect(filtered[0].description).toBe('Evaluate an expression');
416
+ });
417
+
418
+ it('handles empty tool definitions', () => {
419
+ const code = 'await get_weather(city="SF")';
420
+ const filtered = filterToolsByUsage([], code);
421
+
422
+ expect(filtered).toHaveLength(0);
423
+ });
424
+ });
425
+
186
426
  describe('formatCompletedResponse', () => {
187
427
  it('formats response with stdout', () => {
188
428
  const response: t.ProgrammaticExecutionResponse = {
@@ -332,7 +572,7 @@ describe('ProgrammaticToolCalling', () => {
332
572
  name: 'programmatic_code_execution',
333
573
  args,
334
574
  toolMap,
335
- // No programmaticToolDefs
575
+ // No `toolDefs`
336
576
  };
337
577
 
338
578
  await expect(ptcTool.invoke(args, { toolCall })).rejects.toThrow(
@@ -340,7 +580,7 @@ describe('ProgrammaticToolCalling', () => {
340
580
  );
341
581
  });
342
582
 
343
- it('uses programmaticToolDefs from config when tools not provided', async () => {
583
+ it('uses toolDefs from config when tools not provided', async () => {
344
584
  // Skip this test - requires mocking fetch which has complex typing
345
585
  // This functionality is tested in the live script tests instead
346
586
  });
@@ -35,11 +35,7 @@ export type ToolNodeOptions = {
35
35
  data: ToolErrorData,
36
36
  metadata?: Record<string, unknown>
37
37
  ) => Promise<void>;
38
- /** Tools available for programmatic code execution (allowed_callers includes 'code_execution') */
39
- programmaticToolMap?: ToolMap;
40
- /** Tool definitions for programmatic code execution (sent to Code API for stub generation) */
41
- programmaticToolDefs?: LCTool[];
42
- /** Tool registry for tool search (deferred tool definitions) */
38
+ /** Tool registry for lazy computation of programmatic tools and tool search */
43
39
  toolRegistry?: LCToolRegistry;
44
40
  };
45
41
 
@@ -128,6 +124,8 @@ export type LCTool = {
128
124
  /** Map of tool names to tool definitions */
129
125
  export type LCToolRegistry = Map<string, LCTool>;
130
126
 
127
+ export type ProgrammaticCache = { toolMap: ToolMap; toolDefs: LCTool[] };
128
+
131
129
  /** Parameters for creating a Tool Search Regex tool */
132
130
  export type ToolSearchRegexParams = {
133
131
  apiKey?: string;
@@ -241,6 +239,8 @@ export type ProgrammaticToolCallingParams = {
241
239
  maxRoundTrips?: number;
242
240
  /** HTTP proxy URL */
243
241
  proxy?: string;
242
+ /** Enable debug logging (or set PTC_DEBUG=true env var) */
243
+ debug?: boolean;
244
244
  /** Environment variable key for API key */
245
245
  [key: string]: unknown;
246
246
  };