Download packages/core/src/agents/remote-session-invocation.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 9.03 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/agents/remote-session-invocation.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/core/src/agents/remote-session-invocation.ts
-
curl -L -o remote-session-invocation.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/agents/remote-session-invocation.ts
9.03 kB
| /** | |
| * @license | |
| * Copyright 2026 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| 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'; | |
| /** Optional configuration for remote agent invocations. */ | |
| export interface SubagentInvocationOptions { | |
| toolName?: string; | |
| toolDisplayName?: string; | |
| onAgentEvent?: (event: AgentEvent) => void; | |
| } | |
| /** | |
| * Session-based remote agent invocation. | |
| * | |
| * This implementation delegates execution to {@link RemoteSubagentSession}, | |
| * which wraps the A2A client streaming behind the AgentProtocol interface. | |
| * | |
| * Cross-invocation A2A session state (contextId/taskId) is persisted via a | |
| * static map keyed by a composite of agent name and target URL. This ensures | |
| * agents with the same name but different endpoints maintain independent state. | |
| */ | |
| export class RemoteSessionInvocation extends BaseToolInvocation< | |
| RemoteAgentInputs, | |
| ToolResult | |
| > { | |
| // Persist A2A conversation state across ephemeral invocation instances. | |
| // Keyed by composite of name + target URL so agents with the same name | |
| // but different endpoints don't share state. | |
| private static readonly sessionState = new Map< | |
| string, | |
| { contextId?: string; taskId?: string } | |
| >(); | |
| /** | |
| * Builds a composite key for the sessionState map. | |
| * Format: `name::targetUrl` (or just `name` if no URL can be derived). | |
| */ | |
| 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.`, | |
| ); | |
| } | |
| // Safe to pass strict object to super | |
| super( | |
| { query }, | |
| messageBus, | |
| options?.toolName ?? definition.name, | |
| options?.toolDisplayName ?? definition.displayName, | |
| ); | |
| this._onAgentEvent = options?.onAgentEvent; | |
| // Validate that A2AClientManager is available at construction time | |
| 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) => { | |
| // Policy updates are now handled centrally by the scheduler | |
| }, | |
| }; | |
| } | |
| async execute(options: ExecuteOptions): Promise<ToolResult> { | |
| const { abortSignal: _signal, updateOutput } = options; | |
| const agentName = this.definition.displayName ?? this.definition.name; | |
| const emptyActivity: SubagentActivityItem[] = []; | |
| // Seed session with prior A2A conversation state | |
| const stateKey = RemoteSessionInvocation.sessionKey(this.definition); | |
| const priorState = RemoteSessionInvocation.sessionState.get(stateKey); | |
| const session = new RemoteSubagentSession( | |
| this.definition, | |
| this.context, | |
| this.messageBus, | |
| priorState, | |
| ); | |
| // Wire external abort signal to session abort | |
| const abortListener = () => void session.abort(); | |
| _signal?.addEventListener('abort', abortListener, { once: true }); | |
| // Subscribe for parent session observability | |
| 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: 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(); | |
| // The protocol resolves aborts with an empty result rather than | |
| // rejecting. Detect this and surface proper error state. | |
| 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, | |
| }; | |
| } | |
| // 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 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 { | |
| // Persist A2A state for next invocation — even on abort/error | |
| 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; | |
| } | |
| /** | |
| * 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 { | |
| if (error instanceof A2AAgentError) { | |
| return error.userMessage; | |
| } | |
| return `Error calling remote agent: ${ | |
| error instanceof Error ? error.message : String(error) | |
| }`; | |
| } | |
| } | |