@librechat/agents 3.0.66 → 3.0.67

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 (37) hide show
  1. package/dist/cjs/common/enum.cjs +1 -1
  2. package/dist/cjs/common/enum.cjs.map +1 -1
  3. package/dist/cjs/main.cjs +8 -7
  4. package/dist/cjs/main.cjs.map +1 -1
  5. package/dist/cjs/messages/tools.cjs +2 -2
  6. package/dist/cjs/messages/tools.cjs.map +1 -1
  7. package/dist/cjs/tools/ToolNode.cjs +1 -1
  8. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  9. package/dist/cjs/tools/{ToolSearchRegex.cjs → ToolSearch.cjs} +154 -66
  10. package/dist/cjs/tools/ToolSearch.cjs.map +1 -0
  11. package/dist/esm/common/enum.mjs +1 -1
  12. package/dist/esm/common/enum.mjs.map +1 -1
  13. package/dist/esm/main.mjs +1 -1
  14. package/dist/esm/messages/tools.mjs +2 -2
  15. package/dist/esm/messages/tools.mjs.map +1 -1
  16. package/dist/esm/tools/ToolNode.mjs +1 -1
  17. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  18. package/dist/esm/tools/{ToolSearchRegex.mjs → ToolSearch.mjs} +153 -66
  19. package/dist/esm/tools/ToolSearch.mjs.map +1 -0
  20. package/dist/types/common/enum.d.ts +1 -1
  21. package/dist/types/index.d.ts +1 -1
  22. package/dist/types/tools/{ToolSearchRegex.d.ts → ToolSearch.d.ts} +33 -23
  23. package/dist/types/types/tools.d.ts +5 -1
  24. package/package.json +2 -2
  25. package/src/common/enum.ts +1 -1
  26. package/src/index.ts +1 -1
  27. package/src/messages/__tests__/tools.test.ts +21 -21
  28. package/src/messages/tools.ts +2 -2
  29. package/src/scripts/programmatic_exec_agent.ts +4 -4
  30. package/src/scripts/{tool_search_regex.ts → tool_search.ts} +5 -5
  31. package/src/tools/ToolNode.ts +1 -1
  32. package/src/tools/{ToolSearchRegex.ts → ToolSearch.ts} +195 -74
  33. package/src/tools/__tests__/{ToolSearchRegex.integration.test.ts → ToolSearch.integration.test.ts} +6 -6
  34. package/src/tools/__tests__/{ToolSearchRegex.test.ts → ToolSearch.test.ts} +212 -3
  35. package/src/types/tools.ts +6 -1
  36. package/dist/cjs/tools/ToolSearchRegex.cjs.map +0 -1
  37. package/dist/esm/tools/ToolSearchRegex.mjs.map +0 -1
