@librechat/agents 3.0.42 → 3.0.43

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 +5 -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 +55 -32
  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 +54 -33
  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 +15 -23
  34. package/dist/types/tools/ToolNode.d.ts +9 -7
  35. package/dist/types/types/tools.d.ts +5 -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 +71 -39
  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 +180 -5
  51. package/src/types/tools.ts +3 -5
@@ -3,40 +3,32 @@ import { DynamicStructuredTool } from '@langchain/core/tools';
3
3
  import type * as t from '@/types';
4
4
  declare const ProgrammaticToolCallingSchema: z.ZodObject<{
5
5
  code: z.ZodString;
6
- tools: z.ZodOptional<z.ZodArray<z.ZodObject<{
7
- name: z.ZodString;
8
- description: z.ZodOptional<z.ZodString>;
9
- parameters: z.ZodAny;
10
- }, "strip", z.ZodTypeAny, {
11
- name: string;
12
- description?: string | undefined;
13
- parameters?: any;
14
- }, {
15
- name: string;
16
- description?: string | undefined;
17
- parameters?: any;
18
- }>, "many">>;
19
6
  session_id: z.ZodOptional<z.ZodString>;
20
7
  timeout: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
21
8
  }, "strip", z.ZodTypeAny, {
22
9
  code: string;
23
10
  timeout: number;
24
- tools?: {
25
- name: string;
26
- description?: string | undefined;
27
- parameters?: any;
28
- }[] | undefined;
29
11
  session_id?: string | undefined;
30
12
  }, {
31
13
  code: string;
32
- tools?: {
33
- name: string;
34
- description?: string | undefined;
35
- parameters?: any;
36
- }[] | undefined;
37
14
  timeout?: number | undefined;
38
15
  session_id?: string | undefined;
39
16
  }>;
17
+ /**
18
+ * Extracts tool names that are actually called in the Python code.
19
+ * Matches patterns like `await tool_name(`, `tool_name(`, and asyncio.gather calls.
20
+ * @param code - The Python code to analyze
21
+ * @param availableToolNames - Set of available tool names to match against
22
+ * @returns Set of tool names found in the code
23
+ */
24
+ export declare function extractUsedToolNames(code: string, availableToolNames: Set<string>): Set<string>;
25
+ /**
26
+ * Filters tool definitions to only include tools actually used in the code.
27
+ * @param toolDefs - All available tool definitions
28
+ * @param code - The Python code to analyze
29
+ * @returns Filtered array of tool definitions
30
+ */
31
+ export declare function filterToolsByUsage(toolDefs: t.LCTool[], code: string): t.LCTool[];
40
32
  /**
41
33
  * Makes an HTTP request to the Code API.
42
34
  * @param endpoint - The API endpoint URL
@@ -5,7 +5,6 @@ import type { BaseMessage } from '@langchain/core/messages';
5
5
  import type * as t from '@/types';
6
6
  import { RunnableCallable } from '@/utils';
7
7
  export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
8
- tools: t.GenericTool[];
9
8
  private toolMap;
10
9
  private loadRuntimeTools?;
11
10
  handleToolErrors: boolean;
@@ -13,13 +12,16 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
13
12
  toolCallStepIds?: Map<string, string>;
14
13
  errorHandler?: t.ToolNodeConstructorParams['errorHandler'];
15
14
  private toolUsageCount;
16
- /** Tools available for programmatic code execution */
17
- private programmaticToolMap?;
18
- /** Tool definitions for programmatic code execution (sent to Code API) */
19
- private programmaticToolDefs?;
20
- /** Tool registry for tool search (deferred tools) */
15
+ /** Tool registry for filtering (lazy computation of programmatic maps) */
21
16
  private toolRegistry?;
