Download packages/core/src/agents/remote-subagent-protocol.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 14.1 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/agents/remote-subagent-protocol.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/core/src/agents/remote-subagent-protocol.ts
-
curl -L -o remote-subagent-protocol.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/agents/remote-subagent-protocol.ts
14.1 kB
| /** | |
| * @license | |
| * Copyright 2026 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| /** | |
| * @fileoverview RemoteSubagentProtocol — wraps A2A remote agent streaming | |
| * behind the AgentProtocol interface. | |
| * | |
| * Pattern mirrors LocalSubagentProtocol and LegacyAgentProtocol, but the loop | |
| * body drives A2AClientManager instead of LocalAgentExecutor. | |
| */ | |
| import { randomUUID } from 'node:crypto'; | |
| import type { AgentLoopContext } from '../config/agent-loop-context.js'; | |
| import { AgentSession } from '../agent/agent-session.js'; | |
| import type { | |
| AgentProtocol, | |
| AgentSend, | |
| AgentEvent, | |
| StreamEndReason, | |
| Unsubscribe, | |
| ContentPart, | |
| } from '../agent/types.js'; | |
| import type { ToolResult } from '../tools/tools.js'; | |
| import { | |
| DEFAULT_QUERY_STRING, | |
| type RemoteAgentDefinition, | |
| type SubagentProgress, | |
| SubagentState, | |
| getRemoteAgentTargetUrl, | |
| getAgentCardLoadOptions, | |
| } from './types.js'; | |
| import { A2AResultReassembler, extractIdsFromResponse } from './a2aUtils.js'; | |
| import type { AuthenticationHandler } from '@a2a-js/sdk/client'; | |
| import { A2AAuthProviderFactory } from './auth-provider/factory.js'; | |
| import { A2AAgentError } from './a2a-errors.js'; | |
| import { debugLogger } from '../utils/debugLogger.js'; | |
| import type { MessageBus } from '../confirmation-bus/message-bus.js'; | |
| // --------------------------------------------------------------------------- | |
| // Helpers | |
| // --------------------------------------------------------------------------- | |
| function isAbortLikeError(err: unknown): boolean { | |
| return err instanceof Error && err.name === 'AbortError'; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // RemoteSubagentProtocol | |
| // --------------------------------------------------------------------------- | |
| class RemoteSubagentProtocol implements AgentProtocol { | |
| private _events: AgentEvent[] = []; | |
| private _subscribers = new Set<(event: AgentEvent) => void>(); | |
| private _streamId: string = randomUUID(); | |
| private _eventCounter = 0; | |
| private _agentStartEmitted = false; | |
| private _agentEndEmitted = false; | |
| private _activeStreamId: string | undefined; | |
| private _abortController = new AbortController(); | |
| // A2A conversation state — persists across sends within this session instance | |
| private contextId: string | undefined; | |
| private taskId: string | undefined; | |
| private authHandler: AuthenticationHandler | undefined; | |
| // Agent display name (for SubagentProgress construction) | |
| private readonly _agentName: string; | |
| // Latest SubagentProgress — updated per chunk, used for error recovery | |
| private _latestProgress: SubagentProgress | undefined; | |
| // Result promise wiring — re-created per stream in _beginNewStream() | |
| private _resultResolve!: (result: ToolResult) => void; | |
| private _resultReject!: (err: unknown) => void; | |
| private _resultPromise: Promise<ToolResult> | undefined; | |
| constructor( | |
| private readonly definition: RemoteAgentDefinition, | |
| private readonly context: AgentLoopContext, | |
| // Required for API parity across protocol constructors (local, remote, legacy) | |
| _messageBus: MessageBus, | |
| initialState?: { contextId?: string; taskId?: string }, | |
| ) { | |
| this._agentName = definition.displayName ?? definition.name; | |
| if (initialState) { | |
| this.contextId = initialState.contextId; | |
| this.taskId = initialState.taskId; | |
| } | |
| } | |
| /** | |
| * Returns the current A2A conversation state. | |
| * Used by the invocation layer to persist state across invocations. | |
| */ | |
| getSessionState(): { contextId?: string; taskId?: string } { | |
| return { contextId: this.contextId, taskId: this.taskId }; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // AgentProtocol interface | |
| // --------------------------------------------------------------------------- | |
| get events(): readonly AgentEvent[] { | |
| return this._events; | |
| } | |
| subscribe(callback: (event: AgentEvent) => void): Unsubscribe { | |
| this._subscribers.add(callback); | |
| return () => { | |
| this._subscribers.delete(callback); | |
| }; | |
| } | |
| async send(payload: AgentSend): Promise<{ streamId: string | null }> { | |
| if ('message' in payload && payload.message) { | |
| if (this._activeStreamId) { | |
| throw new Error( | |
| 'RemoteSubagentProtocol.send() cannot be called while a stream is active.', | |
| ); | |
| } | |
| const query = | |
| payload.message.content | |
| .filter((p): p is ContentPart & { type: 'text' } => p.type === 'text') | |
| .map((p) => p.text) | |
| .join('') || DEFAULT_QUERY_STRING; | |
| this._beginNewStream(); | |
| const streamId = this._streamId; | |
| setTimeout(() => { | |
| void this._runStreamInBackground(query); | |
| }, 0); | |
| return { streamId }; | |
| } | |
| // update/action/elicitations not used for remote agents | |
| return { streamId: null }; | |
| } | |
| async abort(): Promise<void> { | |
| this._abortController.abort(); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Protocol-specific: result access | |
| // --------------------------------------------------------------------------- | |
| getResult(): Promise<ToolResult> { | |
| if (!this._resultPromise) { | |
| return Promise.reject(new Error('No active or completed stream')); | |
| } | |
| return this._resultPromise; | |
| } | |
| getLatestProgress(): SubagentProgress | undefined { | |
| return this._latestProgress; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Core: A2A streaming | |
| // --------------------------------------------------------------------------- | |
| private _beginNewStream(): void { | |
| this._streamId = randomUUID(); | |
| this._eventCounter = 0; | |
| this._abortController = new AbortController(); | |
| this._agentStartEmitted = false; | |
| this._agentEndEmitted = false; | |
| this._activeStreamId = this._streamId; | |
| this._resultPromise = new Promise<ToolResult>((resolve, reject) => { | |
| this._resultResolve = resolve; | |
| this._resultReject = reject; | |
| }); | |
| } | |
| private async _runStreamInBackground(query: string): Promise<void> { | |
| this._ensureAgentStart(); | |
| try { | |
| await this._runStream(query); | |
| } catch (err: unknown) { | |
| if (this._abortController.signal.aborted || isAbortLikeError(err)) { | |
| this._ensureAgentEnd('aborted'); | |
| // Abort resolves with an empty result — partial output is intentionally | |
| // dropped since the caller requested cancellation. | |
| this._resultResolve({ | |
| llmContent: [{ text: '' }], | |
| returnDisplay: '', | |
| }); | |
| } else { | |
| this._emitErrorAndAgentEnd(err); | |
| this._resultReject(err); | |
| } | |
| } finally { | |
| this._clearActiveStream(); | |
| } | |
| } | |
| private async _runStream(query: string): Promise<void> { | |
| const clientManager = this.context.config.getA2AClientManager(); | |
| if (!clientManager) { | |
| throw new Error( | |
| `RemoteSubagentProtocol: A2AClientManager not available for '${this.definition.name}'.`, | |
| ); | |
| } | |
| const authHandler = await this._getAuthHandler(); | |
| if (!clientManager.getClient(this.definition.name)) { | |
| await clientManager.loadAgent( | |
| this.definition.name, | |
| getAgentCardLoadOptions(this.definition), | |
| authHandler, | |
| ); | |
| } | |
| const reassembler = new A2AResultReassembler(); | |
| let prevText = ''; | |
| const stream = clientManager.sendMessageStream( | |
| this.definition.name, | |
| query, | |
| { | |
| contextId: this.contextId, | |
| taskId: this.taskId, | |
| signal: this._abortController.signal, | |
| }, | |
| ); | |
| for await (const chunk of stream) { | |
| reassembler.update(chunk); | |
| const { | |
| contextId: newContextId, | |
| taskId: newTaskId, | |
| clearTaskId, | |
| } = extractIdsFromResponse(chunk); | |
| if (newContextId) this.contextId = newContextId; | |
| this.taskId = clearTaskId ? undefined : (newTaskId ?? this.taskId); | |
| const currentText = reassembler.toString(); | |
| // Update latest progress snapshot (for invocation's error recovery) | |
| this._latestProgress = { | |
| isSubagentProgress: true, | |
| agentName: this._agentName, | |
| state: SubagentState.RUNNING, | |
| recentActivity: reassembler.toActivityItems(), | |
| result: currentText, | |
| }; | |
| // Emit delta as a message event | |
| const delta = currentText.slice(prevText.length); | |
| if (delta) { | |
| this._emit([ | |
| this._makeEvent('message', { | |
| role: 'agent', | |
| content: [{ type: 'text', text: delta }], | |
| }), | |
| ]); | |
| prevText = currentText; | |
| } | |
| } | |
| const finalOutput = reassembler.toString(); | |
| debugLogger.debug( | |
| `[RemoteSubagentProtocol] ${this.definition.name} finished, output length: ${finalOutput.length}`, | |
| ); | |
| const finalProgress: SubagentProgress = { | |
| isSubagentProgress: true, | |
| agentName: this._agentName, | |
| state: SubagentState.COMPLETED, | |
| result: finalOutput, | |
| recentActivity: reassembler.toActivityItems(), | |
| }; | |
| this._latestProgress = finalProgress; | |
| this._finishStream('completed'); | |
| this._resultResolve({ | |
| llmContent: [{ text: finalOutput }], | |
| returnDisplay: finalProgress, | |
| }); | |
| } | |
| private async _getAuthHandler(): Promise<AuthenticationHandler | undefined> { | |
| if (this.authHandler) return this.authHandler; | |
| if (!this.definition.auth) return undefined; | |
| 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; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Internal helpers | |
| // --------------------------------------------------------------------------- | |
| private _emit(events: AgentEvent[]): void { | |
| if (events.length === 0) return; | |
| const subscribers = [...this._subscribers]; | |
| for (const event of events) { | |
| this._events.push(event); | |
| if (event.type === 'agent_end') { | |
| this._agentEndEmitted = true; | |
| } | |
| for (const sub of subscribers) { | |
| sub(event); | |
| } | |
| } | |
| } | |
| private _clearActiveStream(): void { | |
| this._activeStreamId = undefined; | |
| } | |
| private _ensureAgentStart(): void { | |
| if (!this._agentStartEmitted) { | |
| this._agentStartEmitted = true; | |
| this._emit([this._makeEvent('agent_start', {})]); | |
| } | |
| } | |
| private _ensureAgentEnd(reason: StreamEndReason = 'completed'): void { | |
| if (!this._agentEndEmitted && this._agentStartEmitted) { | |
| this._emit([this._makeEvent('agent_end', { reason })]); | |
| } | |
| } | |
| private _finishStream(reason: StreamEndReason): void { | |
| this._ensureAgentEnd(reason); | |
| this._clearActiveStream(); | |
| } | |
| private _emitErrorAndAgentEnd(err: unknown): void { | |
| const message = this._formatError(err); | |
| this._ensureAgentStart(); | |
| const meta: Record<string, unknown> = {}; | |
| if (err instanceof Error) { | |
| meta['errorName'] = err.constructor.name; | |
| meta['stack'] = err.stack; | |
| } | |
| this._emit([ | |
| this._makeEvent('error', { | |
| status: 'INTERNAL', | |
| message, | |
| fatal: true, | |
| ...(Object.keys(meta).length > 0 ? { _meta: meta } : {}), | |
| }), | |
| ]); | |
| this._ensureAgentEnd('failed'); | |
| } | |
| private _formatError(error: unknown): string { | |
| if (error instanceof A2AAgentError) { | |
| return error.userMessage; | |
| } | |
| return `Error calling remote agent: ${error instanceof Error ? error.message : String(error)}`; | |
| } | |
| private _nextEventFields() { | |
| return { | |
| id: `${this._streamId}-${this._eventCounter++}`, | |
| timestamp: new Date().toISOString(), | |
| streamId: this._streamId, | |
| }; | |
| } | |
| private _makeEvent<T extends AgentEvent['type']>( | |
| type: T, | |
| payload: Omit<AgentEvent<T>, 'id' | 'timestamp' | 'streamId' | 'type'>, | |
| ): AgentEvent { | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| return { | |
| ...this._nextEventFields(), | |
| type, | |
| ...payload, | |
| } as AgentEvent; | |
| } | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Public export | |
| // --------------------------------------------------------------------------- | |
| export class RemoteSubagentSession extends AgentSession { | |
| private readonly _remoteProtocol: RemoteSubagentProtocol; | |
| constructor( | |
| definition: RemoteAgentDefinition, | |
| context: AgentLoopContext, | |
| messageBus: MessageBus, | |
| initialState?: { contextId?: string; taskId?: string }, | |
| ) { | |
| const protocol = new RemoteSubagentProtocol( | |
| definition, | |
| context, | |
| messageBus, | |
| initialState, | |
| ); | |
| super(protocol); | |
| this._remoteProtocol = protocol; | |
| } | |
| /** | |
| * Returns the ToolResult once the remote agent stream completes. | |
| * Used by RemoteAgentInvocation to return the result. | |
| */ | |
| getResult(): Promise<ToolResult> { | |
| return this._remoteProtocol.getResult(); | |
| } | |
| /** | |
| * Returns the most recent SubagentProgress snapshot, updated per streaming | |
| * chunk. Useful for constructing error progress when getResult() rejects. | |
| */ | |
| getLatestProgress(): SubagentProgress | undefined { | |
| return this._remoteProtocol.getLatestProgress(); | |
| } | |
| /** | |
| * Returns the current A2A conversation state (contextId/taskId). | |
| * Used by the invocation layer to persist state across invocations. | |
| */ | |
| getSessionState(): { contextId?: string; taskId?: string } { | |
| return this._remoteProtocol.getSessionState(); | |
| } | |
| /** | |
| * Convenience: start execution with a query string. | |
| * Equivalent to send({message: {content: [{type:'text', text: query}]}}). | |
| */ | |
| async startWithQuery(query: string): Promise<{ streamId: string | null }> { | |
| return this.send({ | |
| message: { content: [{ type: 'text', text: query }] }, | |
| }); | |
| } | |
| } | |