feat(core): Improve validation logic for subagents.

This commit is contained in:
Your Name
2026-01-22 18:25:23 +00:00
parent 87a0db20d2
commit df22fd22e7
2 changed files with 56 additions and 58 deletions

View File

@@ -121,19 +121,22 @@ describe('DelegateToAgentTool', () => {
);
});
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(
it('should throw helpful error when agent_name does not exist', () => {
expect(() =>
tool.build({
agent_name: 'non_existent_agent',
} as DelegateParams),
).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 throw error when agent_name is missing', () => {
expect(() => tool.build({} as DelegateParams)).toThrow(
"Missing 'agent_name' parameter. Please specify the agent you want to delegate to.",
);
});
it('should validate correct arguments', async () => {
const invocation = tool.build({
agent_name: 'test_agent',
@@ -152,28 +155,24 @@ describe('DelegateToAgentTool', () => {
);
});
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(
it('should throw helpful error for missing required argument', () => {
expect(() =>
tool.build({
agent_name: 'test_agent',
arg2: 123,
} as DelegateParams),
).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(
it('should throw helpful error for invalid argument type', () => {
expect(() =>
tool.build({
agent_name: 'test_agent',
arg1: 123,
} as DelegateParams),
).toThrow(
`Invalid arguments for agent 'test_agent': params/arg1 must be string. Input schema: ${JSON.stringify(mockAgentDef.inputConfig.inputSchema)}.`,
);
});

View File

@@ -121,11 +121,33 @@ 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.
override validateToolParams(params: DelegateParams): string | null {
if (!params.agent_name) {
return "Missing 'agent_name' parameter. Please specify the agent you want to delegate to.";
}
const definition = this.registry.getDefinition(params.agent_name);
if (!definition) {
const availableAgents = this.registry
.getAllDefinitions()
.map((def) => `'${def.name}' (${def.description})`)
.join(', ');
return `Agent '${params.agent_name}' not found. Available agents are: ${availableAgents}. Please choose a valid agent_name.`;
}
const { agent_name: _agent_name, ...agentArgs } = params;
// Validate specific agent arguments here using SchemaValidator to generate helpful error messages.
const validationError = SchemaValidator.validate(
definition.inputConfig.inputSchema,
agentArgs,
);
if (validationError) {
return `Invalid arguments for agent '${definition.name}': ${validationError}. Input schema: ${JSON.stringify(definition.inputConfig.inputSchema)}.`;
}
return null;
}
@@ -173,8 +195,8 @@ class DelegateInvocation extends BaseToolInvocation<
override async shouldConfirmExecute(
abortSignal: AbortSignal,
): Promise<ToolCallConfirmationDetails | false> {
const definition = this.registry.getDefinition(this.params.agent_name);
if (!definition || definition.kind !== 'remote') {
const definition = this.registry.getDefinition(this.params.agent_name)!;
if (definition.kind !== 'remote') {
// Local agents should execute without confirmation. Inner tool calls will bubble up their own confirmations to the user.
return false;
}
@@ -191,32 +213,9 @@ class DelegateInvocation extends BaseToolInvocation<
signal: AbortSignal,
updateOutput?: (output: string | AnsiOutput) => void,
): Promise<ToolResult> {
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 definition = this.registry.getDefinition(this.params.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,