@librechat/agents 3.0.41 → 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 (52) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +177 -69
  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 +19 -36
  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 +177 -69
  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 +19 -36
  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 +52 -16
  30. package/dist/types/common/enum.d.ts +1 -1
  31. package/dist/types/graphs/Graph.d.ts +1 -1
  32. package/dist/types/messages/index.d.ts +1 -0
  33. package/dist/types/messages/tools.d.ts +17 -0
  34. package/dist/types/tools/ProgrammaticToolCalling.d.ts +15 -23
  35. package/dist/types/tools/ToolNode.d.ts +9 -7
  36. package/dist/types/types/tools.d.ts +5 -5
  37. package/package.json +1 -1
  38. package/src/agents/AgentContext.ts +205 -80
  39. package/src/agents/__tests__/AgentContext.test.ts +805 -0
  40. package/src/common/enum.ts +1 -1
  41. package/src/graphs/Graph.ts +24 -40
  42. package/src/messages/__tests__/tools.test.ts +473 -0
  43. package/src/messages/index.ts +1 -0
  44. package/src/messages/tools.ts +99 -0
  45. package/src/scripts/code_exec_ptc.ts +78 -21
  46. package/src/scripts/programmatic_exec.ts +3 -3
  47. package/src/scripts/programmatic_exec_agent.ts +4 -4
  48. package/src/tools/ProgrammaticToolCalling.ts +71 -39
  49. package/src/tools/ToolNode.ts +33 -14
  50. package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +9 -9
  51. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +180 -5
  52. package/src/types/tools.ts +3 -5
@@ -4,3 +4,4 @@ export * from './prune';
4
4
  export * from './format';
5
5
  export * from './cache';
6
6
  export * from './content';
