@librechat/agents 3.0.65 → 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 (49) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +12 -10
  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/MultiAgentGraph.cjs +22 -7
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  7. package/dist/cjs/main.cjs +8 -7
  8. package/dist/cjs/main.cjs.map +1 -1
  9. package/dist/cjs/messages/tools.cjs +2 -2
  10. package/dist/cjs/messages/tools.cjs.map +1 -1
  11. package/dist/cjs/tools/ToolNode.cjs +19 -4
  12. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  13. package/dist/cjs/tools/{ToolSearchRegex.cjs → ToolSearch.cjs} +154 -66
  14. package/dist/cjs/tools/ToolSearch.cjs.map +1 -0
  15. package/dist/esm/agents/AgentContext.mjs +12 -10
  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/MultiAgentGraph.mjs +22 -7
  20. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  21. package/dist/esm/main.mjs +1 -1
  22. package/dist/esm/messages/tools.mjs +2 -2
  23. package/dist/esm/messages/tools.mjs.map +1 -1
  24. package/dist/esm/tools/ToolNode.mjs +19 -4
  25. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  26. package/dist/esm/tools/{ToolSearchRegex.mjs → ToolSearch.mjs} +153 -66
  27. package/dist/esm/tools/ToolSearch.mjs.map +1 -0
  28. package/dist/types/agents/AgentContext.d.ts +7 -3
  29. package/dist/types/common/enum.d.ts +1 -1
  30. package/dist/types/graphs/MultiAgentGraph.d.ts +3 -3
  31. package/dist/types/index.d.ts +1 -1
  32. package/dist/types/tools/{ToolSearchRegex.d.ts → ToolSearch.d.ts} +33 -23
  33. package/dist/types/types/tools.d.ts +5 -1
  34. package/package.json +2 -2
  35. package/src/agents/AgentContext.ts +20 -12
  36. package/src/common/enum.ts +1 -1
  37. package/src/graphs/MultiAgentGraph.ts +29 -8
  38. package/src/index.ts +1 -1
  39. package/src/messages/__tests__/tools.test.ts +21 -21
  40. package/src/messages/tools.ts +2 -2
  41. package/src/scripts/programmatic_exec_agent.ts +4 -4
  42. package/src/scripts/{tool_search_regex.ts → tool_search.ts} +5 -5
  43. package/src/tools/ToolNode.ts +23 -5
  44. package/src/tools/{ToolSearchRegex.ts → ToolSearch.ts} +195 -74
  45. package/src/tools/__tests__/{ToolSearchRegex.integration.test.ts → ToolSearch.integration.test.ts} +6 -6
  46. package/src/tools/__tests__/{ToolSearchRegex.test.ts → ToolSearch.test.ts} +212 -3
  47. package/src/types/tools.ts +6 -1
  48. package/dist/cjs/tools/ToolSearchRegex.cjs.map +0 -1
  49. package/dist/esm/tools/ToolSearchRegex.mjs.map +0 -1
@@ -1,19 +1,18 @@
1
1
  import { z } from 'zod';
2
2
  import { DynamicStructuredTool } from '@langchain/core/tools';
3
3
  import type * as t from '@/types';
