| |
| |
| |
| |
| |
|
|
| 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, |
| type SubagentActivityItem, |
| SubagentState, |
| getRemoteAgentTargetUrl, |
| } 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'; |
|
|
| |
| export interface SubagentInvocationOptions { |
| toolName?: string; |
| toolDisplayName?: string; |
| onAgentEvent?: (event: AgentEvent) => void; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export class RemoteSessionInvocation extends BaseToolInvocation< |
| RemoteAgentInputs, |
| ToolResult |
| > { |
| |
| |
| |
| private static readonly sessionState = new Map< |
| string, |
| { contextId?: string; taskId?: string } |
| >(); |
|
|
| |
| |
| |
| |
| private static sessionKey(definition: RemoteAgentDefinition): string { |
| const url = getRemoteAgentTargetUrl(definition); |
| return url ? `${definition.name}::${url}` : definition.name; |
| } |
|
|
| private readonly _onAgentEvent?: (event: AgentEvent) => void; |
|
|
| constructor( |
| private readonly definition: RemoteAgentDefinition, |
| private readonly context: AgentLoopContext, |
| params: AgentInputs, |
| messageBus: MessageBus, |
| options?: SubagentInvocationOptions, |
| ) { |
| 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, |
| options?.toolName ?? definition.name, |
| options?.toolDisplayName ?? definition.displayName, |
| ); |
| this._onAgentEvent = options?.onAgentEvent; |
|
|
| |
| if (!this.context.config.getA2AClientManager()) { |
| throw new Error( |
| `Failed to initialize RemoteSessionInvocation 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> { |
| 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 agentName = this.definition.displayName ?? this.definition.name; |
| const emptyActivity: SubagentActivityItem[] = []; |
|
|
| |
| const stateKey = RemoteSessionInvocation.sessionKey(this.definition); |
| const priorState = RemoteSessionInvocation.sessionState.get(stateKey); |
| const session = new RemoteSubagentSession( |
| this.definition, |
| this.context, |
| this.messageBus, |
| priorState, |
| ); |
|
|
| |
| const abortListener = () => void session.abort(); |
| _signal?.addEventListener('abort', abortListener, { once: true }); |
|
|
| |
| let unsubscribeParent: (() => void) | undefined; |
| if (this._onAgentEvent) { |
| unsubscribeParent = session.subscribe(this._onAgentEvent); |
| } |
|
|
| |
| 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: SubagentState.RUNNING, |
| recentActivity: [ |
| { |
| id: 'pending', |
| type: 'thought', |
| content: 'Working...', |
| status: SubagentState.RUNNING, |
| }, |
| ], |
| }); |
| } |
|
|
| await session.send({ |
| message: { content: [{ type: 'text', text: this.params.query }] }, |
| }); |
|
|
| const result = await session.getResult(); |
|
|
| |
| |
| if (_signal?.aborted) { |
| const partialProgress = session.getLatestProgress(); |
| const recentActivity = this.stopRunningActivities( |
| partialProgress?.recentActivity ?? emptyActivity, |
| SubagentState.CANCELLED, |
| ); |
| const errorProgress: SubagentProgress = { |
| isSubagentProgress: true, |
| agentName, |
| state: SubagentState.CANCELLED, |
| result: |
| typeof partialProgress?.result === 'string' |
| ? partialProgress.result |
| : '', |
| recentActivity, |
| }; |
| if (updateOutput) updateOutput(errorProgress); |
| return { |
| llmContent: [{ text: 'Operation cancelled by user' }], |
| returnDisplay: errorProgress, |
| }; |
| } |
|
|
| |
| 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 isAbort = |
| (error instanceof Error && error.name === 'AbortError') || |
| errorMessage.includes('Aborted'); |
|
|
| const status = isAbort ? SubagentState.CANCELLED : SubagentState.ERROR; |
| const recentActivity = this.stopRunningActivities( |
| partialProgress?.recentActivity ?? emptyActivity, |
| status, |
| ); |
|
|
| const errorProgress: SubagentProgress = { |
| isSubagentProgress: true, |
| agentName, |
| state: status, |
| result: fullDisplay, |
| recentActivity, |
| }; |
|
|
| if (updateOutput) { |
| updateOutput(errorProgress); |
| } |
|
|
| return { |
| llmContent: [{ text: fullDisplay }], |
| returnDisplay: errorProgress, |
| }; |
| } finally { |
| |
| RemoteSessionInvocation.sessionState.set( |
| stateKey, |
| session.getSessionState(), |
| ); |
| _signal?.removeEventListener('abort', abortListener); |
| unsubscribeProgress(); |
| unsubscribeParent?.(); |
| } |
| } |
|
|
| private stopRunningActivities( |
| activity: SubagentActivityItem[], |
| status: SubagentState, |
| ): SubagentActivityItem[] { |
| const result: SubagentActivityItem[] = []; |
| for (const item of activity) { |
| result.push( |
| item.status === SubagentState.RUNNING ? { ...item, status } : item, |
| ); |
| } |
| return result; |
| } |
|
|
| |
| |
| |
| |
| |
| private formatExecutionError(error: unknown): string { |
| if (error instanceof A2AAgentError) { |
| return error.userMessage; |
| } |
|
|
| return `Error calling remote agent: ${ |
| error instanceof Error ? error.message : String(error) |
| }`; |
| } |
| } |
|
|