7
+ export * from './tools';
@@ -0,0 +1,17 @@
1
+ import type { BaseMessage } from '@langchain/core/messages';
2
+ /**
3
+ * Extracts discovered tool names from tool search results in the current turn.
4
+ * Only processes tool search messages after the latest AI message with tool calls.
5
+ *
6
+ * Similar pattern to formatArtifactPayload - finds relevant messages efficiently
7
+ * by identifying the latest AI parent and only processing subsequent tool messages.
8
+ *
9
+ * @param messages - All messages in the conversation
10
+ * @returns Array of discovered tool names (empty if no new discoveries)
11
+ */
12
+ export declare function extractToolDiscoveries(messages: BaseMessage[]): string[];
13
+ /**
14
+ * Checks if the current turn has any tool search results.
15
+ * Quick check to avoid full extraction when not needed.
16
+ */
17
+ export declare function hasToolSearchInCurrentTurn(messages: BaseMessage[]): boolean;
@@ -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.41",
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) => {
@@ -122,6 +114,8 @@ export class AgentContext {
122
114
  * Used for tool search and programmatic tool calling.
123
115
  */
124
116
  toolRegistry?: t.LCToolRegistry;
117
+ /** Set of tool names discovered via tool search (to be loaded) */
118
+ discoveredToolNames: Set<string> = new Set();
125
119
  /** Instructions for this agent */
126
120
  instructions?: string;
127
121
  /** Additional instructions for this agent */
@@ -137,12 +131,16 @@ export class AgentContext {
137
131
  ContentTypes.TEXT;
138
132
  /** Whether tools should end the workflow */
139
133
  toolEnd: boolean = false;
140
- /** System runnable for this agent */
141
- systemRunnable?: Runnable<
134
+ /** Cached system runnable (created lazily) */
135
+ private cachedSystemRunnable?: Runnable<
142
136
  BaseMessage[],
143
137
  (BaseMessage | SystemMessage)[],
144
138
  RunnableConfig<Record<string, unknown>>
145
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;
146
144
  /** Promise for token calculation initialization */
147
145
  tokenCalculationPromise?: Promise<void>;
148
146
  /** Format content blocks as strings (for legacy compatibility) */
@@ -203,39 +201,145 @@ export class AgentContext {
203
201
  }
204
202
 
205
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');
206
224
 
207
- this.systemRunnable = this.createSystemRunnable();
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 '';
236
+
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
+ );
208
256
  }
209
257
 
210
258
  /**
211
- * 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).
212
262
  */
213
- private createSystemRunnable():
263
+ get systemRunnable():
214
264
  | Runnable<
215
265
  BaseMessage[],
216
266
  (BaseMessage | SystemMessage)[],
217
267
  RunnableConfig<Record<string, unknown>>
218
268
  >
219
269
  | undefined {
220
- let finalInstructions: string | BaseMessageFields | undefined =
221
- 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 ?? '';
222
299
 
223
300
  if (
224
301
  this.additionalInstructions != null &&
225
302
  this.additionalInstructions !== ''
226
303
  ) {
227
- finalInstructions =
228
- finalInstructions != null && finalInstructions
229
- ? `${finalInstructions}\n\n${this.additionalInstructions}`
230
- : 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;
231
337
  }
232
338
 
339
+ let finalInstructions: string | BaseMessageFields = instructionsString;
340
+
233
341
  // Handle Anthropic prompt caching
234
- if (
235
- finalInstructions != null &&
236
- finalInstructions !== '' &&
237
- this.provider === Providers.ANTHROPIC
238
- ) {
342
+ if (this.provider === Providers.ANTHROPIC) {
239
343
  const anthropicOptions = this.clientOptions as
240
344
  | t.AnthropicClientOptions
241
345
  | undefined;
@@ -251,7 +355,7 @@ export class AgentContext {
251
355
  content: [
252
356
  {
253
357
  type: 'text',
254
- text: this.instructions,
358
+ text: instructionsString,
255
359
  cache_control: { type: 'ephemeral' },
256
360
  },
257
361
  ],
@@ -259,19 +363,18 @@ export class AgentContext {
259
363
  }
260
364
  }
261
365
 
262
- if (finalInstructions != null && finalInstructions !== '') {
263
- const systemMessage = new SystemMessage(finalInstructions);
366
+ const systemMessage = new SystemMessage(finalInstructions);
264
367
 
265
- if (this.tokenCounter) {
266
- this.instructionTokens += this.tokenCounter(systemMessage);
267
- }
268
-
269
- return RunnableLambda.from((messages: BaseMessage[]) => {
270
- return [systemMessage, ...messages];
271
- }).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;
272
373
  }
273
374
 
274
- return undefined;
375
+ return RunnableLambda.from((messages: BaseMessage[]) => {
376
+ return [systemMessage, ...messages];
377
+ }).withConfig({ runName: 'prompt' });
275
378
  }
276
379
 
277
380
  /**
@@ -279,6 +382,9 @@ export class AgentContext {
279
382
  */
280
383
  reset(): void {
281
384
  this.instructionTokens = 0;
385
+ this.systemMessageTokens = 0;
386
+ this.cachedSystemRunnable = undefined;
387
+ this.systemRunnableStale = true;
282
388
  this.lastToken = undefined;
283
389
  this.indexTokenCountMap = {};
284
390
  this.currentUsage = undefined;
@@ -286,6 +392,7 @@ export class AgentContext {
286
392
  this.lastStreamCall = undefined;
287
393
  this.tokenTypeSwitch = undefined;
288
394
  this.currentTokenType = ContentTypes.TEXT;
395
+ this.discoveredToolNames.clear();
289
396
  }
290
397
 
291
398
  /**
@@ -345,65 +452,83 @@ export class AgentContext {
345
452
  }
346
453
 
347
454
  /**
348
- * Gets a map of tools that allow programmatic (code_execution) calling.
349
- * Filters toolMap based on toolRegistry's allowed_callers settings.
350
- * @returns ToolMap containing only tools that allow code_execution
455
+ * Gets the tool registry for deferred tools (for tool search).
456
+ * @param onlyDeferred If true, only returns tools with defer_loading=true
457
+ * @returns LCToolRegistry with tool definitions
351
458
  */
352
- getProgrammaticToolMap(): t.ToolMap {
353
- const programmaticMap: t.ToolMap = new Map();
459
+ getDeferredToolRegistry(onlyDeferred: boolean = true): t.LCToolRegistry {
460
+ const registry: t.LCToolRegistry = new Map();
354
461
 
355
- if (!this.toolMap) {
356
- return programmaticMap;
462
+ if (!this.toolRegistry) {
463
+ return registry;
357
464
  }
358
465
 
359
- for (const [name, tool] of this.toolMap) {
360
- const toolDef = this.toolRegistry?.get(name);
361
- if (toolAllowsCaller(toolDef, 'code_execution')) {
362
- programmaticMap.set(name, tool);
466
+ for (const [name, toolDef] of this.toolRegistry) {
467
+ if (!onlyDeferred || toolDef.defer_loading === true) {
468
+ registry.set(name, toolDef);
363
469
  }
364
470
  }
365
471
 
366
- return programmaticMap;
472
+ return registry;
367
473
  }
368
474
 
369
475
  /**
370
- * Gets tool definitions for tools that allow programmatic calling.
371
- * Used to send to the Code API for stub generation.
372
- * @returns Array of LCTool definitions for programmatic tools
476
+ * Marks tools as discovered via tool search.
477
+ * Discovered tools will be included in the next model binding.
478
+ * Only marks system runnable stale if NEW tools were actually added.
479
+ * @param toolNames - Array of discovered tool names
480
+ * @returns true if any new tools were discovered
373
481
  */
374
- getProgrammaticToolDefs(): t.LCTool[] {
375
- if (!this.toolRegistry) {
376
- return [];
377
- }
378
-
379
- const defs: t.LCTool[] = [];
380
- for (const [_name, toolDef] of this.toolRegistry) {
381
- if (toolAllowsCaller(toolDef, 'code_execution')) {
382
- defs.push(toolDef);
482
+ markToolsAsDiscovered(toolNames: string[]): boolean {
483
+ let hasNewDiscoveries = false;
484
+ for (const name of toolNames) {
485
+ if (!this.discoveredToolNames.has(name)) {
486
+ this.discoveredToolNames.add(name);
487
+ hasNewDiscoveries = true;
383
488
  }
384
489
  }
385
-
386
- return defs;
490
+ if (hasNewDiscoveries) {
491
+ this.systemRunnableStale = true;
492
+ }
493
+ return hasNewDiscoveries;
387
494
  }
388
495
 
389
496
  /**
390
- * Gets the tool registry for deferred tools (for tool search).
391
- * @param onlyDeferred If true, only returns tools with defer_loading=true
392
- * @returns LCToolRegistry with tool definitions
497
+ * Gets tools that should be bound to the LLM.
498
+ * Includes:
499
+ * 1. Non-deferred tools with allowed_callers: ['direct']
500
+ * 2. Discovered tools (from tool search)
501
+ * @returns Array of tools to bind to model
393
502
  */
394
- getDeferredToolRegistry(onlyDeferred: boolean = true): t.LCToolRegistry {
395
- const registry: t.LCToolRegistry = new Map();
396
-
397
- if (!this.toolRegistry) {
398
- return registry;
503
+ getToolsForBinding(): t.GraphTools | undefined {
504
+ if (!this.tools || !this.toolRegistry) {
505
+ return this.tools;
399
506
  }
400
507
 
401
- for (const [name, toolDef] of this.toolRegistry) {
402
- if (!onlyDeferred || toolDef.defer_loading === true) {
403
- registry.set(name, toolDef);
508
+ const toolsToInclude = this.tools.filter((tool) => {
509
+ if (!('name' in tool)) {
510
+ return true; // No name, include by default
404
511
  }
405
- }
406
512
 
407
- return registry;
513
+ const toolDef = this.toolRegistry?.get(tool.name);
514
+ if (!toolDef) {
515
+ return true; // Not in registry, include by default
516
+ }
517
+
518
+ // Check if discovered (overrides defer_loading)
519
+ if (this.discoveredToolNames.has(tool.name)) {
520
+ // Discovered tools must still have allowed_callers: ['direct']
521
+ const allowedCallers = toolDef.allowed_callers ?? ['direct'];
522
+ return allowedCallers.includes('direct');
523
+ }
524
+
525
+ // Not discovered: must be direct-callable AND not deferred
526
+ const allowedCallers = toolDef.allowed_callers ?? ['direct'];
527
+ return (
528
+ allowedCallers.includes('direct') && toolDef.defer_loading !== true
529
+ );
530
+ });
531
+
532
+ return toolsToInclude;
408
533
  }
409
534
  }