/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; 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'; 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 schema: z.ZodTypeAny; if (definitions.length === 0) { // Fallback if no agents are registered (mostly for testing/safety) schema = z.object({ agent_name: z.string().describe('No agents are currently available.'), }); } else { const agentSchemas = definitions.map((def) => { const inputShape: Record = { agent_name: z.literal(def.name).describe(def.description), }; for (const [key, inputDef] of Object.entries(def.inputConfig.inputs)) { if (key === 'agent_name') { throw new Error( `Agent '${def.name}' cannot have an input parameter named 'agent_name' as it is a reserved parameter for delegation.`, ); } let validator: z.ZodTypeAny; // Map input types to Zod 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: { // This provides compile-time exhaustiveness checking. const _exhaustiveCheck: never = inputDef.type; void _exhaustiveCheck; throw new Error(`Unhandled agent input type: '${inputDef.type}'`); } } if (!inputDef.required) { validator = validator.optional(); } inputShape[key] = validator.describe(inputDef.description); } // Cast required because Zod can't infer the discriminator from dynamic keys return z.object( inputShape, ) as z.ZodDiscriminatedUnionOption<'agent_name'>; }); // Create the discriminated union // z.discriminatedUnion requires at least 2 options, so we handle the single agent case if (agentSchemas.length === 1) { schema = agentSchemas[0]; } else { schema = z.discriminatedUnion( 'agent_name', agentSchemas as [ z.ZodDiscriminatedUnionOption<'agent_name'>, z.ZodDiscriminatedUnionOption<'agent_name'>, ...Array>, ], ); } } super( DELEGATE_TO_AGENT_TOOL_NAME, 'Delegate to Agent', registry.getToolDescription(), Kind.Think, zodToJsonSchema(schema), messageBus, /* isOutputMarkdown */ true, /* canUpdateOutput */ true, ); } 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) { throw new Error( `Agent '${this.params.agent_name}' not found in registry.`, ); } const { agent_name: _agent_name, ...agentArgs } = this.params; 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); } }