22
- constructor({ tools, toolMap, name, tags, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, programmaticToolMap, programmaticToolDefs, toolRegistry, }: t.ToolNodeConstructorParams);
17
+ /** Cached programmatic tools (computed once on first PTC call) */
18
+ private programmaticCache?;
19
+ constructor({ tools, toolMap, name, tags, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, }: t.ToolNodeConstructorParams);
20
+ /**
21
+ * Returns cached programmatic tools, computing once on first access.
22
+ * Single iteration builds both toolMap and toolDefs simultaneously.
23
+ */
24
+ private getProgrammaticTools;
23
25
  /**
24
26
  * Returns a snapshot of the current tool usage counts.
25
27
  * @returns A ReadonlyMap where keys are tool names and values are their usage counts.
@@ -25,11 +25,7 @@ export type ToolNodeOptions = {
25
25
  loadRuntimeTools?: ToolRefGenerator;
26
26
  toolCallStepIds?: Map<string, string>;
27
27
  errorHandler?: (data: ToolErrorData, metadata?: Record<string, unknown>) => Promise<void>;
28
- /** Tools available for programmatic code execution (allowed_callers includes 'code_execution') */
29
- programmaticToolMap?: ToolMap;
30
- /** Tool definitions for programmatic code execution (sent to Code API for stub generation) */
31
- programmaticToolDefs?: LCTool[];
32
- /** Tool registry for tool search (deferred tool definitions) */
28
+ /** Tool registry for lazy computation of programmatic tools and tool search */
33
29
  toolRegistry?: LCToolRegistry;
34
30
  };
35
31
  export type ToolNodeConstructorParams = ToolRefs & ToolNodeOptions;
@@ -97,6 +93,10 @@ export type LCTool = {
97
93
  };
98
94
  /** Map of tool names to tool definitions */
99
95
  export type LCToolRegistry = Map<string, LCTool>;
96
+ export type ProgrammaticCache = {
97
+ toolMap: ToolMap;
98
+ toolDefs: LCTool[];
99
+ };
100
100
  /** Parameters for creating a Tool Search Regex tool */
101
101
  export type ToolSearchRegexParams = {
102
102
  apiKey?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.0.42",
3
+ "version": "3.0.43",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -13,19 +13,6 @@ import type * as t from '@/types';
13
13
  import type { createPruneMessages } from '@/messages';
14
14
  import { ContentTypes, Providers } from '@/common';
15
15
 
16
- /**
17
- * Checks if a tool allows the specified caller type.
18
- * Default is 'direct' only if allowed_callers is not specified.
19
- */
20
- function toolAllowsCaller(
21
- toolDef: t.LCTool | undefined,
22
- caller: t.AllowedCaller
23
- ): boolean {
24
- if (!toolDef) return false;
25
- const allowedCallers = toolDef.allowed_callers ?? ['direct'];
26
- return allowedCallers.includes(caller);
27
- }
28
-
29
16
  /**
30
17
  * Encapsulates agent-specific state that can vary between agents in a multi-agent system
31
18
  */
@@ -73,12 +60,17 @@ export class AgentContext {
73
60
  });
74
61
 
