From 2c6781d1341d3af1066fbafb3877ed6f7cf56590 Mon Sep 17 00:00:00 2001 From: Christian Gunderman Date: Fri, 23 Jan 2026 02:18:31 +0000 Subject: [PATCH] Refactor subagent delegation to be one tool per agent (#17346) --- evals/generalist_agent.eval.ts | 9 +- evals/subagents.eval.ts | 13 +- packages/core/src/agents/agentLoader.test.ts | 13 - packages/core/src/agents/agentLoader.ts | 14 +- .../src/agents/delegate-to-agent-tool.test.ts | 346 ------------------ .../core/src/agents/delegate-to-agent-tool.ts | 240 ------------ .../core/src/agents/generalist-agent.test.ts | 6 +- packages/core/src/agents/generalist-agent.ts | 7 +- .../core/src/agents/local-executor.test.ts | 30 ++ packages/core/src/agents/local-executor.ts | 12 + packages/core/src/agents/registry.test.ts | 22 +- packages/core/src/agents/registry.ts | 41 +-- packages/core/src/agents/subagent-tool.ts | 132 +++++++ packages/core/src/config/config.test.ts | 31 +- packages/core/src/config/config.ts | 42 ++- .../core/__snapshots__/prompts.test.ts.snap | 2 +- packages/core/src/core/prompts.ts | 5 +- packages/core/src/tools/tool-names.ts | 2 - 18 files changed, 247 insertions(+), 720 deletions(-) delete mode 100644 packages/core/src/agents/delegate-to-agent-tool.test.ts delete mode 100644 packages/core/src/agents/delegate-to-agent-tool.ts create mode 100644 packages/core/src/agents/subagent-tool.ts diff --git a/evals/generalist_agent.eval.ts b/evals/generalist_agent.eval.ts index 8e3e11e9ba..f93005d509 100644 --- a/evals/generalist_agent.eval.ts +++ b/evals/generalist_agent.eval.ts @@ -25,14 +25,7 @@ describe('generalist_agent', () => { 'Please use the generalist agent to create a file called "generalist_test_file.txt" containing exactly the following text: success', assert: async (rig) => { // 1) Verify the generalist agent was invoked via delegate_to_agent - const foundToolCall = await rig.waitForToolCall( - 'delegate_to_agent', - undefined, - (args) => { - const parsed = JSON.parse(args); - return parsed.agent_name === 'generalist'; - }, - ); + const foundToolCall = await rig.waitForToolCall('generalist'); expect( foundToolCall, 'Expected to find a delegate_to_agent tool call for generalist agent', diff --git a/evals/subagents.eval.ts b/evals/subagents.eval.ts index 4d97d38952..7e9b3cd808 100644 --- a/evals/subagents.eval.ts +++ b/evals/subagents.eval.ts @@ -47,18 +47,7 @@ describe('subagent eval test cases', () => { 'README.md': 'TODO: update the README.', }, assert: async (rig, _result) => { - await rig.expectToolCallSuccess( - ['delegate_to_agent'], - undefined, - (args) => { - try { - const parsed = JSON.parse(args); - return parsed.agent_name === 'docs-agent'; - } catch { - return false; - } - }, - ); + await rig.expectToolCallSuccess(['docs-agent']); }, }); }); diff --git a/packages/core/src/agents/agentLoader.test.ts b/packages/core/src/agents/agentLoader.test.ts index bf7a77b44b..7391161542 100644 --- a/packages/core/src/agents/agentLoader.test.ts +++ b/packages/core/src/agents/agentLoader.test.ts @@ -111,19 +111,6 @@ Body`); ); }); - it('should throw AgentLoadError if tools list includes forbidden tool', async () => { - const filePath = await writeAgentMarkdown(`--- -name: test-agent -description: Test -tools: - - delegate_to_agent ---- -Body`); - await expect(parseAgentMarkdown(filePath)).rejects.toThrow( - /tools list cannot include 'delegate_to_agent'/, - ); - }); - it('should parse a valid remote agent markdown file', async () => { const filePath = await writeAgentMarkdown(`--- kind: remote diff --git a/packages/core/src/agents/agentLoader.ts b/packages/core/src/agents/agentLoader.ts index 79295d4855..385d1e9b59 100644 --- a/packages/core/src/agents/agentLoader.ts +++ b/packages/core/src/agents/agentLoader.ts @@ -10,10 +10,7 @@ import { type Dirent } from 'node:fs'; import * as path from 'node:path'; import { z } from 'zod'; import type { AgentDefinition } from './types.js'; -import { - isValidToolName, - DELEGATE_TO_AGENT_TOOL_NAME, -} from '../tools/tool-names.js'; +import { isValidToolName } from '../tools/tool-names.js'; import { FRONTMATTER_REGEX } from '../skills/skillLoader.js'; /** @@ -217,15 +214,6 @@ export async function parseAgentMarkdown( // Local agent validation // Validate tools - if ( - frontmatter.tools && - frontmatter.tools.includes(DELEGATE_TO_AGENT_TOOL_NAME) - ) { - throw new AgentLoadError( - filePath, - `Validation failed: tools list cannot include '${DELEGATE_TO_AGENT_TOOL_NAME}'. Sub-agents cannot delegate to other agents.`, - ); - } // Construct the local agent definition const agentDef: FrontmatterLocalAgentDefinition = { diff --git a/packages/core/src/agents/delegate-to-agent-tool.test.ts b/packages/core/src/agents/delegate-to-agent-tool.test.ts deleted file mode 100644 index 89cd1babdb..0000000000 --- a/packages/core/src/agents/delegate-to-agent-tool.test.ts +++ /dev/null @@ -1,346 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { - DelegateToAgentTool, - type DelegateParams, -} from './delegate-to-agent-tool.js'; -import { AgentRegistry } from './registry.js'; -import type { Config } from '../config/config.js'; -import type { AgentDefinition } from './types.js'; -import { LocalSubagentInvocation } from './local-invocation.js'; -import type { MessageBus } from '../confirmation-bus/message-bus.js'; -import { MessageBusType } from '../confirmation-bus/types.js'; -import { DELEGATE_TO_AGENT_TOOL_NAME } from '../tools/tool-names.js'; -import { RemoteAgentInvocation } from './remote-invocation.js'; -import { createMockMessageBus } from '../test-utils/mock-message-bus.js'; - -vi.mock('./local-invocation.js', () => ({ - LocalSubagentInvocation: vi.fn().mockImplementation(() => ({ - execute: vi - .fn() - .mockResolvedValue({ content: [{ type: 'text', text: 'Success' }] }), - shouldConfirmExecute: vi.fn().mockResolvedValue(false), - })), -})); - -vi.mock('./remote-invocation.js', () => ({ - RemoteAgentInvocation: vi.fn().mockImplementation(() => ({ - execute: vi.fn().mockResolvedValue({ - content: [{ type: 'text', text: 'Remote Success' }], - }), - shouldConfirmExecute: vi.fn().mockResolvedValue({ - type: 'info', - title: 'Remote Confirmation', - prompt: 'Confirm remote call', - onConfirm: vi.fn(), - }), - })), -})); - -describe('DelegateToAgentTool', () => { - let registry: AgentRegistry; - let config: Config; - let tool: DelegateToAgentTool; - let messageBus: MessageBus; - - const mockAgentDef: AgentDefinition = { - kind: 'local', - name: 'test_agent', - description: 'A test agent', - promptConfig: {}, - modelConfig: { - model: 'test-model', - generateContentConfig: { - temperature: 0, - topP: 0, - }, - }, - inputConfig: { - inputSchema: { - type: 'object', - properties: { - arg1: { type: 'string', description: 'Argument 1' }, - arg2: { type: 'number', description: 'Argument 2' }, - }, - required: ['arg1'], - }, - }, - runConfig: { maxTurns: 1, maxTimeMinutes: 1 }, - toolConfig: { tools: [] }, - }; - - const mockRemoteAgentDef: AgentDefinition = { - kind: 'remote', - name: 'remote_agent', - description: 'A remote agent', - agentCardUrl: 'https://example.com/agent.json', - inputConfig: { - inputSchema: { - type: 'object', - properties: { - query: { type: 'string', description: 'Query' }, - }, - required: ['query'], - }, - }, - }; - - beforeEach(() => { - config = { - getDebugMode: () => false, - getActiveModel: () => 'test-model', - modelConfigService: { - registerRuntimeModelConfig: vi.fn(), - }, - } as unknown as Config; - - registry = new AgentRegistry(config); - // Manually register the mock agent (bypassing protected method for testing) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (registry as any).agents.set(mockAgentDef.name, mockAgentDef); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (registry as any).agents.set(mockRemoteAgentDef.name, mockRemoteAgentDef); - - messageBus = createMockMessageBus(); - - tool = new DelegateToAgentTool(registry, config, messageBus); - }); - - it('should use dynamic description from registry', () => { - // registry has mockAgentDef registered in beforeEach - expect(tool.description).toContain( - 'Delegates a task to a specialized sub-agent', - ); - expect(tool.description).toContain( - `- **${mockAgentDef.name}**: ${mockAgentDef.description}`, - ); - }); - - it('should throw helpful error when agent_name does not exist', async () => { - // We allow validation to pass now, checking happens in execute. - const invocation = tool.build({ - agent_name: 'non_existent_agent', - } as DelegateParams); - - await expect(() => - invocation.execute(new AbortController().signal), - ).rejects.toThrow( - "Agent 'non_existent_agent' not found. Available agents are: 'test_agent' (A test agent), 'remote_agent' (A remote agent). Please choose a valid agent_name.", - ); - }); - - it('should validate correct arguments', async () => { - const invocation = tool.build({ - agent_name: 'test_agent', - arg1: 'valid', - }); - - const result = await invocation.execute(new AbortController().signal); - expect(result).toEqual({ content: [{ type: 'text', text: 'Success' }] }); - expect(LocalSubagentInvocation).toHaveBeenCalledWith( - mockAgentDef, - config, - { arg1: 'valid' }, - messageBus, - mockAgentDef.name, - mockAgentDef.name, - ); - }); - - it('should throw helpful error for missing required argument', async () => { - const invocation = tool.build({ - agent_name: 'test_agent', - arg2: 123, - } as DelegateParams); - - await expect(() => - invocation.execute(new AbortController().signal), - ).rejects.toThrow( - `Invalid arguments for agent 'test_agent': params must have required property 'arg1'. Input schema: ${JSON.stringify(mockAgentDef.inputConfig.inputSchema)}.`, - ); - }); - - it('should throw helpful error for invalid argument type', async () => { - const invocation = tool.build({ - agent_name: 'test_agent', - arg1: 123, - } as DelegateParams); - - await expect(() => - invocation.execute(new AbortController().signal), - ).rejects.toThrow( - `Invalid arguments for agent 'test_agent': params/arg1 must be string. Input schema: ${JSON.stringify(mockAgentDef.inputConfig.inputSchema)}.`, - ); - }); - - it('should allow optional arguments to be omitted', async () => { - const invocation = tool.build({ - agent_name: 'test_agent', - arg1: 'valid', - // arg2 is optional - }); - - await expect( - invocation.execute(new AbortController().signal), - ).resolves.toBeDefined(); - }); - - it('should throw error if an agent has an input named "agent_name"', () => { - const invalidAgentDef: AgentDefinition = { - ...mockAgentDef, - name: 'invalid_agent', - inputConfig: { - inputSchema: { - type: 'object', - properties: { - agent_name: { - type: 'string', - description: 'Conflict', - }, - }, - required: ['agent_name'], - }, - }, - }; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (registry as any).agents.set(invalidAgentDef.name, invalidAgentDef); - - expect(() => new DelegateToAgentTool(registry, config, messageBus)).toThrow( - "Agent 'invalid_agent' cannot have an input parameter named 'agent_name' as it is a reserved parameter for delegation.", - ); - }); - - it('should allow a remote agent missing a "query" input (will default at runtime)', () => { - const invalidRemoteAgentDef: AgentDefinition = { - kind: 'remote', - name: 'invalid_remote', - description: 'Conflict', - agentCardUrl: 'https://example.com/agent.json', - inputConfig: { - inputSchema: { - type: 'object', - properties: { - not_query: { - type: 'string', - description: 'Not a query', - }, - }, - required: ['not_query'], - }, - }, - }; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (registry as any).agents.set( - invalidRemoteAgentDef.name, - invalidRemoteAgentDef, - ); - - expect( - () => new DelegateToAgentTool(registry, config, messageBus), - ).not.toThrow(); - }); - - it('should execute local agents silently without requesting confirmation', async () => { - const invocation = tool.build({ - agent_name: 'test_agent', - arg1: 'valid', - }); - - // Trigger confirmation check - const result = await invocation.shouldConfirmExecute( - new AbortController().signal, - ); - - expect(result).toBe(false); - - // Verify it did NOT call messageBus.publish with 'delegate_to_agent' - const delegateToAgentPublish = vi - .mocked(messageBus.publish) - .mock.calls.find( - (call) => - call[0].type === MessageBusType.TOOL_CONFIRMATION_REQUEST && - call[0].toolCall.name === DELEGATE_TO_AGENT_TOOL_NAME, - ); - expect(delegateToAgentPublish).toBeUndefined(); - }); - - it('should delegate to remote agent correctly', async () => { - const invocation = tool.build({ - agent_name: 'remote_agent', - query: 'hello remote', - }); - - const result = await invocation.execute(new AbortController().signal); - expect(result).toEqual({ - content: [{ type: 'text', text: 'Remote Success' }], - }); - expect(RemoteAgentInvocation).toHaveBeenCalledWith( - mockRemoteAgentDef, - { query: 'hello remote' }, - messageBus, - 'remote_agent', - 'remote_agent', - ); - }); - - describe('Confirmation', () => { - it('should return false for local agents (silent execution)', async () => { - const invocation = tool.build({ - agent_name: 'test_agent', - arg1: 'valid', - }); - - // Local agents should now return false directly, bypassing policy check - const result = await invocation.shouldConfirmExecute( - new AbortController().signal, - ); - - expect(result).toBe(false); - - const delegateToAgentPublish = vi - .mocked(messageBus.publish) - .mock.calls.find( - (call) => - call[0].type === MessageBusType.TOOL_CONFIRMATION_REQUEST && - call[0].toolCall.name === DELEGATE_TO_AGENT_TOOL_NAME, - ); - expect(delegateToAgentPublish).toBeUndefined(); - }); - - it('should forward to remote agent confirmation logic', async () => { - const invocation = tool.build({ - agent_name: 'remote_agent', - query: 'hello remote', - }); - - const result = await invocation.shouldConfirmExecute( - new AbortController().signal, - ); - - // Verify it returns the mock confirmation from RemoteAgentInvocation - expect(result).toMatchObject({ - type: 'info', - title: 'Remote Confirmation', - }); - - // Verify it did NOT call messageBus.publish with 'delegate_to_agent' - // directly from DelegateInvocation, but instead went into RemoteAgentInvocation. - // RemoteAgentInvocation (the mock) doesn't call publish in its mock implementation. - const delegateToAgentPublish = vi - .mocked(messageBus.publish) - .mock.calls.find( - (call) => - call[0].type === MessageBusType.TOOL_CONFIRMATION_REQUEST && - call[0].toolCall.name === DELEGATE_TO_AGENT_TOOL_NAME, - ); - expect(delegateToAgentPublish).toBeUndefined(); - }); - }); -}); diff --git a/packages/core/src/agents/delegate-to-agent-tool.ts b/packages/core/src/agents/delegate-to-agent-tool.ts deleted file mode 100644 index 064428940d..0000000000 --- a/packages/core/src/agents/delegate-to-agent-tool.ts +++ /dev/null @@ -1,240 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - BaseDeclarativeTool, - Kind, - type ToolInvocation, - type ToolResult, - BaseToolInvocation, - type ToolCallConfirmationDetails, -} from '../tools/tools.js'; -import type { AnsiOutput } from '../utils/terminalSerializer.js'; -import { DELEGATE_TO_AGENT_TOOL_NAME } from '../tools/tool-names.js'; -import type { AgentRegistry } from './registry.js'; -import type { Config } from '../config/config.js'; -import type { MessageBus } from '../confirmation-bus/message-bus.js'; -import type { AgentDefinition, AgentInputs } from './types.js'; -import { SubagentToolWrapper } from './subagent-tool-wrapper.js'; -import { SchemaValidator } from '../utils/schemaValidator.js'; -import { type AnySchema } from 'ajv'; -import { debugLogger } from '../utils/debugLogger.js'; - -export type DelegateParams = { agent_name: string } & Record; - -export class DelegateToAgentTool extends BaseDeclarativeTool< - DelegateParams, - ToolResult -> { - constructor( - private readonly registry: AgentRegistry, - private readonly config: Config, - messageBus: MessageBus, - ) { - const definitions = registry.getAllDefinitions(); - - let toolSchema: AnySchema; - - if (definitions.length === 0) { - // Fallback if no agents are registered (mostly for testing/safety) - toolSchema = { - type: 'object', - properties: { - agent_name: { - type: 'string', - description: 'No agents are currently available.', - }, - }, - required: ['agent_name'], - }; - } else { - const agentSchemas = definitions.map((def) => { - const schemaError = SchemaValidator.validateSchema( - def.inputConfig.inputSchema, - ); - if (schemaError) { - throw new Error(`Invalid schema for ${def.name}: ${schemaError}`); - } - - const inputSchema = def.inputConfig.inputSchema; - if (typeof inputSchema !== 'object' || inputSchema === null) { - throw new Error(`Agent '${def.name}' must provide an object schema.`); - } - - const schemaObj = inputSchema as Record; - const properties = schemaObj['properties'] as - | Record - | undefined; - if (properties && 'agent_name' in properties) { - throw new Error( - `Agent '${def.name}' cannot have an input parameter named 'agent_name' as it is a reserved parameter for delegation.`, - ); - } - - if (def.kind === 'remote') { - if (!properties || !properties['query']) { - debugLogger.log( - 'INFO', - `Remote agent '${def.name}' does not define a 'query' property in its inputSchema. It will default to 'Get Started!' during invocation.`, - ); - } - } - - return { - type: 'object', - properties: { - agent_name: { - const: def.name, - description: def.description, - }, - ...(properties || {}), - }, - required: [ - 'agent_name', - ...((schemaObj['required'] as string[]) || []), - ], - } as AnySchema; - }); - - // Create the anyOf schema - if (agentSchemas.length === 1) { - toolSchema = agentSchemas[0]; - } else { - toolSchema = { - anyOf: agentSchemas, - }; - } - } - - super( - DELEGATE_TO_AGENT_TOOL_NAME, - 'Delegate to Agent', - registry.getToolDescription(), - Kind.Think, - toolSchema, - messageBus, - /* isOutputMarkdown */ true, - /* canUpdateOutput */ true, - ); - } - - override validateToolParams(_params: DelegateParams): string | null { - // We override the default schema validation because the generic JSON schema validation - // produces poor error messages for discriminated unions (anyOf). - // Instead, we perform detailed, agent-specific validation in the `execute` method - // to provide rich error messages that help the LLM self-heal. - return null; - } - - protected createInvocation( - params: DelegateParams, - messageBus: MessageBus, - _toolName?: string, - _toolDisplayName?: string, - ): ToolInvocation { - return new DelegateInvocation( - params, - this.registry, - this.config, - messageBus, - _toolName, - _toolDisplayName, - ); - } -} - -class DelegateInvocation extends BaseToolInvocation< - DelegateParams, - ToolResult -> { - constructor( - params: DelegateParams, - private readonly registry: AgentRegistry, - private readonly config: Config, - messageBus: MessageBus, - _toolName?: string, - _toolDisplayName?: string, - ) { - super( - params, - messageBus, - _toolName ?? DELEGATE_TO_AGENT_TOOL_NAME, - _toolDisplayName, - ); - } - - getDescription(): string { - return `Delegating to agent '${this.params.agent_name}'`; - } - - override async shouldConfirmExecute( - abortSignal: AbortSignal, - ): Promise { - const definition = this.registry.getDefinition(this.params.agent_name); - if (!definition || definition.kind !== 'remote') { - // Local agents should execute without confirmation. Inner tool calls will bubble up their own confirmations to the user. - return false; - } - - const { agent_name: _agent_name, ...agentArgs } = this.params; - const invocation = this.buildSubInvocation( - definition, - agentArgs as AgentInputs, - ); - return invocation.shouldConfirmExecute(abortSignal); - } - - async execute( - signal: AbortSignal, - updateOutput?: (output: string | AnsiOutput) => void, - ): Promise { - const definition = this.registry.getDefinition(this.params.agent_name); - if (!definition) { - const availableAgents = this.registry - .getAllDefinitions() - .map((def) => `'${def.name}' (${def.description})`) - .join(', '); - - throw new Error( - `Agent '${this.params.agent_name}' not found. Available agents are: ${availableAgents}. Please choose a valid agent_name.`, - ); - } - - const { agent_name: _agent_name, ...agentArgs } = this.params; - - // Validate specific agent arguments here using SchemaValidator to generate helpful error messages. - const validationError = SchemaValidator.validate( - definition.inputConfig.inputSchema, - agentArgs, - ); - - if (validationError) { - throw new Error( - `Invalid arguments for agent '${definition.name}': ${validationError}. Input schema: ${JSON.stringify(definition.inputConfig.inputSchema)}.`, - ); - } - - const invocation = this.buildSubInvocation( - definition, - agentArgs as AgentInputs, - ); - - return invocation.execute(signal, updateOutput); - } - - private buildSubInvocation( - definition: AgentDefinition, - agentArgs: AgentInputs, - ): ToolInvocation { - const wrapper = new SubagentToolWrapper( - definition, - this.config, - this.messageBus, - ); - - return wrapper.build(agentArgs); - } -} diff --git a/packages/core/src/agents/generalist-agent.test.ts b/packages/core/src/agents/generalist-agent.test.ts index efd651c121..27046872da 100644 --- a/packages/core/src/agents/generalist-agent.test.ts +++ b/packages/core/src/agents/generalist-agent.test.ts @@ -7,7 +7,6 @@ import { describe, it, expect, vi } from 'vitest'; import { GeneralistAgent } from './generalist-agent.js'; import { makeFakeConfig } from '../test-utils/config.js'; -import { DELEGATE_TO_AGENT_TOOL_NAME } from '../tools/tool-names.js'; import type { ToolRegistry } from '../tools/tool-registry.js'; import type { AgentRegistry } from './registry.js'; @@ -15,10 +14,11 @@ describe('GeneralistAgent', () => { it('should create a valid generalist agent definition', () => { const config = makeFakeConfig(); vi.spyOn(config, 'getToolRegistry').mockReturnValue({ - getAllToolNames: () => ['tool1', 'tool2', DELEGATE_TO_AGENT_TOOL_NAME], + getAllToolNames: () => ['tool1', 'tool2', 'agent-tool'], } as unknown as ToolRegistry); vi.spyOn(config, 'getAgentRegistry').mockReturnValue({ getDirectoryContext: () => 'mock directory context', + getAllAgentNames: () => ['agent-tool'], } as unknown as AgentRegistry); const agent = GeneralistAgent(config); @@ -27,7 +27,7 @@ describe('GeneralistAgent', () => { expect(agent.kind).toBe('local'); expect(agent.modelConfig.model).toBe('inherit'); expect(agent.toolConfig?.tools).toBeDefined(); - expect(agent.toolConfig?.tools).not.toContain(DELEGATE_TO_AGENT_TOOL_NAME); + expect(agent.toolConfig?.tools).toContain('agent-tool'); expect(agent.toolConfig?.tools).toContain('tool1'); expect(agent.promptConfig.systemPrompt).toContain('CLI agent'); // Ensure it's non-interactive diff --git a/packages/core/src/agents/generalist-agent.ts b/packages/core/src/agents/generalist-agent.ts index 492fee52de..4f9040a7b0 100644 --- a/packages/core/src/agents/generalist-agent.ts +++ b/packages/core/src/agents/generalist-agent.ts @@ -8,7 +8,6 @@ import { z } from 'zod'; import type { Config } from '../config/config.js'; import { getCoreSystemPrompt } from '../core/prompts.js'; import type { LocalAgentDefinition } from './types.js'; -import { DELEGATE_TO_AGENT_TOOL_NAME } from '../tools/tool-names.js'; const GeneralistAgentSchema = z.object({ response: z.string().describe('The final response from the agent.'), @@ -48,11 +47,7 @@ export const GeneralistAgent = ( model: 'inherit', }, get toolConfig() { - // TODO(15179): Support recursive agent invocation. - const tools = config - .getToolRegistry() - .getAllToolNames() - .filter((name) => name !== DELEGATE_TO_AGENT_TOOL_NAME); + const tools = config.getToolRegistry().getAllToolNames(); return { tools, }; diff --git a/packages/core/src/agents/local-executor.test.ts b/packages/core/src/agents/local-executor.test.ts index 4578bfab8d..2cd04a4a6e 100644 --- a/packages/core/src/agents/local-executor.test.ts +++ b/packages/core/src/agents/local-executor.test.ts @@ -61,6 +61,7 @@ import type { ModelConfigKey, ResolvedModelConfig, } from '../services/modelConfigService.js'; +import type { AgentRegistry } from './registry.js'; import { getModelConfigAlias } from './registry.js'; import type { ModelRouterService } from '../routing/modelRouterService.js'; @@ -298,6 +299,9 @@ describe('LocalAgentExecutor', () => { parentToolRegistry.registerTool(MOCK_TOOL_NOT_ALLOWED); vi.spyOn(mockConfig, 'getToolRegistry').mockReturnValue(parentToolRegistry); + vi.spyOn(mockConfig, 'getAgentRegistry').mockReturnValue({ + getAllAgentNames: () => [], + } as unknown as AgentRegistry); mockedGetDirectoryContextString.mockResolvedValue( 'Mocked Environment Context', @@ -411,6 +415,32 @@ describe('LocalAgentExecutor', () => { const secondPart = startHistory?.[1]?.parts?.[0]; expect(secondPart?.text).toBe('OK, starting on TestGoal.'); }); + + it('should filter out subagent tools to prevent recursion', async () => { + const subAgentName = 'recursive-agent'; + // Register a mock tool that simulates a subagent + parentToolRegistry.registerTool(new MockTool({ name: subAgentName })); + + // Mock the agent registry to return the subagent name + vi.spyOn( + mockConfig.getAgentRegistry(), + 'getAllAgentNames', + ).mockReturnValue([subAgentName]); + + const definition = createTestDefinition([LS_TOOL_NAME, subAgentName]); + const executor = await LocalAgentExecutor.create( + definition, + mockConfig, + onActivity, + ); + + const agentRegistry = executor['toolRegistry']; + + // LS should be present + expect(agentRegistry.getTool(LS_TOOL_NAME)).toBeDefined(); + // Subagent should be filtered out + expect(agentRegistry.getTool(subAgentName)).toBeUndefined(); + }); }); describe('run (Execution Loop and Logic)', () => { diff --git a/packages/core/src/agents/local-executor.ts b/packages/core/src/agents/local-executor.ts index d20ca4c51c..506f333684 100644 --- a/packages/core/src/agents/local-executor.ts +++ b/packages/core/src/agents/local-executor.ts @@ -110,10 +110,22 @@ export class LocalAgentExecutor { runtimeContext.getMessageBus(), ); const parentToolRegistry = runtimeContext.getToolRegistry(); + const allAgentNames = new Set( + runtimeContext.getAgentRegistry().getAllAgentNames(), + ); if (definition.toolConfig) { for (const toolRef of definition.toolConfig.tools) { if (typeof toolRef === 'string') { + // Check if the tool is a subagent to prevent recursion. + // We do not allow agents to call other agents. + if (allAgentNames.has(toolRef)) { + debugLogger.warn( + `[LocalAgentExecutor] Skipping subagent tool '${toolRef}' for agent '${definition.name}' to prevent recursion.`, + ); + continue; + } + // If the tool is referenced by name, retrieve it from the parent // registry and register it with the agent's isolated registry. const toolFromParent = parentToolRegistry.getTool(toolRef); diff --git a/packages/core/src/agents/registry.test.ts b/packages/core/src/agents/registry.test.ts index 3d0cdec1a0..e55f4214aa 100644 --- a/packages/core/src/agents/registry.test.ts +++ b/packages/core/src/agents/registry.test.ts @@ -943,10 +943,10 @@ describe('AgentRegistry', () => { }); }); - describe('getToolDescription', () => { + describe('getDirectoryContext', () => { it('should return default message when no agents are registered', () => { - expect(registry.getToolDescription()).toContain( - 'No agents are currently available', + expect(registry.getDirectoryContext()).toContain( + 'No sub-agents are currently available.', ); }); @@ -958,18 +958,12 @@ describe('AgentRegistry', () => { description: 'Another agent description', }); - const description = registry.getToolDescription(); + const description = registry.getDirectoryContext(); - expect(description).toContain( - 'Delegates a task to a specialized sub-agent', - ); - expect(description).toContain('Available agents:'); - expect(description).toContain( - `- **${MOCK_AGENT_V1.name}**: ${MOCK_AGENT_V1.description}`, - ); - expect(description).toContain( - `- **AnotherAgent**: Another agent description`, - ); + expect(description).toContain('Sub-agents are specialized expert agents'); + expect(description).toContain('Available Sub-Agents'); + expect(description).toContain(`- ${MOCK_AGENT_V1.name}`); + expect(description).toContain(`- AnotherAgent`); }); }); }); diff --git a/packages/core/src/agents/registry.ts b/packages/core/src/agents/registry.ts index 9b317d9e3c..cc68156344 100644 --- a/packages/core/src/agents/registry.ts +++ b/packages/core/src/agents/registry.ts @@ -21,7 +21,6 @@ import { type ModelConfig, ModelConfigService, } from '../services/modelConfigService.js'; -import { DELEGATE_TO_AGENT_TOOL_NAME } from '../tools/tool-names.js'; /** * Returns the model config alias for a given agent definition. @@ -393,23 +392,6 @@ export class AgentRegistry { return this.allDefinitions.get(name); } - /** - * Generates a description for the delegate_to_agent tool. - * Unlike getDirectoryContext() which is for system prompts, - * this is formatted for tool descriptions. - */ - getToolDescription(): string { - if (this.agents.size === 0) { - return 'Delegates a task to a specialized sub-agent. No agents are currently available.'; - } - - const agentDescriptions = Array.from(this.agents.entries()) - .map(([name, def]) => `- **${name}**: ${def.description}`) - .join('\n'); - - return `Delegates a task to a specialized sub-agent.\n\nAvailable agents:\n${agentDescriptions}`; - } - /** * Generates a markdown "Phone Book" of available agents and their schemas. * This MUST be injected into the System Prompt of the parent agent. @@ -423,20 +405,23 @@ export class AgentRegistry { context += `Sub-agents are specialized expert agents that you can use to assist you in the completion of all or part of a task. - ALWAYS use \`${DELEGATE_TO_AGENT_TOOL_NAME}\` to delegate to a subagent if one - exists that has expertise relevant to your task. + Each sub-agent is available as a tool of the same name. - For example: - - Prompt: 'Fix test', Description: 'An agent with expertise in fixing tests.' -> should use the sub-agent. - - Prompt: 'Update the license header', Description: 'An agent with expertise in licensing and copyright.' -> should use the sub-agent. - - Prompt: 'Diagram the architecture of the codebase', Description: 'Agent with architecture experience'. -> should use the sub-agent. - - Prompt: 'Implement a fix for [bug]' -> Should decompose the project into subtasks, which may utilize available agents like 'plan', 'validate', and 'fix-tests'. + You MUST always delegate tasks to the sub-agent with the + relevant expertise, if one is available. - The following are the available sub-agents:\n\n`; + The following tools can be used to start sub-agents:\n\n`; - for (const [name, def] of this.agents) { - context += `- **${name}**: ${def.description}\n`; + for (const [name] of this.agents) { + context += `- ${name}\n`; } + + context += `Remember that the closest relevant sub-agent should still be used even if its expertise is broader than the given task. + + For example: + - A license-agent -> Should be used for a range of tasks, including reading, validating, and updating licenses and headers. + - A test-fixing-agent -> Should be used both for fixing tests as well as investigating test failures.`; + return context; } } diff --git a/packages/core/src/agents/subagent-tool.ts b/packages/core/src/agents/subagent-tool.ts new file mode 100644 index 0000000000..191422753e --- /dev/null +++ b/packages/core/src/agents/subagent-tool.ts @@ -0,0 +1,132 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + BaseDeclarativeTool, + Kind, + type ToolInvocation, + type ToolResult, + BaseToolInvocation, + type ToolCallConfirmationDetails, +} from '../tools/tools.js'; +import type { AnsiOutput } from '../utils/terminalSerializer.js'; +import type { Config } from '../config/config.js'; +import type { MessageBus } from '../confirmation-bus/message-bus.js'; +import type { AgentDefinition, AgentInputs } from './types.js'; +import { SubagentToolWrapper } from './subagent-tool-wrapper.js'; +import { SchemaValidator } from '../utils/schemaValidator.js'; + +export class SubagentTool extends BaseDeclarativeTool { + constructor( + private readonly definition: AgentDefinition, + private readonly config: Config, + messageBus: MessageBus, + ) { + const inputSchema = definition.inputConfig.inputSchema; + + // Validate schema on construction + const schemaError = SchemaValidator.validateSchema(inputSchema); + if (schemaError) { + throw new Error( + `Invalid schema for agent ${definition.name}: ${schemaError}`, + ); + } + + super( + definition.name, + definition.displayName ?? definition.name, + definition.description, + Kind.Think, + inputSchema, + messageBus, + /* isOutputMarkdown */ true, + /* canUpdateOutput */ true, + ); + } + + protected createInvocation( + params: AgentInputs, + messageBus: MessageBus, + _toolName?: string, + _toolDisplayName?: string, + ): ToolInvocation { + return new SubAgentInvocation( + params, + this.definition, + this.config, + messageBus, + _toolName, + _toolDisplayName, + ); + } +} + +class SubAgentInvocation extends BaseToolInvocation { + constructor( + params: AgentInputs, + private readonly definition: AgentDefinition, + private readonly config: Config, + messageBus: MessageBus, + _toolName?: string, + _toolDisplayName?: string, + ) { + super( + params, + messageBus, + _toolName ?? definition.name, + _toolDisplayName ?? definition.displayName ?? definition.name, + ); + } + + getDescription(): string { + return `Delegating to agent '${this.definition.name}'`; + } + + override async shouldConfirmExecute( + abortSignal: AbortSignal, + ): Promise { + if (this.definition.kind !== 'remote') { + // Local agents should execute without confirmation. Inner tool calls will bubble up their own confirmations to the user. + return false; + } + + const invocation = this.buildSubInvocation(this.definition, this.params); + return invocation.shouldConfirmExecute(abortSignal); + } + + async execute( + signal: AbortSignal, + updateOutput?: (output: string | AnsiOutput) => void, + ): Promise { + const validationError = SchemaValidator.validate( + this.definition.inputConfig.inputSchema, + this.params, + ); + + if (validationError) { + throw new Error( + `Invalid arguments for agent '${this.definition.name}': ${validationError}. Input schema: ${JSON.stringify(this.definition.inputConfig.inputSchema)}.`, + ); + } + + const invocation = this.buildSubInvocation(this.definition, this.params); + + return invocation.execute(signal, updateOutput); + } + + private buildSubInvocation( + definition: AgentDefinition, + agentArgs: AgentInputs, + ): ToolInvocation { + const wrapper = new SubagentToolWrapper( + definition, + this.config, + this.messageBus, + ); + + return wrapper.build(agentArgs); + } +} diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index 23b9f78025..815104f231 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -155,8 +155,8 @@ vi.mock('../agents/registry.js', () => { return { AgentRegistry: AgentRegistryMock }; }); -vi.mock('../agents/delegate-to-agent-tool.js', () => ({ - DelegateToAgentTool: vi.fn(), +vi.mock('../agents/subagent-tool.js', () => ({ + SubagentTool: vi.fn(), })); vi.mock('../resources/resource-registry.js', () => ({ @@ -966,12 +966,15 @@ describe('Server Config (config.ts)', () => { AgentRegistryMock.prototype.getDefinition.mockReturnValue( mockAgentDefinition, ); + AgentRegistryMock.prototype.getAllDefinitions.mockReturnValue([ + mockAgentDefinition, + ]); - const DelegateToAgentToolMock = ( - (await vi.importMock('../agents/delegate-to-agent-tool.js')) as { - DelegateToAgentTool: Mock; + const SubAgentToolMock = ( + (await vi.importMock('../agents/subagent-tool.js')) as { + SubagentTool: Mock; } - ).DelegateToAgentTool; + ).SubagentTool; await config.initialize(); @@ -981,8 +984,8 @@ describe('Server Config (config.ts)', () => { } ).ToolRegistry.prototype.registerTool; - expect(DelegateToAgentToolMock).toHaveBeenCalledTimes(1); - expect(DelegateToAgentToolMock).toHaveBeenCalledWith( + expect(SubAgentToolMock).toHaveBeenCalledTimes(1); + expect(SubAgentToolMock).toHaveBeenCalledWith( expect.anything(), // AgentRegistry config, expect.anything(), // MessageBus @@ -990,7 +993,7 @@ describe('Server Config (config.ts)', () => { const calls = registerToolMock.mock.calls; const registeredWrappers = calls.filter( - (call) => call[0] instanceof DelegateToAgentToolMock, + (call) => call[0] instanceof SubAgentToolMock, ); expect(registeredWrappers).toHaveLength(1); }); @@ -1007,15 +1010,15 @@ describe('Server Config (config.ts)', () => { }; const config = new Config(params); - const DelegateToAgentToolMock = ( - (await vi.importMock('../agents/delegate-to-agent-tool.js')) as { - DelegateToAgentTool: Mock; + const SubAgentToolMock = ( + (await vi.importMock('../agents/subagent-tool.js')) as { + SubagentTool: Mock; } - ).DelegateToAgentTool; + ).SubagentTool; await config.initialize(); - expect(DelegateToAgentToolMock).not.toHaveBeenCalled(); + expect(SubAgentToolMock).not.toHaveBeenCalled(); }); describe('with minified tool class names', () => { diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 06806fe93e..921017c8de 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -101,8 +101,7 @@ import { getCodeAssistServer } from '../code_assist/codeAssist.js'; import type { Experiments } from '../code_assist/experiments/experiments.js'; import { AgentRegistry } from '../agents/registry.js'; import { setGlobalProxy } from '../utils/fetch.js'; -import { DelegateToAgentTool } from '../agents/delegate-to-agent-tool.js'; -import { DELEGATE_TO_AGENT_TOOL_NAME } from '../tools/tool-names.js'; +import { SubagentTool } from '../agents/subagent-tool.js'; import { getExperiments } from '../code_assist/experiments/experiments.js'; import { ExperimentFlags } from '../code_assist/experiments/flagNames.js'; import { debugLogger } from '../utils/debugLogger.js'; @@ -218,6 +217,7 @@ import { } from '../utils/extensionLoader.js'; import { McpClientManager } from '../tools/mcp-client-manager.js'; import type { EnvironmentSanitizationConfig } from '../services/environmentSanitization.js'; +import { getErrorMessage } from '../utils/errors.js'; export type { FileFilteringOptions }; export { @@ -1967,8 +1967,7 @@ export class Config { } // Register Subagents as Tools - // Register DelegateToAgentTool if agents are enabled - this.registerDelegateToAgentTool(registry); + this.registerSubAgentTools(registry); await registry.discoverAllTools(); registry.sortTools(); @@ -1976,27 +1975,36 @@ export class Config { } /** - * Registers the DelegateToAgentTool if agents or related features are enabled. + * Registers SubAgentTools for all available agents. */ - private registerDelegateToAgentTool(registry: ToolRegistry): void { + private registerSubAgentTools(registry: ToolRegistry): void { const agentsOverrides = this.getAgentsSettings().overrides ?? {}; if ( this.isAgentsEnabled() || agentsOverrides['codebase_investigator']?.enabled !== false || agentsOverrides['cli_help']?.enabled !== false ) { - // Check if the delegate tool itself is allowed (if allowedTools is set) const allowedTools = this.getAllowedTools(); - const isAllowed = - !allowedTools || allowedTools.includes(DELEGATE_TO_AGENT_TOOL_NAME); + const definitions = this.agentRegistry.getAllDefinitions(); - if (isAllowed) { - const delegateTool = new DelegateToAgentTool( - this.agentRegistry, - this, - this.getMessageBus(), - ); - registry.registerTool(delegateTool); + for (const definition of definitions) { + const isAllowed = + !allowedTools || allowedTools.includes(definition.name); + + if (isAllowed) { + try { + const tool = new SubagentTool( + definition, + this, + this.getMessageBus(), + ); + registry.registerTool(tool); + } catch (e: unknown) { + debugLogger.warn( + `Failed to register tool for agent ${definition.name}: ${getErrorMessage(e)}`, + ); + } + } } } } @@ -2092,7 +2100,7 @@ export class Config { private onAgentsRefreshed = async () => { if (this.toolRegistry) { - this.registerDelegateToAgentTool(this.toolRegistry); + this.registerSubAgentTools(this.toolRegistry); } // Propagate updates to the active chat session const client = this.getGeminiClient(); diff --git a/packages/core/src/core/__snapshots__/prompts.test.ts.snap b/packages/core/src/core/__snapshots__/prompts.test.ts.snap index 9ef64e312c..0336ffcf9a 100644 --- a/packages/core/src/core/__snapshots__/prompts.test.ts.snap +++ b/packages/core/src/core/__snapshots__/prompts.test.ts.snap @@ -453,7 +453,7 @@ Mock Agent Directory ## Software Engineering Tasks When requested to perform tasks like fixing bugs, adding features, refactoring, or explaining code, follow this sequence: -1. **Understand & Strategize:** Think about the user's request and the relevant codebase context. When the task involves **complex refactoring, codebase exploration or system-wide analysis**, your **first and primary action** must be to delegate to the 'codebase_investigator' agent using the 'delegate_to_agent' tool. Use it to build a comprehensive understanding of the code, its structure, and dependencies. For **simple, targeted searches** (like finding a specific function name, file path, or variable declaration), you should use 'search_file_content' or 'glob' directly. +1. **Understand & Strategize:** Think about the user's request and the relevant codebase context. When the task involves **complex refactoring, codebase exploration or system-wide analysis**, your **first and primary action** must be to delegate to the 'codebase_investigator' agent using the 'codebase_investigator' tool. Use it to build a comprehensive understanding of the code, its structure, and dependencies. For **simple, targeted searches** (like finding a specific function name, file path, or variable declaration), you should use 'search_file_content' or 'glob' directly. 2. **Plan:** Build a coherent and grounded (based on the understanding in step 1) plan for how you intend to resolve the user's task. If 'codebase_investigator' was used, do not ignore the output of the agent, you must use it as the foundation of your plan. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process. As part of the plan, you should use an iterative development process that includes writing unit tests to verify your changes. Use output logs or debug statements as part of this process to arrive at a solution. 3. **Implement:** Use the available tools (e.g., 'replace', 'write_file' 'run_shell_command' ...) to act on the plan, strictly adhering to the project's established conventions (detailed under 'Core Mandates'). 4. **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. Identify the correct test commands and frameworks by examining 'README' files, build/package configuration (e.g., 'package.json'), or existing test execution patterns. NEVER assume standard test commands. When executing test commands, prefer "run once" or "CI" modes to ensure the command terminates after completion. diff --git a/packages/core/src/core/prompts.ts b/packages/core/src/core/prompts.ts index 81b0570314..1d079c272c 100644 --- a/packages/core/src/core/prompts.ts +++ b/packages/core/src/core/prompts.ts @@ -15,7 +15,6 @@ import { SHELL_TOOL_NAME, WRITE_FILE_TOOL_NAME, WRITE_TODOS_TOOL_NAME, - DELEGATE_TO_AGENT_TOOL_NAME, ACTIVATE_SKILL_TOOL_NAME, } from '../tools/tool-names.js'; import process from 'node:process'; @@ -198,7 +197,7 @@ Use '${READ_FILE_TOOL_NAME}' to understand context and validate any assumptions ## Software Engineering Tasks When requested to perform tasks like fixing bugs, adding features, refactoring, or explaining code, follow this sequence: -1. **Understand & Strategize:** Think about the user's request and the relevant codebase context. When the task involves **complex refactoring, codebase exploration or system-wide analysis**, your **first and primary action** must be to delegate to the '${CodebaseInvestigatorAgent.name}' agent using the '${DELEGATE_TO_AGENT_TOOL_NAME}' tool. Use it to build a comprehensive understanding of the code, its structure, and dependencies. For **simple, targeted searches** (like finding a specific function name, file path, or variable declaration), you should use '${GREP_TOOL_NAME}' or '${GLOB_TOOL_NAME}' directly. +1. **Understand & Strategize:** Think about the user's request and the relevant codebase context. When the task involves **complex refactoring, codebase exploration or system-wide analysis**, your **first and primary action** must be to delegate to the '${CodebaseInvestigatorAgent.name}' agent using the '${CodebaseInvestigatorAgent.name}' tool. Use it to build a comprehensive understanding of the code, its structure, and dependencies. For **simple, targeted searches** (like finding a specific function name, file path, or variable declaration), you should use '${GREP_TOOL_NAME}' or '${GLOB_TOOL_NAME}' directly. 2. **Plan:** Build a coherent and grounded (based on the understanding in step 1) plan for how you intend to resolve the user's task. If '${CodebaseInvestigatorAgent.name}' was used, do not ignore the output of the agent, you must use it as the foundation of your plan. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process. As part of the plan, you should use an iterative development process that includes writing unit tests to verify your changes. Use output logs or debug statements as part of this process to arrive at a solution.`, primaryWorkflows_prefix_ci_todo: ` @@ -206,7 +205,7 @@ When requested to perform tasks like fixing bugs, adding features, refactoring, ## Software Engineering Tasks When requested to perform tasks like fixing bugs, adding features, refactoring, or explaining code, follow this sequence: -1. **Understand & Strategize:** Think about the user's request and the relevant codebase context. When the task involves **complex refactoring, codebase exploration or system-wide analysis**, your **first and primary action** must be to delegate to the '${CodebaseInvestigatorAgent.name}' agent using the '${DELEGATE_TO_AGENT_TOOL_NAME}' tool. Use it to build a comprehensive understanding of the code, its structure, and dependencies. For **simple, targeted searches** (like finding a specific function name, file path, or variable declaration), you should use '${GREP_TOOL_NAME}' or '${GLOB_TOOL_NAME}' directly. +1. **Understand & Strategize:** Think about the user's request and the relevant codebase context. When the task involves **complex refactoring, codebase exploration or system-wide analysis**, your **first and primary action** must be to delegate to the '${CodebaseInvestigatorAgent.name}' agent using the '${CodebaseInvestigatorAgent.name}' tool. Use it to build a comprehensive understanding of the code, its structure, and dependencies. For **simple, targeted searches** (like finding a specific function name, file path, or variable declaration), you should use '${GREP_TOOL_NAME}' or '${GLOB_TOOL_NAME}' directly. 2. **Plan:** Build a coherent and grounded (based on the understanding in step 1) plan for how you intend to resolve the user's task. If '${CodebaseInvestigatorAgent.name}' was used, do not ignore the output of the agent, you must use it as the foundation of your plan. For complex tasks, break them down into smaller, manageable subtasks and use the \`${WRITE_TODOS_TOOL_NAME}\` tool to track your progress. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process. As part of the plan, you should use an iterative development process that includes writing unit tests to verify your changes. Use output logs or debug statements as part of this process to arrive at a solution.`, primaryWorkflows_todo: ` diff --git a/packages/core/src/tools/tool-names.ts b/packages/core/src/tools/tool-names.ts index 42ccadc877..34e18c42a6 100644 --- a/packages/core/src/tools/tool-names.ts +++ b/packages/core/src/tools/tool-names.ts @@ -23,7 +23,6 @@ export const MEMORY_TOOL_NAME = 'save_memory'; export const GET_INTERNAL_DOCS_TOOL_NAME = 'get_internal_docs'; export const ACTIVATE_SKILL_TOOL_NAME = 'activate_skill'; export const EDIT_TOOL_NAMES = new Set([EDIT_TOOL_NAME, WRITE_FILE_TOOL_NAME]); -export const DELEGATE_TO_AGENT_TOOL_NAME = 'delegate_to_agent'; export const ASK_USER_TOOL_NAME = 'ask_user'; /** Prefix used for tools discovered via the toolDiscoveryCommand. */ @@ -46,7 +45,6 @@ export const ALL_BUILTIN_TOOL_NAMES = [ LS_TOOL_NAME, MEMORY_TOOL_NAME, ACTIVATE_SKILL_TOOL_NAME, - DELEGATE_TO_AGENT_TOOL_NAME, ASK_USER_TOOL_NAME, ] as const;