diff --git a/packages/core/src/agents/delegate-to-agent-tool.test.ts b/packages/core/src/agents/delegate-to-agent-tool.test.ts index b709c01911..bb60048835 100644 --- a/packages/core/src/agents/delegate-to-agent-tool.test.ts +++ b/packages/core/src/agents/delegate-to-agent-tool.test.ts @@ -5,7 +5,10 @@ */ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { DelegateToAgentTool } from './delegate-to-agent-tool.js'; +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'; @@ -110,14 +113,17 @@ describe('DelegateToAgentTool', () => { ); }); - it('should validate agent_name exists in registry', async () => { - // Zod validation happens at build time now (or rather, build validates the schema) - // Since we use discriminated union, an invalid agent_name won't match any option. - expect(() => - tool.build({ - agent_name: 'non_existent_agent', - }), - ).toThrow(); + 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 () => { @@ -138,24 +144,30 @@ describe('DelegateToAgentTool', () => { ); }); - it('should throw error for missing required argument', async () => { - // Missing arg1 should fail Zod validation - expect(() => - tool.build({ - agent_name: 'test_agent', - arg2: 123, - }), - ).toThrow(); + 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( + "arg1: Required. Expected inputs: 'arg1' (required string), 'arg2' (optional number).", + ); }); - it('should throw error for invalid argument type', async () => { - // arg1 should be string, passing number - expect(() => - tool.build({ - agent_name: 'test_agent', - arg1: 123, - }), - ).toThrow(); + 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( + "arg1: Expected string, received number. Expected inputs: 'arg1' (required string), 'arg2' (optional number).", + ); }); it('should allow optional arguments to be omitted', async () => { diff --git a/packages/core/src/agents/delegate-to-agent-tool.ts b/packages/core/src/agents/delegate-to-agent-tool.ts index 84d398aa4d..be833a7f01 100644 --- a/packages/core/src/agents/delegate-to-agent-tool.ts +++ b/packages/core/src/agents/delegate-to-agent-tool.ts @@ -22,7 +22,7 @@ import type { MessageBus } from '../confirmation-bus/message-bus.js'; import type { AgentDefinition, AgentInputs } from './types.js'; import { SubagentToolWrapper } from './subagent-tool-wrapper.js'; -type DelegateParams = { agent_name: string } & Record; +export type DelegateParams = { agent_name: string } & Record; export class DelegateToAgentTool extends BaseDeclarativeTool< DelegateParams, @@ -126,6 +126,14 @@ export class DelegateToAgentTool extends BaseDeclarativeTool< ); } + 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, @@ -190,12 +198,79 @@ class DelegateInvocation extends BaseToolInvocation< ): 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 in registry.`, + `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 Zod to generate helpful error messages. + const inputShape: Record = {}; + for (const [key, inputDef] of Object.entries( + definition.inputConfig.inputs, + )) { + let validator: z.ZodTypeAny; + + switch (inputDef.type) { + case 'string': + validator = z.string(); + break; + case 'number': + validator = z.number(); + break; + case 'boolean': + validator = z.boolean(); + break; + case 'integer': + validator = z.number().int(); + break; + case 'string[]': + validator = z.array(z.string()); + break; + case 'number[]': + validator = z.array(z.number()); + break; + default: + validator = z.unknown(); + } + + if (!inputDef.required) { + validator = validator.optional(); + } + + inputShape[key] = validator.describe(inputDef.description); + } + + const agentSchema = z.object(inputShape); + + try { + agentSchema.parse(agentArgs); + } catch (e) { + if (e instanceof z.ZodError) { + const errorMessages = e.errors.map( + (err) => `${err.path.join('.')}: ${err.message}`, + ); + + const expectedInputs = Object.entries(definition.inputConfig.inputs) + .map( + ([key, input]) => + `'${key}' (${input.required ? 'required' : 'optional'} ${input.type})`, + ) + .join(', '); + + throw new Error( + `${errorMessages.join(', ')}. Expected inputs: ${expectedInputs}.`, + ); + } + throw e; + } + const invocation = this.buildSubInvocation( definition, agentArgs as AgentInputs,