@@ -41,7 +41,7 @@ describe('Tool Discovery Functions', () => {
41
41
  return new ToolMessage({
42
42
  content: `Found ${discoveredTools.length} tools`,
43
43
  tool_call_id: toolCallId,
44
- name: Constants.TOOL_SEARCH_REGEX,
44
+ name: Constants.TOOL_SEARCH,
45
45
  artifact: {
46
46
  tool_references: discoveredTools.map((name) => ({
47
47
  tool_name: name,
@@ -79,7 +79,7 @@ describe('Tool Discovery Functions', () => {
79
79
  createAIMessage('Searching...', [
80
80
  {
81
81
  id: 'call_1',
82
- name: Constants.TOOL_SEARCH_REGEX,
82
+ name: Constants.TOOL_SEARCH,
83
83
  args: { pattern: 'database' },
84
84
  },
85
85
  ]),
@@ -97,12 +97,12 @@ describe('Tool Discovery Functions', () => {
97
97
  createAIMessage('Searching...', [
98
98
  {
99
99
  id: 'call_1',
100
- name: Constants.TOOL_SEARCH_REGEX,
100
+ name: Constants.TOOL_SEARCH,
101
101
  args: { pattern: 'database' },
102
102
  },
103
103
  {
104
104
  id: 'call_2',
105
- name: Constants.TOOL_SEARCH_REGEX,
105
+ name: Constants.TOOL_SEARCH,
106
106
  args: { pattern: 'file' },
107
107
  },
108
108
  ]),
@@ -137,7 +137,7 @@ describe('Tool Discovery Functions', () => {
137
137
  new ToolMessage({
138
138
  content: 'Some result',
139
139
  tool_call_id: 'orphan_call',
140
- name: Constants.TOOL_SEARCH_REGEX,
140
+ name: Constants.TOOL_SEARCH,
141
141
  }),
142
142
  ];
143
143
 
@@ -153,7 +153,7 @@ describe('Tool Discovery Functions', () => {
153
153
  createAIMessage('Searching...', [
154
154
  {
155
155
  id: 'old_call',
156
- name: Constants.TOOL_SEARCH_REGEX,
156
+ name: Constants.TOOL_SEARCH,
157
157
  args: { pattern: 'old' },
158
158
  },
159
159
  ]),
@@ -163,7 +163,7 @@ describe('Tool Discovery Functions', () => {
163
163
  createAIMessage('Searching again...', [
164
164
  {
165
165
  id: 'new_call',
166
- name: Constants.TOOL_SEARCH_REGEX,
166
+ name: Constants.TOOL_SEARCH,
167
167
  args: { pattern: 'new' },
168
168
  },
169
169
  ]),
@@ -182,7 +182,7 @@ describe('Tool Discovery Functions', () => {
182
182
  createAIMessage('Working...', [
183
183
  {
184
184
  id: 'search_call',
185
- name: Constants.TOOL_SEARCH_REGEX,
185
+ name: Constants.TOOL_SEARCH,
186
186
  args: { pattern: 'test' },
187
187
  },
188
188
  { id: 'other_call', name: 'get_weather', args: { city: 'NYC' } },
@@ -202,14 +202,14 @@ describe('Tool Discovery Functions', () => {
202
202
  createAIMessage('Searching...', [
203
203
  {
204
204
  id: 'call_1',
205
- name: Constants.TOOL_SEARCH_REGEX,
205
+ name: Constants.TOOL_SEARCH,
206
206
  args: { pattern: 'xyz' },
207
207
  },
208
208
  ]),
209
209
  new ToolMessage({
210
210
  content: 'No tools found',
211
211
  tool_call_id: 'call_1',
212
- name: Constants.TOOL_SEARCH_REGEX,
212
+ name: Constants.TOOL_SEARCH,
213
213
  artifact: {
214
214
  tool_references: [],
215
215
  metadata: { total_searched: 10, pattern: 'xyz' },
@@ -228,14 +228,14 @@ describe('Tool Discovery Functions', () => {
228
228
  createAIMessage('Searching...', [
229
229
  {
230
230
  id: 'call_1',
231
- name: Constants.TOOL_SEARCH_REGEX,
231
+ name: Constants.TOOL_SEARCH,
232
232
  args: { pattern: 'test' },
233
233
  },
234
234
  ]),
235
235
  new ToolMessage({
236
236
  content: 'Error occurred',
237
237
  tool_call_id: 'call_1',
238
- name: Constants.TOOL_SEARCH_REGEX,
238
+ name: Constants.TOOL_SEARCH,
239
239
  // No artifact
240
240
  }),
241
241
  ];
@@ -251,7 +251,7 @@ describe('Tool Discovery Functions', () => {
251
251
  createAIMessage('Searching...', [
252
252
  {
253
253
  id: 'call_1',
254
- name: Constants.TOOL_SEARCH_REGEX,
254
+ name: Constants.TOOL_SEARCH,
255
255
  args: { pattern: 'test' },
256
256
  },
257
257
  ]),
@@ -271,7 +271,7 @@ describe('Tool Discovery Functions', () => {
271
271
  createAIMessage('First search', [
272
272
  {
273
273
  id: 'first_call',
274
- name: Constants.TOOL_SEARCH_REGEX,
274
+ name: Constants.TOOL_SEARCH,
275
275
  args: { pattern: 'first' },
276
276
  },
277
277
  ]),
@@ -280,7 +280,7 @@ describe('Tool Discovery Functions', () => {
280
280
  createAIMessage('Second search', [
281
281
  {
282
282
  id: 'second_call',
283
- name: Constants.TOOL_SEARCH_REGEX,
283
+ name: Constants.TOOL_SEARCH,
284
284
  args: { pattern: 'second' },
285
285
  },
286
286
  ]),
@@ -301,7 +301,7 @@ describe('Tool Discovery Functions', () => {
301
301
  createAIMessage('Searching...', [
302
302
  {
303
303
  id: 'call_1',
304
- name: Constants.TOOL_SEARCH_REGEX,
304
+ name: Constants.TOOL_SEARCH,
305
305
  args: { pattern: 'test' },
306
306
  },
307
307
  ]),
@@ -335,7 +335,7 @@ describe('Tool Discovery Functions', () => {
335
335
  new ToolMessage({
336
336
  content: 'Result',
337
337
  tool_call_id: 'orphan',
338
- name: Constants.TOOL_SEARCH_REGEX,
338
+ name: Constants.TOOL_SEARCH,
339
339
  }),
340
340
  ];
341
341
 
@@ -364,7 +364,7 @@ describe('Tool Discovery Functions', () => {
364
364
  createAIMessage('Working...', [
365
365
  {
366
366
  id: 'search_call',
367
- name: Constants.TOOL_SEARCH_REGEX,
367
+ name: Constants.TOOL_SEARCH,
368
368
  args: { pattern: 'test' },
369
369
  },
370
370
  { id: 'weather_call', name: 'get_weather', args: { city: 'NYC' } },
@@ -384,7 +384,7 @@ describe('Tool Discovery Functions', () => {
384
384
  createAIMessage('Searching...', [
385
385
  {
386
386
  id: 'old_call',
387
- name: Constants.TOOL_SEARCH_REGEX,
387
+ name: Constants.TOOL_SEARCH,
388
388
  args: { pattern: 'old' },
389
389
  },
390
390
  ]),
@@ -410,7 +410,7 @@ describe('Tool Discovery Functions', () => {
410
410
  createAIMessage('Searching...', [
411
411
  {
412
412
  id: 'call_1',
413
- name: Constants.TOOL_SEARCH_REGEX,
413
+ name: Constants.TOOL_SEARCH,
414
414
  args: { pattern: 'test' },
415
415
  },
416
416
  ]),
@@ -446,7 +446,7 @@ describe('Tool Discovery Functions', () => {
446
446
  createAIMessage('Searching...', [
447
447
  {
448
448
  id: 'call_1',
449
- name: Constants.TOOL_SEARCH_REGEX,
449
+ name: Constants.TOOL_SEARCH,
450
450
  args: { pattern: 'test' },
451
451
  },
452
452
  ]),
@@ -43,7 +43,7 @@ export function extractToolDiscoveries(messages: BaseMessage[]): string[] {
43
43
  for (let i = latestAIParentIndex + 1; i < messages.length; i++) {
44
44
  const msg = messages[i];
45
45
  if (!(msg instanceof ToolMessage)) continue;
46
- if (msg.name !== Constants.TOOL_SEARCH_REGEX) continue;
46
+ if (msg.name !== Constants.TOOL_SEARCH) continue;
47
47
  if (!toolCallIds.has(msg.tool_call_id)) continue;
48
48
 
49
49
  // This is a tool search result from the current turn
@@ -88,7 +88,7 @@ export function hasToolSearchInCurrentTurn(messages: BaseMessage[]): boolean {
88
88
  const msg = messages[i];
89
89
  if (
90
90
  msg instanceof ToolMessage &&
91
- msg.name === Constants.TOOL_SEARCH_REGEX &&
91
+ msg.name === Constants.TOOL_SEARCH &&
92
92
  toolCallIds.has(msg.tool_call_id)
93
93
  ) {
94
94
  return true;
@@ -23,7 +23,7 @@ import type { RunnableConfig } from '@langchain/core/runnables';
23
23
  import type * as t from '@/types';
24
24
  import { createCodeExecutionTool } from '@/tools/CodeExecutor';
25
25
  import { createProgrammaticToolCallingTool } from '@/tools/ProgrammaticToolCalling';
26
- import { createToolSearchRegexTool } from '@/tools/ToolSearchRegex';
26
+ import { createToolSearch } from '@/tools/ToolSearch';
27
27
  import { getLLMConfig } from '@/utils/llmConfig';
28
28
  import { getArgs } from '@/scripts/args';
29
29
  import { Run } from '@/run';
@@ -40,7 +40,7 @@ import {
40
40
 
41
41
  /**
42
42
  * Tool registry only needs business logic tools that require filtering.
43
- * Special tools (execute_code, run_tools_with_code, tool_search_regex)
43
+ * Special tools (execute_code, run_tools_with_code, tool_search)
44
44
  * are always bound directly to the LLM and don't need registry entries.
45
45
  */
46
46
  function createAgentToolRegistry(): t.LCToolRegistry {
@@ -73,7 +73,7 @@ async function main(): Promise<void> {
73
73
  // Create special tools (PTC, code execution, tool search)
74
74
  const codeExecTool = createCodeExecutionTool();
75
75
  const ptcTool = createProgrammaticToolCallingTool();
76
- const toolSearchTool = createToolSearchRegexTool();
76
+ const toolSearchTool = createToolSearch();
77
77
 
78
78
  // Build complete tool list and map
79
79
  const allTools = [...mockTools, codeExecTool, ptcTool, toolSearchTool];
@@ -199,7 +199,7 @@ Use the run_tools_with_code tool to do this efficiently - don't call each tool s
199
199
  console.log('='.repeat(70));
200
200
  console.log('\nKey observations:');
201
201
  console.log(
202
- '1. LLM only sees tools with allowed_callers including "direct" (get_weather, execute_code, run_tools_with_code, tool_search_regex)'
202
+ '1. LLM only sees tools with allowed_callers including "direct" (get_weather, execute_code, run_tools_with_code, tool_search)'
203
203
  );
204
204
  console.log(
205
205
  '2. When PTC is invoked, ToolNode automatically injects programmatic tools (get_team_members, get_expenses, get_weather)'
@@ -1,7 +1,7 @@
1
- // src/scripts/tool_search_regex.ts
1
+ // src/scripts/tool_search.ts
2
2
  /**
3
3
  * Test script for the Tool Search Regex tool.
4
- * Run with: npm run tool_search_regex
4
+ * Run with: npm run tool_search
5
5
  *
6
6
  * Demonstrates runtime registry injection - the tool registry is passed
7
7
  * at invocation time, not at initialization time.
@@ -9,7 +9,7 @@
9
9
  import { config } from 'dotenv';
10
10
  config();
11
11
 
12
- import { createToolSearchRegexTool } from '@/tools/ToolSearchRegex';
12
+ import { createToolSearch } from '@/tools/ToolSearch';
13
13
  import type { LCToolRegistry } from '@/types';
14
14
  import { createToolSearchToolRegistry } from '@/test/mockTools';
15
15
 
@@ -22,7 +22,7 @@ interface RunTestOptions {
22
22
  }
23
23
 
24
24
  async function runTest(
25
- searchTool: ReturnType<typeof createToolSearchRegexTool>,
25
+ searchTool: ReturnType<typeof createToolSearch>,
26
26
  testName: string,
27
27
  query: string,
28
28
  options: RunTestOptions
@@ -82,7 +82,7 @@ async function main(): Promise<void> {
82
82
  );
83
83
 
84
84
  console.log('\nCreating Tool Search Regex tool WITH registry for testing...');
85
- const searchTool = createToolSearchRegexTool({ apiKey, toolRegistry });
85
+ const searchTool = createToolSearch({ apiKey, toolRegistry });
86
86
  console.log('Tool created successfully!');
87
87
  console.log(
88
88
  'Note: In production, ToolNode injects toolRegistry via params when invoked through the graph.\n'
@@ -132,7 +132,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
132
132
  toolMap,
133
133
  toolDefs,
134
134
  };
135
- } else if (call.name === Constants.TOOL_SEARCH_REGEX) {
135
+ } else if (call.name === Constants.TOOL_SEARCH) {
136
136
  invokeParams = {
137
137
  ...invokeParams,
138
138
  toolRegistry: this.toolRegistry,
@@ -1,4 +1,4 @@
1
- // src/tools/ToolSearchRegex.ts
1
+ // src/tools/ToolSearch.ts
2
2
  import { z } from 'zod';
3
3
  import { config } from 'dotenv';
4
4
  import fetch, { RequestInit } from 'node-fetch';
@@ -20,28 +20,43 @@ const MAX_REGEX_COMPLEXITY = 5;
20
20
  /** Default search timeout in milliseconds */
21
21
  const SEARCH_TIMEOUT = 5000;
22
22
 
23
- const ToolSearchRegexSchema = z.object({
24
- query: z
25
- .string()
26
- .min(1)
27
- .max(MAX_PATTERN_LENGTH)
28
- .describe(
29
- 'Regex pattern to search tool names and descriptions. Special regex characters will be sanitized for safety.'
30
- ),
31
- fields: z
32
- .array(z.enum(['name', 'description', 'parameters']))
33
- .optional()
34
- .default(['name', 'description'])
35
- .describe('Which fields to search. Default: name and description'),
36
- max_results: z
37
- .number()
38
- .int()
39
- .min(1)
40
- .max(50)
41
- .optional()
42
- .default(10)
43
- .describe('Maximum number of matching tools to return'),
44
- });
23
+ /** Zod schema type for tool search parameters */
24
+ type ToolSearchSchema = z.ZodObject<{
25
+ query: z.ZodString;
26
+ fields: z.ZodDefault<
27
+ z.ZodOptional<z.ZodArray<z.ZodEnum<['name', 'description', 'parameters']>>>
28
+ >;
29
+ max_results: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
30
+ }>;
31
+
32
+ /**
33
+ * Creates the Zod schema with dynamic query description based on mode.
34
+ * @param mode - The search mode determining query interpretation
35
+ * @returns Zod schema for tool search parameters
36
+ */
37
+ function createToolSearchSchema(mode: t.ToolSearchMode): ToolSearchSchema {
38
+ const queryDescription =
39
+ mode === 'local'
40
+ ? 'Search term to find in tool names and descriptions. Case-insensitive substring matching.'
41
+ : 'Regex pattern to search tool names and descriptions. Special regex characters will be sanitized for safety.';
42
+
43
+ return z.object({
44
+ query: z.string().min(1).max(MAX_PATTERN_LENGTH).describe(queryDescription),
45
+ fields: z
46
+ .array(z.enum(['name', 'description', 'parameters']))
47
+ .optional()
48
+ .default(['name', 'description'])
49
+ .describe('Which fields to search. Default: name and description'),
50
+ max_results: z
51
+ .number()
52
+ .int()
53
+ .min(1)
54
+ .max(50)
55
+ .optional()
56
+ .default(10)
57
+ .describe('Maximum number of matching tools to return'),
58
+ });
59
+ }
45
60
 
46
61
  /**
47
62
  * Escapes special regex characters in a string to use as a literal pattern.
@@ -170,6 +185,80 @@ function simplifyParametersForSearch(
170
185
  return { type: parameters.type };
171
186
  }
172
187
 
188
+ /**
189
+ * Performs safe local substring search without regex.
190
+ * Uses case-insensitive String.includes() for complete safety against ReDoS.
191
+ * @param tools - Array of tool metadata to search
192
+ * @param query - The search term (treated as literal substring)
193
+ * @param fields - Which fields to search
194
+ * @param maxResults - Maximum results to return
195
+ * @returns Search response with matching tools
196
+ */
197
+ function performLocalSearch(
198
+ tools: t.ToolMetadata[],
199
+ query: string,
200
+ fields: string[],
201
+ maxResults: number
202
+ ): t.ToolSearchResponse {
203
+ const lowerQuery = query.toLowerCase();
204
+ const results: t.ToolSearchResult[] = [];
205
+
206
+ for (const tool of tools) {
207
+ let bestScore = 0;
208
+ let matchedField = '';
209
+ let snippet = '';
210
+
211
+ if (fields.includes('name')) {
212
+ const lowerName = tool.name.toLowerCase();
213
+ if (lowerName.includes(lowerQuery)) {
214
+ const isExactMatch = lowerName === lowerQuery;
215
+ const startsWithQuery = lowerName.startsWith(lowerQuery);
216
+ bestScore = isExactMatch ? 1.0 : startsWithQuery ? 0.95 : 0.85;
217
+ matchedField = 'name';
218
+ snippet = tool.name;
219
+ }
220
+ }
221
+
222
+ if (fields.includes('description') && tool.description) {
223
+ const lowerDesc = tool.description.toLowerCase();
224
+ if (lowerDesc.includes(lowerQuery) && bestScore === 0) {
225
+ bestScore = 0.7;
226
+ matchedField = 'description';
227
+ snippet = tool.description.substring(0, 100);
228
+ }
229
+ }
230
+
231
+ if (fields.includes('parameters') && tool.parameters?.properties) {
232
+ const paramNames = Object.keys(tool.parameters.properties)
233
+ .join(' ')
234
+ .toLowerCase();
235
+ if (paramNames.includes(lowerQuery) && bestScore === 0) {
236
+ bestScore = 0.55;
237
+ matchedField = 'parameters';
238
+ snippet = Object.keys(tool.parameters.properties).join(' ');
239
+ }
240
+ }
241
+
242
+ if (bestScore > 0) {
243
+ results.push({
244
+ tool_name: tool.name,
245
+ match_score: bestScore,
246
+ matched_field: matchedField,
247
+ snippet,
248
+ });
249
+ }
250
+ }
251
+
252
+ results.sort((a, b) => b.match_score - a.match_score);
253
+ const topResults = results.slice(0, maxResults);
254
+
255
+ return {
256
+ tool_references: topResults,
257
+ total_tools_searched: tools.length,
258
+ pattern_used: query,
259
+ };
260
+ }
261
+
173
262
  /**
174
263
  * Generates the JavaScript search script to be executed in the sandbox.
175
264
  * Uses plain JavaScript for maximum compatibility with the Code API.
@@ -307,11 +396,15 @@ function formatSearchResults(searchResponse: t.ToolSearchResponse): string {
307
396
  }
308
397
 
309
398
  /**
310
- * Creates a Tool Search Regex tool for discovering tools from a large registry.
399
+ * Creates a Tool Search tool for discovering tools from a large registry.
311
400
  *
312
401
  * This tool enables AI agents to dynamically discover tools from a large library
313
402
  * without loading all tool definitions into the LLM context window. The agent
314
- * can search for relevant tools on-demand using regex patterns.
403
+ * can search for relevant tools on-demand.
404
+ *
405
+ * **Modes:**
406
+ * - `code_interpreter` (default): Uses external sandbox for regex search. Safer for complex patterns.
407
+ * - `local`: Uses safe substring matching locally. No network call, faster, completely safe from ReDoS.
315
408
  *
316
409
  * The tool registry can be provided either:
317
410
  * 1. At initialization time via params.toolRegistry
@@ -321,36 +414,52 @@ function formatSearchResults(searchResponse: t.ToolSearchResponse): string {
321
414
  * @returns A LangChain DynamicStructuredTool for tool searching
322
415
  *
323
416
  * @example
324
- * // Option 1: Registry at initialization
325
- * const tool = createToolSearchRegexTool({ apiKey, toolRegistry });
326
- * await tool.invoke({ query: 'expense' });
417
+ * // Option 1: Code interpreter mode (regex via sandbox)
418
+ * const tool = createToolSearch({ apiKey, toolRegistry });
419
+ * await tool.invoke({ query: 'expense.*report' });
327
420
  *
328
421
  * @example
329
- * // Option 2: Registry at runtime
330
- * const tool = createToolSearchRegexTool({ apiKey });
331
- * await tool.invoke(
332
- * { query: 'expense' },
333
- * { configurable: { toolRegistry, onlyDeferred: true } }
334
- * );
422
+ * // Option 2: Local mode (safe substring search, no API key needed)
423
+ * const tool = createToolSearch({ mode: 'local', toolRegistry });
424
+ * await tool.invoke({ query: 'expense' });
335
425
  */
336
- function createToolSearchRegexTool(
337
- initParams: t.ToolSearchRegexParams = {}
338
- ): DynamicStructuredTool<typeof ToolSearchRegexSchema> {
339
- const apiKey: string =
340
- (initParams[EnvVar.CODE_API_KEY] as string | undefined) ??
341
- initParams.apiKey ??
342
- getEnvironmentVariable(EnvVar.CODE_API_KEY) ??
343
- '';
426
+ function createToolSearch(
427
+ initParams: t.ToolSearchParams = {}
428
+ ): DynamicStructuredTool<ReturnType<typeof createToolSearchSchema>> {
429
+ const mode: t.ToolSearchMode = initParams.mode ?? 'code_interpreter';
430
+ const defaultOnlyDeferred = initParams.onlyDeferred ?? true;
431
+ const schema = createToolSearchSchema(mode);
344
432
 
345
- if (!apiKey) {
346
- throw new Error('No API key provided for tool search regex tool.');
433
+ const apiKey: string =
434
+ mode === 'code_interpreter'
435
+ ? ((initParams[EnvVar.CODE_API_KEY] as string | undefined) ??
436
+ initParams.apiKey ??
437
+ getEnvironmentVariable(EnvVar.CODE_API_KEY) ??
438
+ '')
439
+ : '';
440
+
441
+ if (mode === 'code_interpreter' && !apiKey) {
442
+ throw new Error(
443
+ 'No API key provided for tool search in code_interpreter mode. Use mode: "local" to search without an API key.'
444
+ );
347
445
  }
348
446
 
349
447
  const baseEndpoint = initParams.baseUrl ?? getCodeBaseURL();
350
448
  const EXEC_ENDPOINT = `${baseEndpoint}/exec`;
351
- const defaultOnlyDeferred = initParams.onlyDeferred ?? true;
352
449
 
353
- const description = `
450
+ const description =
451
+ mode === 'local'
452
+ ? `
453
+ Searches through available tools to find ones matching your search term.
454
+
455
+ Usage:
456
+ - Provide a search term to find in tool names and descriptions.
457
+ - Uses case-insensitive substring matching (fast and safe).
458
+ - Use this when you need to discover tools for a specific task.
459
+ - Results include tool names, match quality scores, and snippets showing where the match occurred.
460
+ - Higher scores (0.95+) indicate name matches, medium scores (0.70+) indicate description matches.
461
+ `.trim()
462
+ : `
354
463
  Searches through available tools to find ones matching your query pattern.
355
464
 
356
465
  Usage:
@@ -360,7 +469,7 @@ Usage:
360
469
  - Higher scores (0.9+) indicate name matches, medium scores (0.7+) indicate description matches.
361
470
  `.trim();
362
471
 
363
- return tool<typeof ToolSearchRegexSchema>(
472
+ return tool<typeof schema>(
364
473
  async (params, config) => {
365
474
  const {
366
475
  query,
@@ -368,21 +477,11 @@ Usage:
368
477
  max_results = 10,
369
478
  } = params;
370
479
 
371
- // Extra params injected by ToolNode (follows web_search pattern)
372
480
  const {
373
481
  toolRegistry: paramToolRegistry,
374
482
  onlyDeferred: paramOnlyDeferred,
375
483
  } = config.toolCall ?? {};
376
484
 
377
- const { safe: sanitizedPattern, wasEscaped } = sanitizeRegex(query);
378
-
379
- let warningMessage = '';
380
- if (wasEscaped) {
381
- warningMessage =
382
- 'Note: The provided pattern was converted to a literal search for safety.\n\n';
383
- }
384
-
385
- // Priority: ToolNode injection (via config.toolCall) > initialization params
386
485
  const toolRegistry = paramToolRegistry ?? initParams.toolRegistry;
387
486
  const onlyDeferred =
388
487
  paramOnlyDeferred !== undefined
@@ -391,12 +490,12 @@ Usage:
391
490
 
392
491
  if (toolRegistry == null) {
393
492
  return [
394
- `${warningMessage}Error: No tool registry provided. Configure toolRegistry at agent level or initialization.`,
493
+ 'Error: No tool registry provided. Configure toolRegistry at agent level or initialization.',
395
494
  {
396
495
  tool_references: [],
397
496
  metadata: {
398
497
  total_searched: 0,
399
- pattern: sanitizedPattern,
498
+ pattern: query,
400
499
  error: 'No tool registry provided',
401
500
  },
402
501
  },
@@ -404,14 +503,10 @@ Usage:
404
503
  }
405
504
 
406
505
  const toolsArray: t.LCTool[] = Array.from(toolRegistry.values());
407
-
408
506
  const deferredTools: t.ToolMetadata[] = toolsArray
409
- .filter((lcTool) => {
410
- if (onlyDeferred === true) {
411
- return lcTool.defer_loading === true;
412
- }
413
- return true;
414
- })
507
+ .filter((lcTool) =>
508
+ onlyDeferred === true ? lcTool.defer_loading === true : true
509
+ )
415
510
  .map((lcTool) => ({
416
511
  name: lcTool.name,
417
512
  description: lcTool.description ?? '',
@@ -420,17 +515,42 @@ Usage:
420
515
 
421
516
  if (deferredTools.length === 0) {
422
517
  return [
423
- `${warningMessage}No tools available to search. The tool registry is empty or no deferred tools are registered.`,
518
+ 'No tools available to search. The tool registry is empty or no deferred tools are registered.',
424
519
  {
425
520
  tool_references: [],
521
+ metadata: { total_searched: 0, pattern: query },
522
+ },
523
+ ];
524
+ }
525
+
526
+ if (mode === 'local') {
527
+ const searchResponse = performLocalSearch(
528
+ deferredTools,
529
+ query,
530
+ fields,
531
+ max_results
532
+ );
533
+ const formattedOutput = formatSearchResults(searchResponse);
534
+
535
+ return [
536
+ formattedOutput,
537
+ {
538
+ tool_references: searchResponse.tool_references,
426
539
  metadata: {
427
- total_searched: 0,
428
- pattern: sanitizedPattern,
540
+ total_searched: searchResponse.total_tools_searched,
541
+ pattern: searchResponse.pattern_used,
429
542
  },
430
543
  },
431
544
  ];
432
545
  }
433
546
 
547
+ const { safe: sanitizedPattern, wasEscaped } = sanitizeRegex(query);
548
+ let warningMessage = '';
549
+ if (wasEscaped) {
550
+ warningMessage =
551
+ 'Note: The provided pattern was converted to a literal search for safety.\n\n';
552
+ }
553
+
434
554
  const searchScript = generateSearchScript(
435
555
  deferredTools,
436
556
  fields,
@@ -468,7 +588,7 @@ Usage:
468
588
 
469
589
  if (result.stderr && result.stderr.trim()) {
470
590
  // eslint-disable-next-line no-console
471
- console.warn('[ToolSearchRegex] stderr:', result.stderr);
591
+ console.warn('[ToolSearch] stderr:', result.stderr);
472
592
  }
473
593
 
474
594
  if (!result.stdout || !result.stdout.trim()) {
@@ -499,7 +619,7 @@ Usage:
499
619
  ];
500
620
  } catch (error) {
501
621
  // eslint-disable-next-line no-console
502
- console.error('[ToolSearchRegex] Error:', error);
622
+ console.error('[ToolSearch] Error:', error);
503
623
 
504
624
  const errorMessage =
505
625
  error instanceof Error ? error.message : String(error);
@@ -517,16 +637,17 @@ Usage:
517
637
  }
518
638
  },
519
639
  {
520
- name: Constants.TOOL_SEARCH_REGEX,
640
+ name: Constants.TOOL_SEARCH,
521
641
  description,
522
- schema: ToolSearchRegexSchema,
642
+ schema,
523
643
  responseFormat: Constants.CONTENT_AND_ARTIFACT,
524
644
  }
525
645
  );
526
646
  }
527
647
 
528
648
  export {
529
- createToolSearchRegexTool,
649
+ createToolSearch,
650
+ performLocalSearch,
530
651
  sanitizeRegex,
531
652
  escapeRegexSpecialChars,
532
653
  isDangerousPattern,