/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import type { Config } from '../config/config.js'; import { LocalAgentExecutor } from './local-executor.js'; import type { AnsiOutput } from '../utils/terminalSerializer.js'; import { BaseToolInvocation, type ToolResult } from '../tools/tools.js'; import { ToolErrorType } from '../tools/tool-error.js'; import type { LocalAgentDefinition, AgentInputs, SubagentActivityEvent, } from './types.js'; import type { MessageBus } from '../confirmation-bus/message-bus.js'; import { AgentFactory } from './agent-factory.js'; import type { Turn } from '../core/turn.js'; import { GeminiEventType } from '../core/turn.js'; import { promptIdContext } from '../utils/promptIdContext.js'; const INPUT_PREVIEW_MAX_LENGTH = 50; const DESCRIPTION_MAX_LENGTH = 200; /** * Represents a validated, executable instance of a subagent tool. * * This class orchestrates the execution of a defined agent by: * 1. Initializing the {@link LocalAgentExecutor}. * 2. Running the agent's execution loop. * 3. Bridging the agent's streaming activity (e.g., thoughts) to the tool's * live output stream. * 4. Formatting the final result into a {@link ToolResult}. */ export class LocalSubagentInvocation extends BaseToolInvocation< AgentInputs, ToolResult > { /** * @param definition The definition object that configures the agent. * @param config The global runtime configuration. * @param params The validated input parameters for the agent. * @param messageBus Message bus for policy enforcement. */ constructor( private readonly definition: LocalAgentDefinition, private readonly config: Config, params: AgentInputs, messageBus: MessageBus, _toolName?: string, _toolDisplayName?: string, ) { super( params, messageBus, _toolName ?? definition.name, _toolDisplayName ?? definition.displayName, ); } /** * Returns a concise, human-readable description of the invocation. * Used for logging and display purposes. */ getDescription(): string { const inputSummary = Object.entries(this.params) .map( ([key, value]) => `${key}: ${String(value).slice(0, INPUT_PREVIEW_MAX_LENGTH)}`, ) .join(', '); const description = `Running subagent '${this.definition.name}' with inputs: { ${inputSummary} }`; return description.slice(0, DESCRIPTION_MAX_LENGTH); } /** * Executes the subagent. * * @param signal An `AbortSignal` to cancel the agent's execution. * @param updateOutput A callback to stream intermediate output, such as the * agent's thoughts, to the user interface. * @returns A `Promise` that resolves with the final `ToolResult`. */ async execute( signal: AbortSignal, updateOutput?: (output: string | AnsiOutput) => void, ): Promise { if (this.config.isAgentHarnessEnabled()) { return this.executeWithHarness(signal, updateOutput); } try { if (updateOutput) { updateOutput('Subagent starting...\n'); } // Create an activity callback to bridge the executor's events to the // tool's streaming output. const onActivity = (activity: SubagentActivityEvent): void => { if (!updateOutput) return; if ( activity.type === 'THOUGHT_CHUNK' && typeof activity.data['text'] === 'string' ) { updateOutput(`🤖💭 ${activity.data['text']}`); } }; const executor = await LocalAgentExecutor.create( this.definition, this.config, onActivity, ); const output = await executor.run(this.params, signal); const resultContent = `Subagent '${this.definition.name}' finished. Termination Reason: ${output.terminate_reason} Result: ${output.result}`; const displayContent = ` Subagent ${this.definition.name} Finished Termination Reason:\n ${output.terminate_reason} Result: ${output.result} `; return { llmContent: [{ text: resultContent }], returnDisplay: displayContent, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { llmContent: `Subagent '${this.definition.name}' failed. Error: ${errorMessage}`, returnDisplay: `Subagent Failed: ${this.definition.name}\nError: ${errorMessage}`, error: { message: errorMessage, type: ToolErrorType.EXECUTION_FAILED, }, }; } } private async executeWithHarness( signal: AbortSignal, updateOutput?: (output: string | AnsiOutput) => void, ): Promise { try { if (updateOutput) { updateOutput('Subagent starting (Harness Mode)...\n'); } const harness = AgentFactory.createHarness(this.config, this.definition, { inputs: this.params, parentPromptId: promptIdContext.getStore(), }); const initialRequest = [{ text: 'Start' }]; const stream = harness.run(initialRequest, signal); let turn: Turn | undefined; while (true) { const { value, done } = await stream.next(); if (done) { turn = value; break; } const event = value; if (updateOutput) { if (event.type === GeminiEventType.Thought) { updateOutput(`🤖💭 ${event.value.subject}`); } else if (event.type === GeminiEventType.SubagentActivity) { if (event.value.type === 'TOOL_CALL_START') { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const toolName = event.value.data['name'] as string; updateOutput(`🛠️ Calling tool: ${toolName}...`); } } } } if (!turn) { throw new Error('Agent failed to return a valid turn.'); } const output = turn.getResponseText(); const displayContent = ` Subagent ${this.definition.name} Finished (Harness Mode) Result: ${output} `; return { llmContent: [{ text: output }], returnDisplay: displayContent, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { llmContent: `Subagent '${this.definition.name}' failed (Harness Mode). Error: ${errorMessage}`, returnDisplay: `Subagent Failed: ${this.definition.name}\nError: ${errorMessage}`, error: { message: errorMessage, type: ToolErrorType.EXECUTION_FAILED, }, }; } } }