mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-08 16:11:58 -07:00
f1a3387160
Introduce LocalSubagentProtocol and RemoteSubagentProtocol that implement AgentProtocol, wrapping LocalAgentExecutor and A2A client streaming respectively. LocalSubagentSession and RemoteSubagentSession extend AgentSession and are the public entry points. - LocalSubagentProtocol: translates SubagentActivityEvent -> AgentEvent (THOUGHT_CHUNK->message/thought, TOOL_CALL_START->tool_request, TOOL_CALL_END->tool_response, ERROR->error). Accepts optional rawActivityCallback for rich SubagentProgress display without losing displayName/errorType detail that AgentEvent types do not carry. - RemoteSubagentProtocol: wraps A2A sendMessageStream, maintains contextId/taskId session state, tracks SubagentProgress per chunk for error recovery, and returns a ToolResult with proper SubagentProgress as returnDisplay. - LocalSubagentInvocation: now uses LocalSubagentSession internally, preserving all existing SubagentProgress display logic via rawActivityCallback. External AbortSignal wired through session.abort(). - RemoteAgentInvocation: now uses RemoteSubagentSession, subscribing to message events for live progress updates. - SubagentToolWrapper and SubagentTool: add optional onAgentEvent callback for future parent session observability (currently unused, wired through invocation constructors to avoid a second pass later). - index.ts: export LocalSubagentSession and RemoteSubagentSession. No behavioral change to SubagentProgress display or ToolResult output.
197 lines
6.0 KiB
TypeScript
197 lines
6.0 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import {
|
|
BaseToolInvocation,
|
|
type ToolConfirmationOutcome,
|
|
type ToolResult,
|
|
type ToolCallConfirmationDetails,
|
|
type ToolLiveOutput,
|
|
} from '../tools/tools.js';
|
|
import {
|
|
DEFAULT_QUERY_STRING,
|
|
type RemoteAgentInputs,
|
|
type RemoteAgentDefinition,
|
|
type AgentInputs,
|
|
type SubagentProgress,
|
|
} from './types.js';
|
|
import { type AgentLoopContext } from '../config/agent-loop-context.js';
|
|
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
|
import { A2AAgentError } from './a2a-errors.js';
|
|
import { RemoteSubagentSession } from './remote-subagent-protocol.js';
|
|
import type { AgentEvent } from '../agent/types.js';
|
|
|
|
/**
|
|
* A tool invocation that proxies to a remote A2A agent.
|
|
*
|
|
* This implementation delegates execution to {@link RemoteSubagentSession},
|
|
* which wraps the A2A client streaming behind the AgentProtocol interface.
|
|
*/
|
|
export class RemoteAgentInvocation extends BaseToolInvocation<
|
|
RemoteAgentInputs,
|
|
ToolResult
|
|
> {
|
|
constructor(
|
|
private readonly definition: RemoteAgentDefinition,
|
|
private readonly context: AgentLoopContext,
|
|
params: AgentInputs,
|
|
messageBus: MessageBus,
|
|
_toolName?: string,
|
|
_toolDisplayName?: string,
|
|
private readonly _onAgentEvent?: (event: AgentEvent) => void,
|
|
) {
|
|
const query = params['query'] ?? DEFAULT_QUERY_STRING;
|
|
if (typeof query !== 'string') {
|
|
throw new Error(
|
|
`Remote agent '${definition.name}' requires a string 'query' input.`,
|
|
);
|
|
}
|
|
// Safe to pass strict object to super
|
|
super(
|
|
{ query },
|
|
messageBus,
|
|
_toolName ?? definition.name,
|
|
_toolDisplayName ?? definition.displayName,
|
|
);
|
|
|
|
// Validate that A2AClientManager is available at construction time
|
|
if (!this.context.config.getA2AClientManager()) {
|
|
throw new Error(
|
|
`Failed to initialize RemoteAgentInvocation for '${definition.name}': A2AClientManager is not available.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
getDescription(): string {
|
|
return `Calling remote agent ${this.definition.displayName ?? this.definition.name}`;
|
|
}
|
|
|
|
protected override async getConfirmationDetails(
|
|
_abortSignal: AbortSignal,
|
|
): Promise<ToolCallConfirmationDetails | false> {
|
|
// For now, always require confirmation for remote agents until we have a policy system for them.
|
|
return {
|
|
type: 'info',
|
|
title: `Call Remote Agent: ${this.definition.displayName ?? this.definition.name}`,
|
|
prompt: `Calling remote agent: "${this.params.query}"`,
|
|
onConfirm: async (_outcome: ToolConfirmationOutcome) => {
|
|
// Policy updates are now handled centrally by the scheduler
|
|
},
|
|
};
|
|
}
|
|
|
|
async execute(
|
|
_signal: AbortSignal,
|
|
updateOutput?: (output: ToolLiveOutput) => void,
|
|
): Promise<ToolResult> {
|
|
const agentName = this.definition.displayName ?? this.definition.name;
|
|
const session = new RemoteSubagentSession(
|
|
this.definition,
|
|
this.context,
|
|
this.messageBus,
|
|
);
|
|
|
|
// Wire external abort signal to session abort
|
|
const abortListener = () => void session.abort();
|
|
_signal.addEventListener('abort', abortListener, { once: true });
|
|
|
|
// Subscribe for parent session observability (future use)
|
|
let unsubscribeParent: (() => void) | undefined;
|
|
if (this._onAgentEvent) {
|
|
unsubscribeParent = session.subscribe(this._onAgentEvent);
|
|
}
|
|
|
|
// Subscribe to message events for live SubagentProgress updates
|
|
const unsubscribeProgress = session.subscribe((event: AgentEvent) => {
|
|
if (event.type === 'message' && updateOutput) {
|
|
const currentProgress = session.getLatestProgress();
|
|
if (currentProgress) updateOutput(currentProgress);
|
|
}
|
|
});
|
|
|
|
try {
|
|
if (updateOutput) {
|
|
updateOutput({
|
|
isSubagentProgress: true,
|
|
agentName,
|
|
state: 'running',
|
|
recentActivity: [
|
|
{
|
|
id: 'pending',
|
|
type: 'thought',
|
|
content: 'Working...',
|
|
status: 'running',
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
await session.send({
|
|
message: [{ type: 'text', text: this.params.query }],
|
|
});
|
|
|
|
const result = await session.getResult();
|
|
|
|
// Emit final completed progress
|
|
if (updateOutput) {
|
|
const finalProgress = session.getLatestProgress();
|
|
if (finalProgress) updateOutput(finalProgress);
|
|
}
|
|
|
|
return result;
|
|
} catch (error: unknown) {
|
|
const partialProgress = session.getLatestProgress();
|
|
const partialOutput =
|
|
typeof partialProgress?.result === 'string'
|
|
? partialProgress.result
|
|
: '';
|
|
const errorMessage = this.formatExecutionError(error);
|
|
const fullDisplay = partialOutput
|
|
? `${partialOutput}\n\n${errorMessage}`
|
|
: errorMessage;
|
|
|
|
const errorProgress: SubagentProgress = {
|
|
isSubagentProgress: true,
|
|
agentName,
|
|
state: 'error',
|
|
result: fullDisplay,
|
|
recentActivity: partialProgress?.recentActivity ?? [],
|
|
};
|
|
|
|
if (updateOutput) {
|
|
updateOutput(errorProgress);
|
|
}
|
|
|
|
return {
|
|
llmContent: [{ text: fullDisplay }],
|
|
returnDisplay: errorProgress,
|
|
};
|
|
} finally {
|
|
_signal.removeEventListener('abort', abortListener);
|
|
unsubscribeProgress();
|
|
unsubscribeParent?.();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Formats an execution error into a user-friendly message.
|
|
* Recognizes typed A2AAgentError subclasses and falls back to
|
|
* a generic message for unknown errors.
|
|
*/
|
|
private formatExecutionError(error: unknown): string {
|
|
// All A2A-specific errors include a human-friendly `userMessage` on the
|
|
// A2AAgentError base class. Rely on that to avoid duplicating messages
|
|
// for specific subclasses, which improves maintainability.
|
|
if (error instanceof A2AAgentError) {
|
|
return error.userMessage;
|
|
}
|
|
|
|
return `Error calling remote agent: ${
|
|
error instanceof Error ? error.message : String(error)
|
|
}`;
|
|
}
|
|
}
|