mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-20 18:14:29 -07:00
fix(core): Provide compact, actionable errors for agent delegation failures (#16493)
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
export type DelegateParams = { agent_name: string } & Record<string, unknown>;
|
||||
|
||||
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<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 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<string, z.ZodTypeAny> = {};
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user