4
- declare const ToolSearchRegexSchema: z.ZodObject<{
4
+ /** Zod schema type for tool search parameters */
5
+ type ToolSearchSchema = z.ZodObject<{
5
6
  query: z.ZodString;
6
- fields: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodEnum<["name", "description", "parameters"]>, "many">>>;
7
+ fields: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodEnum<['name', 'description', 'parameters']>>>>;
7
8
  max_results: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
8
- }, "strip", z.ZodTypeAny, {
9
- query: string;
10
- fields: ("name" | "description" | "parameters")[];
11
- max_results: number;
12
- }, {
13
- query: string;
14
- fields?: ("name" | "description" | "parameters")[] | undefined;
15
- max_results?: number | undefined;
16
9
  }>;
10
+ /**
11
+ * Creates the Zod schema with dynamic query description based on mode.
12
+ * @param mode - The search mode determining query interpretation
13
+ * @returns Zod schema for tool search parameters
14
+ */
15
+ declare function createToolSearchSchema(mode: t.ToolSearchMode): ToolSearchSchema;
17
16
  /**
18
17
  * Escapes special regex characters in a string to use as a literal pattern.
19
18
  * @param pattern - The string to escape
@@ -50,11 +49,25 @@ declare function sanitizeRegex(pattern: string): {
50
49
  wasEscaped: boolean;
51
50
  };
52
51
  /**
53
- * Creates a Tool Search Regex tool for discovering tools from a large registry.
52
+ * Performs safe local substring search without regex.
53
+ * Uses case-insensitive String.includes() for complete safety against ReDoS.
54
+ * @param tools - Array of tool metadata to search
55
+ * @param query - The search term (treated as literal substring)
56
+ * @param fields - Which fields to search
57
+ * @param maxResults - Maximum results to return
58
+ * @returns Search response with matching tools
59
+ */
60
+ declare function performLocalSearch(tools: t.ToolMetadata[], query: string, fields: string[], maxResults: number): t.ToolSearchResponse;
61
+ /**
62
+ * Creates a Tool Search tool for discovering tools from a large registry.
54
63
  *
55
64
  * This tool enables AI agents to dynamically discover tools from a large library
56
65
  * without loading all tool definitions into the LLM context window. The agent
57
- * can search for relevant tools on-demand using regex patterns.
66
+ * can search for relevant tools on-demand.
67
+ *
68
+ * **Modes:**
69
+ * - `code_interpreter` (default): Uses external sandbox for regex search. Safer for complex patterns.
70
+ * - `local`: Uses safe substring matching locally. No network call, faster, completely safe from ReDoS.
58
71
  *
59
72
  * The tool registry can be provided either:
60
73
  * 1. At initialization time via params.toolRegistry
@@ -64,17 +77,14 @@ declare function sanitizeRegex(pattern: string): {
64
77
  * @returns A LangChain DynamicStructuredTool for tool searching
65
78
  *
66
79
  * @example
67
- * // Option 1: Registry at initialization
68
- * const tool = createToolSearchRegexTool({ apiKey, toolRegistry });
69
- * await tool.invoke({ query: 'expense' });
80
+ * // Option 1: Code interpreter mode (regex via sandbox)
81
+ * const tool = createToolSearch({ apiKey, toolRegistry });
82
+ * await tool.invoke({ query: 'expense.*report' });
70
83
  *
71
84
  * @example
72
- * // Option 2: Registry at runtime
73
- * const tool = createToolSearchRegexTool({ apiKey });
74
- * await tool.invoke(
75
- * { query: 'expense' },
76
- * { configurable: { toolRegistry, onlyDeferred: true } }
77
- * );
85
+ * // Option 2: Local mode (safe substring search, no API key needed)
86
+ * const tool = createToolSearch({ mode: 'local', toolRegistry });
87
+ * await tool.invoke({ query: 'expense' });
78
88
  */
79
- declare function createToolSearchRegexTool(initParams?: t.ToolSearchRegexParams): DynamicStructuredTool<typeof ToolSearchRegexSchema>;
80
- export { createToolSearchRegexTool, sanitizeRegex, escapeRegexSpecialChars, isDangerousPattern, countNestedGroups, hasNestedQuantifiers, };
89
+ declare function createToolSearch(initParams?: t.ToolSearchParams): DynamicStructuredTool<ReturnType<typeof createToolSearchSchema>>;
90
+ export { createToolSearch, performLocalSearch, sanitizeRegex, escapeRegexSpecialChars, isDangerousPattern, countNestedGroups, hasNestedQuantifiers, };
@@ -99,12 +99,16 @@ export type ProgrammaticCache = {
99
99
  toolMap: ToolMap;
100
100
  toolDefs: LCTool[];
101
101
  };
102
+ /** Search mode: code_interpreter uses external sandbox, local uses safe substring matching */
103
+ export type ToolSearchMode = 'code_interpreter' | 'local';
102
104
  /** Parameters for creating a Tool Search Regex tool */
103
- export type ToolSearchRegexParams = {
105
+ export type ToolSearchParams = {
104
106
  apiKey?: string;
105
107
  toolRegistry?: LCToolRegistry;
106
108
  onlyDeferred?: boolean;
107
109
  baseUrl?: string;
110
+ /** Search mode: 'code_interpreter' (default) uses sandbox for regex, 'local' uses safe substring matching */
111
+ mode?: ToolSearchMode;
108
112
  [key: string]: unknown;
109
113
  };
110
114
  /** Simplified tool metadata for search purposes */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.0.65",
3
+ "version": "3.0.67",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -57,7 +57,7 @@
57
57
  "memory": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/memory.ts --provider 'openAI' --name 'Jo' --location 'New York, NY'",
58
58
  "tool": "node --trace-warnings -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/tools.ts --provider 'openrouter' --name 'Jo' --location 'New York, NY'",
59
59
  "search": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/search.ts --provider 'bedrock' --name 'Jo' --location 'New York, NY'",
60
- "tool_search_regex": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/tool_search_regex.ts",
60
+ "tool_search": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/tool_search.ts",
61
61
  "programmatic_exec": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/programmatic_exec.ts",
62
62
  "code_exec_ptc": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/code_exec_ptc.ts --provider 'openAI' --name 'Jo' --location 'New York, NY'",
63
63
  "programmatic_exec_agent": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/programmatic_exec_agent.ts --provider 'openAI' --name 'Jo' --location 'New York, NY'",
@@ -151,10 +151,13 @@ export class AgentContext {
151
151
  useLegacyContent: boolean = false;
152
152
  /**
153
153
  * Handoff context when this agent receives control via handoff.
154
- * Contains source agent name for system message identity context.
154
+ * Contains source and parallel execution info for system message context.
155
155
  */
156
156
  handoffContext?: {
157
+ /** Source agent that transferred control */
157
158
  sourceAgentName: string;
159
+ /** Names of sibling agents executing in parallel (empty if sequential) */
160
+ parallelSiblings: string[];
158
161
  };
159
162
 
160
163
  constructor({
@@ -345,22 +348,26 @@ export class AgentContext {
345
348
  * This helps the agent understand its role in the multi-agent workflow.
346
349
  */
347
350
  private buildIdentityPreamble(): string {
348
- /** Only include preamble if we have handoff context (indicates multi-agent workflow) */
349
351
  if (!this.handoffContext) return '';
350
352
 
351
- /** Use name (falls back to agentId if not provided) */
352
353
  const displayName = this.name ?? this.agentId;
354
+ const { sourceAgentName, parallelSiblings } = this.handoffContext;
355
+ const isParallel = parallelSiblings.length > 0;
353
356
 
354
357
  const lines: string[] = [];
355
- lines.push('## Agent Context');
356
- lines.push(`You are the "${displayName}" agent.`);
358
+ lines.push('## Multi-Agent Workflow');
359
+ lines.push(
360
+ `You are "${displayName}", transferred from "${sourceAgentName}".`
361
+ );
357
362
 
358
- if (this.handoffContext.sourceAgentName) {
359
- lines.push(
360
- `Control was transferred to you from the "${this.handoffContext.sourceAgentName}" agent.`
361
- );
363
+ if (isParallel) {
364
+ lines.push(`Running in parallel with: ${parallelSiblings.join(', ')}.`);
362
365
  }
363
366
 
367
+ lines.push(
368
+ 'Execute only tasks relevant to your role. Routing is already handled if requested, unless you can route further.'
369
+ );
370
+
364
371
  return lines.join('\n');
365
372
  }
366
373
 
@@ -525,10 +532,11 @@ export class AgentContext {
525
532
  * Sets the handoff context for this agent.
526
533
  * Call this when the agent receives control via handoff from another agent.
527
534
  * Marks system runnable as stale to include handoff context in system message.
528
- * @param sourceAgentName - The name of the agent that handed off to this agent
535
+ * @param sourceAgentName - Name of the agent that transferred control
536
+ * @param parallelSiblings - Names of other agents executing in parallel with this one
529
537
  */
530
- setHandoffContext(sourceAgentName: string): void {
531
- this.handoffContext = { sourceAgentName };
538
+ setHandoffContext(sourceAgentName: string, parallelSiblings: string[]): void {
539
+ this.handoffContext = { sourceAgentName, parallelSiblings };
532
540
  this.systemRunnableStale = true;
533
541
  }
534
542
 
@@ -159,7 +159,7 @@ export enum Callback {
159
159
  export enum Constants {
160
160
  OFFICIAL_CODE_BASEURL = 'https://api.librechat.ai/v1',
161
161
  EXECUTE_CODE = 'execute_code',
162
- TOOL_SEARCH_REGEX = 'tool_search_regex',
162
+ TOOL_SEARCH = 'tool_search',
163
163
  PROGRAMMATIC_TOOL_CALLING = 'run_tools_with_code',
164
164
  WEB_SEARCH = 'web_search',
165
165
  CONTENT_AND_ARTIFACT = 'content_and_artifact',
@@ -497,15 +497,15 @@ export class MultiAgentGraph extends StandardGraph {
497
497
 
498
498
  /**
499
499
  * Detects if the current agent is receiving a handoff and processes the messages accordingly.
500
- * Returns filtered messages with the transfer tool call/message removed, plus any instructions
501
- * and source agent information extracted from the transfer.
500
+ * Returns filtered messages with the transfer tool call/message removed, plus any instructions,
501
+ * source agent, and parallel sibling information extracted from the transfer.
502
502
  *
503
503
  * Supports both single handoffs (last message is the transfer) and parallel handoffs
504
504
  * (multiple transfer ToolMessages, need to find the one targeting this agent).
505
505
  *
506
506
  * @param messages - Current state messages
507
507
  * @param agentId - The agent ID to check for handoff reception
508
- * @returns Object with filtered messages, extracted instructions, and source agent, or null if not a handoff
508
+ * @returns Object with filtered messages, extracted instructions, source agent, and parallel siblings
509
509
  */
510
510
  private processHandoffReception(
511
511
  messages: BaseMessage[],
@@ -514,6 +514,7 @@ export class MultiAgentGraph extends StandardGraph {
514
514
  filteredMessages: BaseMessage[];
515
515
  instructions: string | null;
516
516
  sourceAgentName: string | null;
517
+ parallelSiblings: string[];
517
518
  } | null {
518
519
  if (messages.length === 0) return null;
519
520
 
@@ -575,6 +576,17 @@ export class MultiAgentGraph extends StandardGraph {
575
576
  const sourceAgentName =
576
577
  typeof handoffSourceName === 'string' ? handoffSourceName : null;
577
578
 
579
+ /** Extract parallel siblings (set by ToolNode for parallel handoffs) */
580
+ const rawSiblings = toolMessage.additional_kwargs.handoff_parallel_siblings;
581
+ const siblingIds: string[] = Array.isArray(rawSiblings)
582
+ ? rawSiblings.filter((s): s is string => typeof s === 'string')
583
+ : [];
584
+ /** Convert IDs to display names */
585
+ const parallelSiblings = siblingIds.map((id) => {
586
+ const ctx = this.agentContexts.get(id);
587
+ return ctx?.name ?? id;
588
+ });
589
+
578
590
  /** Get the tool_call_id to find and filter the AI message's tool call */
579
591
  const toolCallId = toolMessage.tool_call_id;
580
592
 
@@ -648,7 +660,12 @@ export class MultiAgentGraph extends StandardGraph {
648
660
  filteredMessages.push(msg);
649
661
  }
650
662
 
651
- return { filteredMessages, instructions, sourceAgentName };
663
+ return {
664
+ filteredMessages,
665
+ instructions,
666
+ sourceAgentName,
667
+ parallelSiblings,
668
+ };
652
669
  }
653
670
 
654
671
  /**
@@ -736,12 +753,16 @@ export class MultiAgentGraph extends StandardGraph {
736
753
  );
737
754
 
738
755
  if (handoffContext !== null) {
739
- const { filteredMessages, instructions, sourceAgentName } =
740
- handoffContext;
756
+ const {
757
+ filteredMessages,
758
+ instructions,
759
+ sourceAgentName,
760
+ parallelSiblings,
761
+ } = handoffContext;
741
762
 
742
763
  /**
743
764
  * Set handoff context on the receiving agent.
744
- * This updates the system message to include agent identity info.
765
+ * Uses pre-computed graph position for depth and parallel info.
745
766
  */
746
767
  const agentContext = this.agentContexts.get(agentId);
747
768
  if (
@@ -749,7 +770,7 @@ export class MultiAgentGraph extends StandardGraph {
749
770
  sourceAgentName != null &&
750
771
  sourceAgentName !== ''
751
772
  ) {
752
- agentContext.setHandoffContext(sourceAgentName);
773
+ agentContext.setHandoffContext(sourceAgentName, parallelSiblings);
753
774
  }
754
775
 
755
776
  /** Build messages for the receiving agent */
package/src/index.ts CHANGED
@@ -12,7 +12,7 @@ export * from './graphs';
12
12
  export * from './tools/Calculator';
13
13
  export * from './tools/CodeExecutor';
14
14
  export * from './tools/ProgrammaticToolCalling';
15
- export * from './tools/ToolSearchRegex';
15
+ export * from './tools/ToolSearch';
16
16
  export * from './tools/handlers';
17
17
  export * from './tools/search';
18
18
 
@@ -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,
@@ -346,11 +346,29 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
346
346
  * This enables LLM-initiated parallel execution when calling multiple
347
347
  * transfer tools simultaneously.
348
348
  */
349
- const sends = handoffCommands.map((cmd) => {
350
- /** Extract destination - handle both string and array formats */
349
+
350
+ /** Collect all destinations for sibling tracking */
351
+ const allDestinations = handoffCommands.map((cmd) => {
351
352
  const goto = cmd.goto;
352
- const destination =
353
- typeof goto === 'string' ? goto : (goto as string[])[0];
353
+ return typeof goto === 'string' ? goto : (goto as string[])[0];
354
+ });
355
+
356
+ const sends = handoffCommands.map((cmd, idx) => {
357
+ const destination = allDestinations[idx];
358
+ /** Get siblings (other destinations, not this one) */
359
+ const siblings = allDestinations.filter((d) => d !== destination);
360
+
361
+ /** Add siblings to ToolMessage additional_kwargs */
362
+ const update = cmd.update as { messages?: BaseMessage[] } | undefined;
363
+ if (update && update.messages) {
364
+ for (const msg of update.messages) {
365
+ if (msg.getType() === 'tool') {
366
+ (msg as ToolMessage).additional_kwargs.handoff_parallel_siblings =
367
+ siblings;
368
+ }
369
+ }
370
+ }
371
+
354
372
  return new Send(destination, cmd.update);
355
373
  });
356
374