75
62
  if (tokenCounter) {
63
+ // Initialize system runnable BEFORE async tool token calculation
64
+ // This ensures system message tokens are in instructionTokens before
65
+ // updateTokenMapWithInstructions is called
66
+ agentContext.initializeSystemRunnable();
67
+
76
68
  const tokenMap = indexTokenCountMap || {};
77
69
  agentContext.indexTokenCountMap = tokenMap;
78
70
  agentContext.tokenCalculationPromise = agentContext
79
71
  .calculateInstructionTokens(tokenCounter)
80
72
  .then(() => {
81
- // Update token map with instruction tokens
73
+ // Update token map with instruction tokens (includes system + tool tokens)
82
74
  agentContext.updateTokenMapWithInstructions(tokenMap);
83
75
  })
84
76
  .catch((err) => {
@@ -139,12 +131,16 @@ export class AgentContext {
139
131
  ContentTypes.TEXT;
140
132
  /** Whether tools should end the workflow */
141
133
  toolEnd: boolean = false;
142
- /** System runnable for this agent */
143
- systemRunnable?: Runnable<
134
+ /** Cached system runnable (created lazily) */
135
+ private cachedSystemRunnable?: Runnable<
144
136
  BaseMessage[],
145
137
  (BaseMessage | SystemMessage)[],
146
138
  RunnableConfig<Record<string, unknown>>
147
139
  >;
140
+ /** Whether system runnable needs rebuild (set when discovered tools change) */
141
+ private systemRunnableStale: boolean = true;
142
+ /** Cached system message token count (separate from tool tokens) */
143
+ private systemMessageTokens: number = 0;
148
144
  /** Promise for token calculation initialization */
149
145
  tokenCalculationPromise?: Promise<void>;
150
146
  /** Format content blocks as strings (for legacy compatibility) */
@@ -205,39 +201,145 @@ export class AgentContext {
205
201
  }
206
202
 
207
203
  this.useLegacyContent = useLegacyContent ?? false;
204
+ }
205
+
206
+ /**
207
+ * Builds instructions text for tools that are ONLY callable via programmatic code execution.
208
+ * These tools cannot be called directly by the LLM but are available through the
209
+ * run_tools_with_code tool.
210
+ *
211
+ * Includes:
212
+ * - Code_execution-only tools that are NOT deferred
213
+ * - Code_execution-only tools that ARE deferred but have been discovered via tool search
214
+ */
215
+ private buildProgrammaticOnlyToolsInstructions(): string {
216
+ if (!this.toolRegistry) return '';
217
+
218
+ const programmaticOnlyTools: t.LCTool[] = [];
219
+ for (const [name, toolDef] of this.toolRegistry) {
220
+ const allowedCallers = toolDef.allowed_callers ?? ['direct'];
221
+ const isCodeExecutionOnly =
222
+ allowedCallers.includes('code_execution') &&
223
+ !allowedCallers.includes('direct');
224
+
225
+ if (!isCodeExecutionOnly) continue;
226
+
227
+ // Include if: not deferred OR deferred but discovered
228
+ const isDeferred = toolDef.defer_loading === true;
229
+ const isDiscovered = this.discoveredToolNames.has(name);
230
+ if (!isDeferred || isDiscovered) {
231
+ programmaticOnlyTools.push(toolDef);
232
+ }
233
+ }
234
+
235
+ if (programmaticOnlyTools.length === 0) return '';
208
236
 
209
- this.systemRunnable = this.createSystemRunnable();
237
+ const toolDescriptions = programmaticOnlyTools
238
+ .map((tool) => {
239
+ let desc = `- **${tool.name}**`;
240
+ if (tool.description != null && tool.description !== '') {
241
+ desc += `: ${tool.description}`;
242
+ }
243
+ if (tool.parameters) {
244
+ desc += `\n Parameters: ${JSON.stringify(tool.parameters, null, 2).replace(/\n/g, '\n ')}`;
245
+ }
246
+ return desc;
247
+ })
248
+ .join('\n\n');
249
+
250
+ return (
251
+ '\n\n## Programmatic-Only Tools\n\n' +
252
+ 'The following tools are available exclusively through the `run_tools_with_code` tool. ' +
253
+ 'You cannot call these tools directly; instead, use `run_tools_with_code` with Python code that invokes them.\n\n' +
254
+ toolDescriptions
255
+ );
210
256
  }
211
257
 
212
258
  /**
213
- * Create system runnable from instructions and calculate tokens if tokenCounter is available
259
+ * Gets the system runnable, creating it lazily if needed.
260
+ * Includes instructions, additional instructions, and programmatic-only tools documentation.
261
+ * Only rebuilds when marked stale (via markToolsAsDiscovered).
214
262
  */
215
- private createSystemRunnable():
263
+ get systemRunnable():
216
264
  | Runnable<
217
265
  BaseMessage[],
218
266
  (BaseMessage | SystemMessage)[],
219
267
  RunnableConfig<Record<string, unknown>>
220
268
  >
221
269
  | undefined {
222
- let finalInstructions: string | BaseMessageFields | undefined =
223
- this.instructions;
270
+ // Return cached if not stale
271
+ if (!this.systemRunnableStale && this.cachedSystemRunnable !== undefined) {
272
+ return this.cachedSystemRunnable;
273
+ }
274
+
275
+ // Stale or first access - rebuild
276
+ const instructionsString = this.buildInstructionsString();
277
+ this.cachedSystemRunnable = this.buildSystemRunnable(instructionsString);
278
+ this.systemRunnableStale = false;
279
+ return this.cachedSystemRunnable;
280
+ }
281
+
282
+ /**
283
+ * Explicitly initializes the system runnable.
284
+ * Call this before async token calculation to ensure system message tokens are counted first.
285
+ */
286
+ initializeSystemRunnable(): void {
287
+ if (this.systemRunnableStale || this.cachedSystemRunnable === undefined) {
288
+ const instructionsString = this.buildInstructionsString();
289
+ this.cachedSystemRunnable = this.buildSystemRunnable(instructionsString);
290
+ this.systemRunnableStale = false;
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Builds the raw instructions string (without creating SystemMessage).
296
+ */
297
+ private buildInstructionsString(): string {
298
+ let result = this.instructions ?? '';
224
299
 
225
300
  if (
226
301
  this.additionalInstructions != null &&
227
302
  this.additionalInstructions !== ''
228
303
  ) {
229
- finalInstructions =
230
- finalInstructions != null && finalInstructions
231
- ? `${finalInstructions}\n\n${this.additionalInstructions}`
232
- : this.additionalInstructions;
304
+ result = result
305
+ ? `${result}\n\n${this.additionalInstructions}`
306
+ : this.additionalInstructions;
307
+ }
308
+
309
+ const programmaticToolsDoc = this.buildProgrammaticOnlyToolsInstructions();
310
+ if (programmaticToolsDoc) {
311
+ result = result
312
+ ? `${result}${programmaticToolsDoc}`
313
+ : programmaticToolsDoc;
314
+ }
315
+
316
+ return result;
317
+ }
318
+
319
+ /**
320
+ * Build system runnable from pre-built instructions string.
321
+ * Only called when content has actually changed.
322
+ */
323
+ private buildSystemRunnable(
324
+ instructionsString: string
325
+ ):
326
+ | Runnable<
327
+ BaseMessage[],
328
+ (BaseMessage | SystemMessage)[],
329
+ RunnableConfig<Record<string, unknown>>
330
+ >
331
+ | undefined {
332
+ if (!instructionsString) {
333
+ // Remove previous tokens if we had a system message before
334
+ this.instructionTokens -= this.systemMessageTokens;
335
+ this.systemMessageTokens = 0;
336
+ return undefined;
233
337
  }
234
338
 
339
+ let finalInstructions: string | BaseMessageFields = instructionsString;
340
+
235
341
  // Handle Anthropic prompt caching
236
- if (
237
- finalInstructions != null &&
238
- finalInstructions !== '' &&
239
- this.provider === Providers.ANTHROPIC
240
- ) {
342
+ if (this.provider === Providers.ANTHROPIC) {
241
343
  const anthropicOptions = this.clientOptions as
242
344
  | t.AnthropicClientOptions
243
345
  | undefined;
@@ -253,7 +355,7 @@ export class AgentContext {
253
355
  content: [
254
356
  {
255
357
  type: 'text',
256
- text: this.instructions,
358
+ text: instructionsString,
257
359
  cache_control: { type: 'ephemeral' },
258
360
  },
259
361
  ],
@@ -261,19 +363,18 @@ export class AgentContext {
261
363
  }
262
364
  }
263
365
 
264
- if (finalInstructions != null && finalInstructions !== '') {
265
- const systemMessage = new SystemMessage(finalInstructions);
266
-
267
- if (this.tokenCounter) {
268
- this.instructionTokens += this.tokenCounter(systemMessage);
269
- }
366
+ const systemMessage = new SystemMessage(finalInstructions);
270
367
 
271
- return RunnableLambda.from((messages: BaseMessage[]) => {
272
- return [systemMessage, ...messages];
273
- }).withConfig({ runName: 'prompt' });
368
+ // Update token counts (subtract old, add new)
369
+ if (this.tokenCounter) {
370
+ this.instructionTokens -= this.systemMessageTokens;
371
+ this.systemMessageTokens = this.tokenCounter(systemMessage);
372
+ this.instructionTokens += this.systemMessageTokens;
274
373
  }
275
374
 
276
- return undefined;
375
+ return RunnableLambda.from((messages: BaseMessage[]) => {
376
+ return [systemMessage, ...messages];
377
+ }).withConfig({ runName: 'prompt' });
277
378
  }
278
379
 
279
380
  /**
@@ -281,6 +382,9 @@ export class AgentContext {
281
382
  */
282
383
  reset(): void {
283
384
  this.instructionTokens = 0;
385
+ this.systemMessageTokens = 0;
386
+ this.cachedSystemRunnable = undefined;
387
+ this.systemRunnableStale = true;
284
388
  this.lastToken = undefined;
285
389
  this.indexTokenCountMap = {};
286
390
  this.currentUsage = undefined;
@@ -347,48 +451,6 @@ export class AgentContext {
347
451
  this.instructionTokens += toolTokens;
348
452
  }
349
453
 
350
- /**
351
- * Gets a map of tools that allow programmatic (code_execution) calling.
352
- * Filters toolMap based on toolRegistry's allowed_callers settings.
353
- * @returns ToolMap containing only tools that allow code_execution
354
- */
355
- getProgrammaticToolMap(): t.ToolMap {
356
- const programmaticMap: t.ToolMap = new Map();
357
-
358
- if (!this.toolMap) {
359
- return programmaticMap;
360
- }
361
-
362
- for (const [name, tool] of this.toolMap) {
363
- const toolDef = this.toolRegistry?.get(name);
364
- if (toolAllowsCaller(toolDef, 'code_execution')) {
365
- programmaticMap.set(name, tool);
366
- }
367
- }
368
-
369
- return programmaticMap;
370
- }
371
-
372
- /**
373
- * Gets tool definitions for tools that allow programmatic calling.
374
- * Used to send to the Code API for stub generation.
375
- * @returns Array of LCTool definitions for programmatic tools
376
- */
377
- getProgrammaticToolDefs(): t.LCTool[] {
378
- if (!this.toolRegistry) {
379
- return [];
380
- }
381
-
382
- const defs: t.LCTool[] = [];
383
- for (const [_name, toolDef] of this.toolRegistry) {
384
- if (toolAllowsCaller(toolDef, 'code_execution')) {
385
- defs.push(toolDef);
386
- }
387
- }
388
-
389
- return defs;
390
- }
391
-
392
454
  /**
393
455
  * Gets the tool registry for deferred tools (for tool search).
394
456
  * @param onlyDeferred If true, only returns tools with defer_loading=true
@@ -413,12 +475,22 @@ export class AgentContext {
413
475
  /**
414
476
  * Marks tools as discovered via tool search.
415
477
  * Discovered tools will be included in the next model binding.
478
+ * Only marks system runnable stale if NEW tools were actually added.
416
479
  * @param toolNames - Array of discovered tool names
480
+ * @returns true if any new tools were discovered
417
481
  */
418
- markToolsAsDiscovered(toolNames: string[]): void {
482
+ markToolsAsDiscovered(toolNames: string[]): boolean {
483
+ let hasNewDiscoveries = false;
419
484
  for (const name of toolNames) {
420
- this.discoveredToolNames.add(name);
485
+ if (!this.discoveredToolNames.has(name)) {
486
+ this.discoveredToolNames.add(name);
487
+ hasNewDiscoveries = true;
488
+ }
489
+ }
490
+ if (hasNewDiscoveries) {
491
+ this.systemRunnableStale = true;
421
492
  }
493
+ return hasNewDiscoveries;
422
494
  }
423
495
 
424
496
  /**