| |
| |
| |
| |
| |
|
|
| import { |
| BaseToolInvocation, |
| type ToolConfirmationOutcome, |
| type ToolResult, |
| type ToolCallConfirmationDetails, |
| type ExecuteOptions, |
| } from '../tools/tools.js'; |
| import { |
| DEFAULT_QUERY_STRING, |
| type RemoteAgentInputs, |
| type RemoteAgentDefinition, |
| type AgentInputs, |
| type SubagentProgress, |
| SubagentState, |
| getAgentCardLoadOptions, |
| getRemoteAgentTargetUrl, |
| } from './types.js'; |
| import { type AgentLoopContext } from '../config/agent-loop-context.js'; |
| import type { MessageBus } from '../confirmation-bus/message-bus.js'; |
| import type { |
| A2AClientManager, |
| SendMessageResult, |
| } from './a2a-client-manager.js'; |
| import { extractIdsFromResponse, A2AResultReassembler } from './a2aUtils.js'; |
| import type { AuthenticationHandler } from '@a2a-js/sdk/client'; |
| import { debugLogger } from '../utils/debugLogger.js'; |
| import { A2AAuthProviderFactory } from './auth-provider/factory.js'; |
| import { A2AAgentError } from './a2a-errors.js'; |
|
|
| |
| |
| |
| |
| |
| |
| export class RemoteAgentInvocation extends BaseToolInvocation< |
| RemoteAgentInputs, |
| ToolResult |
| > { |
| |
| private static readonly sessionState = new Map< |
| string, |
| { contextId?: string; taskId?: string } |
| >(); |
| |
| private contextId: string | undefined; |
| private taskId: string | undefined; |
|
|
| private readonly clientManager: A2AClientManager; |
| private authHandler: AuthenticationHandler | undefined; |
|
|
| constructor( |
| private readonly definition: RemoteAgentDefinition, |
| private readonly context: AgentLoopContext, |
| params: AgentInputs, |
| messageBus: MessageBus, |
| _toolName?: string, |
| _toolDisplayName?: string, |
| ) { |
| const query = params['query'] ?? DEFAULT_QUERY_STRING; |
| if (typeof query !== 'string') { |
| throw new Error( |
| `Remote agent '${definition.name}' requires a string 'query' input.`, |
| ); |
| } |
| |
| super( |
| { query }, |
| messageBus, |
| _toolName ?? definition.name, |
| _toolDisplayName ?? definition.displayName, |
| ); |
| const clientManager = this.context.config.getA2AClientManager(); |
| if (!clientManager) { |
| throw new Error( |
| `Failed to initialize RemoteAgentInvocation for '${definition.name}': A2AClientManager is not available.`, |
| ); |
| } |
| this.clientManager = clientManager; |
| } |
|
|
| getDescription(): string { |
| return `Calling remote agent ${this.definition.displayName ?? this.definition.name}`; |
| } |
|
|
| private async getAuthHandler(): Promise<AuthenticationHandler | undefined> { |
| if (this.authHandler) { |
| return this.authHandler; |
| } |
|
|
| if (this.definition.auth) { |
| const targetUrl = getRemoteAgentTargetUrl(this.definition); |
| const provider = await A2AAuthProviderFactory.create({ |
| authConfig: this.definition.auth, |
| agentName: this.definition.name, |
| targetUrl, |
| agentCardUrl: this.definition.agentCardUrl, |
| }); |
| if (!provider) { |
| throw new Error( |
| `Failed to create auth provider for agent '${this.definition.name}'`, |
| ); |
| } |
| this.authHandler = provider; |
| } |
|
|
| return this.authHandler; |
| } |
|
|
| protected override async getConfirmationDetails( |
| _abortSignal: AbortSignal, |
| ): Promise<ToolCallConfirmationDetails | false> { |
| |
| return { |
| type: 'info', |
| title: `Call Remote Agent: ${this.definition.displayName ?? this.definition.name}`, |
| prompt: `Calling remote agent: "${this.params.query}"`, |
| onConfirm: async (_outcome: ToolConfirmationOutcome) => { |
| |
| }, |
| }; |
| } |
|
|
| async execute(options: ExecuteOptions): Promise<ToolResult> { |
| const { abortSignal: _signal, updateOutput } = options; |
| |
| |
| |
| const reassembler = new A2AResultReassembler(); |
| const agentName = this.definition.displayName ?? this.definition.name; |
| try { |
| if (updateOutput) { |
| updateOutput({ |
| isSubagentProgress: true, |
| agentName, |
| state: SubagentState.RUNNING, |
| recentActivity: [ |
| { |
| id: 'pending', |
| type: 'thought', |
| content: 'Working...', |
| status: SubagentState.RUNNING, |
| }, |
| ], |
| }); |
| } |
|
|
| const priorState = RemoteAgentInvocation.sessionState.get( |
| this.definition.name, |
| ); |
| if (priorState) { |
| this.contextId = priorState.contextId; |
| this.taskId = priorState.taskId; |
| } |
|
|
| const authHandler = await this.getAuthHandler(); |
|
|
| if (!this.clientManager.getClient(this.definition.name)) { |
| await this.clientManager.loadAgent( |
| this.definition.name, |
| getAgentCardLoadOptions(this.definition), |
| authHandler, |
| ); |
| } |
|
|
| const message = this.params.query; |
|
|
| const stream = this.clientManager.sendMessageStream( |
| this.definition.name, |
| message, |
| { |
| contextId: this.contextId, |
| taskId: this.taskId, |
| signal: _signal, |
| }, |
| ); |
|
|
| let finalResponse: SendMessageResult | undefined; |
|
|
| for await (const chunk of stream) { |
| if (_signal.aborted) { |
| throw new Error('Operation aborted'); |
| } |
| finalResponse = chunk; |
| reassembler.update(chunk); |
|
|
| if (updateOutput) { |
| updateOutput({ |
| isSubagentProgress: true, |
| agentName, |
| state: SubagentState.RUNNING, |
| recentActivity: reassembler.toActivityItems(), |
| result: reassembler.toString(), |
| }); |
| } |
|
|
| const { |
| contextId: newContextId, |
| taskId: newTaskId, |
| clearTaskId, |
| } = extractIdsFromResponse(chunk); |
|
|
| if (newContextId) { |
| this.contextId = newContextId; |
| } |
|
|
| this.taskId = clearTaskId ? undefined : (newTaskId ?? this.taskId); |
| } |
|
|
| if (!finalResponse) { |
| throw new Error('No response from remote agent.'); |
| } |
|
|
| const finalOutput = reassembler.toString(); |
|
|
| debugLogger.debug( |
| `[RemoteAgent] Final response from ${this.definition.name}:\n${JSON.stringify(finalResponse, null, 2)}`, |
| ); |
|
|
| const finalProgress: SubagentProgress = { |
| isSubagentProgress: true, |
| agentName, |
| state: SubagentState.COMPLETED, |
| result: finalOutput, |
| recentActivity: reassembler.toActivityItems(), |
| }; |
|
|
| if (updateOutput) { |
| updateOutput(finalProgress); |
| } |
|
|
| return { |
| llmContent: [{ text: finalOutput }], |
| returnDisplay: finalProgress, |
| }; |
| } catch (error: unknown) { |
| const partialOutput = reassembler.toString(); |
| |
| const errorMessage = this.formatExecutionError(error); |
| const fullDisplay = partialOutput |
| ? `${partialOutput}\n\n${errorMessage}` |
| : errorMessage; |
|
|
| const errorProgress: SubagentProgress = { |
| isSubagentProgress: true, |
| agentName, |
| state: SubagentState.ERROR, |
| result: fullDisplay, |
| recentActivity: reassembler.toActivityItems(), |
| }; |
|
|
| if (updateOutput) { |
| updateOutput(errorProgress); |
| } |
|
|
| return { |
| llmContent: [{ text: fullDisplay }], |
| returnDisplay: errorProgress, |
| }; |
| } finally { |
| |
| RemoteAgentInvocation.sessionState.set(this.definition.name, { |
| contextId: this.contextId, |
| taskId: this.taskId, |
| }); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| private formatExecutionError(error: unknown): string { |
| |
| |
| |
| if (error instanceof A2AAgentError) { |
| return error.userMessage; |
| } |
|
|
| return `Error calling remote agent: ${ |
| error instanceof Error ? error.message : String(error) |
| }`; |
| } |
| } |
|
|