import { Disposable, IReference } from "@codingame/monaco-vscode-api/vscode/vs/base/common/lifecycle"; import { URI } from "@codingame/monaco-vscode-api/vscode/vs/base/common/uri"; import { ILogService } from "@codingame/monaco-vscode-api/vscode/vs/platform/log/common/log.service"; import { IConfigurationService } from "@codingame/monaco-vscode-api/vscode/vs/platform/configuration/common/configuration.service"; import { IAgentConnection, IAgentCreateChatOptions, IAgentCreateSessionConfig, IAgentResolveSessionConfigParams, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, AuthenticateParams, AuthenticateResult, IMcpNotification } from "../common/agentService.js"; import { type IRemoteWatchHandle } from "../common/agentHostFileSystemProvider.js"; import { type IActiveSubscriptionInfo, type IAgentSubscription } from "../common/state/agentSubscription.js"; import { IAgentHostResourceService } from "@codingame/monaco-vscode-api/vscode/vs/platform/agentHost/common/agentHostResourceService.service"; import type { CommandMap } from "@codingame/monaco-vscode-api/vscode/vs/platform/agentHost/common/state/protocol/messages"; import { type ActionEnvelope, type IRootConfigChangedAction, type SessionAction, type TerminalAction, type ClientAnnotationsAction } from "../common/state/sessionActions.js"; import { StateComponents, type RootState } from "@codingame/monaco-vscode-api/vscode/vs/platform/agentHost/common/state/sessionState"; import { type IStateSnapshot } from "@codingame/monaco-vscode-api/vscode/vs/platform/agentHost/common/state/sessionProtocol"; import { type IVscodeUpgradeResult } from "@codingame/monaco-vscode-api/vscode/vs/platform/agentHost/common/state/protocolUpgrade"; import { type IProtocolTransport } from "../common/state/sessionTransport.js"; import { type CompletionsParams, type CompletionsResult, type CreateTerminalParams, type ResolveSessionConfigResult, type SessionConfigCompletionsResult } from "@codingame/monaco-vscode-api/vscode/vs/platform/agentHost/common/state/protocol/commands"; import type { InvokeChangesetOperationParams, InvokeChangesetOperationResult } from "@codingame/monaco-vscode-api/vscode/vs/platform/agentHost/common/state/protocol/channels-changeset/commands"; import { ILoadEstimator } from "@codingame/monaco-vscode-api/vscode/vs/base/parts/ipc/common/ipc.net"; import type { OtlpExportLogsParams } from "@codingame/monaco-vscode-api/vscode/vs/platform/agentHost/common/state/protocol/channels-otlp/notifications"; import type { InitializeResult } from "@codingame/monaco-vscode-api/vscode/vs/platform/agentHost/common/state/protocol/common/commands"; /** * High-level connection state of a {@link RemoteAgentHostProtocolClient}. * Exposed via {@link RemoteAgentHostProtocolClient.onDidChangeConnectionState} * so consumers can surface transient reconnect activity in the UI. */ export declare enum AgentHostClientState { /** Initial handshake in progress. */ Connecting = "connecting", /** Transport is open and handshake/reconnect has completed. */ Connected = "connected", /** Transport closed unexpectedly; an automatic reconnect is in flight or scheduled. */ Reconnecting = "reconnecting", /** Client has been disposed or has given up reconnecting. Terminal state. */ Closed = "closed" } /** * A protocol-level client for a single remote agent host connection. * Manages the WebSocket transport, handshake, subscriptions, action dispatch, * and command/response correlation. * * Implements {@link IAgentConnection} so consumers can program against * a single interface regardless of whether the agent host is local or remote. */ export declare class RemoteAgentHostProtocolClient extends Disposable implements IAgentConnection { private readonly _logService; private readonly _resourceService; private readonly _configurationService; readonly _serviceBrand: undefined; private readonly _clientId; private readonly _address; private readonly _transportFactory; private _transport; /** Disposable holding the listeners attached to the current transport. */ private readonly _transportListeners; private readonly _connectionAuthority; private _serverSeq; private _nextClientSeq; private _defaultDirectory; /** * Latest `initialize` response from the host. Captured at the end of * {@link connect} and re-captured after a soft-reconnect that pulled * a fresh snapshot. `undefined` before the handshake completes. */ private _initializeResult; private readonly _subscriptionManager; private readonly _onDidAction; readonly onDidAction: import("@codingame/monaco-vscode-api/vscode/vs/base/common/event").Event; private readonly _onDidNotification; readonly onDidNotification: import("@codingame/monaco-vscode-api/vscode/vs/base/common/event").Event; private readonly _onMcpNotification; readonly onMcpNotification: import("@codingame/monaco-vscode-api/vscode/vs/base/common/event").Event; /** * Fires for every `otlp/exportLogs` notification the host sends on a * channel this client has subscribed to. Each payload is an * OTLP/JSON `ExportLogsServiceRequest` value verbatim; consumers * decode it (see `iterateOtlpLogRecords`) and route the records to a * registered logger or sink. * * Channel URIs are kept opaque on the wire so the same event covers * every {@link TelemetryCapabilities.logs} URI the host advertises — * subscribers should filter by `channel` if they care. */ private readonly _onDidReceiveOtlpLogs; readonly onDidReceiveOtlpLogs: import("@codingame/monaco-vscode-api/vscode/vs/base/common/event").Event; private readonly _onDidClose; readonly onDidClose: import("@codingame/monaco-vscode-api/vscode/vs/base/common/event").Event; private readonly _onDidChangeConnectionState; readonly onDidChangeConnectionState: import("@codingame/monaco-vscode-api/vscode/vs/base/common/event").Event; /** * Discriminated state union. Read via narrowing (`_state.kind === ...`); * reconnect-only fields like the gate/outbox/attempt counter are only * accessible while {@link _state.kind} is {@link AgentHostClientState.Reconnecting}, * and the close error is only accessible while it's {@link AgentHostClientState.Closed}. */ private _state; /** Pending JSON-RPC requests keyed by request id. */ private readonly _pendingRequests; private _nextRequestId; /** * Timestamp of the most recent message of any kind received from the * server. Used only for diagnostic logging when the close timer fires. */ private _lastReadTime; /** * Liveness watchdog — see {@link _resetLivenessTimers}. * * {@link _pingTimer} fires after {@link PING_INTERVAL_MS} of inbound * silence and sends an application-level `ping` so we have something * to time out on. {@link _closeTimer} fires after another * {@link LIVENESS_TIMEOUT_MS} of continued silence and force-closes * the transport so the renderer's reconnect logic kicks in. Both are * reset on every received message, so busy connections generate no * ping traffic at all. * * Detects silently-dead transports (e.g. SSH/tunnel after laptop * sleep + network change) that don't produce a socket close event of * their own. */ private readonly _pingTimer; private readonly _closeTimer; /** * Used to suppress watchdog-triggered closes when our own JS event loop * has been pegged — in that case the silence is on our side, not the * remote's, and tearing down the transport would just generate a useless * reconnect cycle that aborts in-flight requests. */ private readonly _loadEstimator; /** * Comparison keys of customization URIs we have already granted implicit * read access for on this connection. Dedupes repeat sends so we don't * pile up grants per dispatch. Cleared with the connection. */ private readonly _grantedCustomizationUris; get clientId(): string; get address(): string; get defaultDirectory(): string | undefined; get connectionState(): AgentHostClientState; /** * The latest `initialize` response from the host, or `undefined` if * the handshake has not completed yet. Callers read fields they care * about (e.g. `telemetry`, `completionTriggerCharacters`, * `defaultDirectory`) directly off this object — keeping the result * intact avoids adding a new getter every time the protocol grows. */ get initializeResult(): InitializeResult | undefined; constructor(address: string, transportOrFactory: IProtocolTransport | (() => IProtocolTransport), loadEstimator: ILoadEstimator | undefined, _logService: ILogService, _resourceService: IAgentHostResourceService, _configurationService: IConfigurationService); /** * Install a transport and wire listeners. Used both for the initial * transport and for replacements created by the factory during a * transport-level reconnect. */ private _installTransport; /** * Transition to a new {@link ClientState}. Fires {@link onDidChangeConnectionState} * only when the variant kind actually changes; in-place mutation of * reconnect-state fields (e.g. swapping the gate on a failed retry) does * NOT count as a transition and produces no event. */ private _transitionTo; private _newReconnectGate; private _newReconnectState; dispose(): void; /** * Connect to the remote agent host and perform the protocol handshake. */ connect(): Promise; /** * Externally signal that the transport has closed. Used by services * managing a passive transport (SSH / dev-tunnels) when they observe * a connection-loss IPC event independent of the transport's own * onClose — without this, a single dropped IPC delivery on the * transport's close channel leaves the client stranded in * `Connected` until its watchdog fires (which can take hours when * the renderer is backgrounded and `setTimeout` is throttled). * * Idempotent — no-op if already closed or mid-reconnect. */ notifyTransportClosed(): void; /** * Called from the transport's `onClose` event. When a {@link _transportFactory} * is configured we attempt to soft-reconnect rather than fire `onDidClose` — * the protocol-level `reconnect` request lets the server replay missed * actions and preserves the `clientId` so pending tool calls etc. are not * cancelled by the host-side disconnect timeout. Without a factory * (passive-transport SSH/relay path) we fall back to "close means closed" * and let the service decide whether to spin up a fresh client. */ private _handleTransportClose; private _scheduleReconnect; private _attemptReconnect; /** * Apply a `reconnect` RPC result to the subscription manager. On `replay` * we feed each missed envelope through the normal action path; on * `snapshot` we reseat each named subscription with the fresh state and * advance the server seq cursor accordingly. */ private _applyReconnectResult; /** * Drain queued outgoing wire traffic after a successful soft reconnect: * * 1. Resend pending optimistic session actions that the server did NOT * echo back in the replay buffer (i.e. anything still on * {@link AgentSubscriptionManager.getPendingSessionActions}). * 2. Flush every message that {@link _sendNotification} queued onto the * outbox while the gate was engaged. * * Replays are deduped against the outbox by `clientSeq` so a session * action that was both optimistic-tracked AND queued during the * reconnect window only goes out once. */ private _drainAfterReconnect; get rootState(): IAgentSubscription; getSubscription(kind: StateComponents, resource: URI, owner: string): IReference>; getSubscriptionUnmanaged(_kind: StateComponents, resource: URI): IAgentSubscription | undefined; getInflightSessionCreate(resource: URI): Promise | undefined; getActiveSubscriptions(): readonly IActiveSubscriptionInfo[]; dispatch(channel: string, action: SessionAction | TerminalAction | ClientAnnotationsAction | IRootConfigChangedAction): void; /** * Subscribe to state at a URI. Returns the current state snapshot. * * For stateless channels (e.g. `ahp-otlp:` telemetry channels) use * {@link subscribeStateless} — calling this method on a stateless * channel rejects because the server omits `snapshot` on the * response. */ subscribe(resource: URI): Promise; /** * Subscribe to a stateless channel — one for which the server does * not maintain replayable state and therefore omits `snapshot` from * the `subscribe` response. Used today for the host's OTLP telemetry * channels (`ahp-otlp:`). * * Returns once the subscription is confirmed by the server. * Subsequent notifications on the channel arrive via the relevant * dispatch event (e.g. {@link onDidReceiveOtlpLogs} for log records). */ subscribeStateless(resource: URI): Promise; /** * Unsubscribe from state at a URI. */ unsubscribe(resource: URI): void; /** * Dispatch a client action to the server. Returns the clientSeq used. */ private dispatchAction; /** * Create a new session on the remote agent host. */ createSession(config?: IAgentCreateSessionConfig): Promise; resolveSessionConfig(params: IAgentResolveSessionConfigParams): Promise; sessionConfigCompletions(params: IAgentSessionConfigCompletionsParams): Promise; completions(params: CompletionsParams): Promise; /** * Send an application-level ping and wait for the server's response. * Used by {@link _watchdogTick} to keep idle connections under * watchdog supervision; safe to call from external code as well. * * The returned promise rejects with a {@link ProtocolError} if the * connection closes before a response arrives. */ ping(): Promise; /** * Returns the trigger characters captured from the `initialize` handshake. * Empty when the remote host did not announce any. */ getCompletionTriggerCharacters(): Promise; /** * Authenticate with the remote agent host using a specific scheme. */ authenticate(params: AuthenticateParams): Promise; /** * Gracefully shut down all sessions on the remote host. */ shutdown(): Promise; /** * Dispose a session on the remote agent host. */ disposeSession(session: URI): Promise; createChat(session: URI, chat: URI, options?: IAgentCreateChatOptions): Promise; disposeChat(chat: URI): Promise; /** * Create a new terminal on the remote agent host. */ createTerminal(params: CreateTerminalParams): Promise; /** * Dispose a terminal on the remote agent host. */ disposeTerminal(terminal: URI): Promise; invokeChangesetOperation(params: InvokeChangesetOperationParams): Promise; /** * Send a request on an `mcp://` AHP side channel. The agent-host * routes by `params.channel` so we inject it automatically. */ handleMcpRequest(channel: string, method: string, params: Record | undefined): Promise; /** * List all sessions from the remote agent host. */ listSessions(): Promise; private _toLocalProjectUri; /** * Inspect an outgoing client-dispatched action and grant implicit reads * for any customization URIs it carries. Today this covers * `SessionActiveClientSet`, which is the only client-dispatched * action that ships customization URIs to the host. */ private _grantImplicitReadsForOutgoingAction; /** * Register implicit read grants for each customization URI that we are * about to send to the host. The host needs to read these to materialize * the customization, but should not need to write them. Grants are * deduped per connection and revoked when the connection closes. */ private _grantImplicitReadsForCustomizations; /** * List the contents of a directory on the remote host's filesystem. */ resourceList(uri: URI): Promise; /** * Read the content of a resource on the remote host. */ resourceRead(uri: URI): Promise; resourceWrite(params: CommandMap["resourceWrite"]["params"]): Promise; resourceCopy(params: CommandMap["resourceCopy"]["params"]): Promise; resourceDelete(params: CommandMap["resourceDelete"]["params"]): Promise; resourceMove(params: CommandMap["resourceMove"]["params"]): Promise; resourceResolve(params: CommandMap["resourceResolve"]["params"]): Promise; resourceMkdir(params: CommandMap["resourceMkdir"]["params"]): Promise; createResourceWatch(params: CommandMap["createResourceWatch"]["params"]): Promise; /** * Convenience wrapper used by {@link AHPFileSystemProvider.watch}: * runs `createResourceWatch` + `subscribe` and returns a handle that * surfaces `resourceWatch/changed` envelopes as * {@link IFileChange}[] events. Disposing the handle unsubscribes * the watch channel. */ watchResource(params: CommandMap["createResourceWatch"]["params"]): Promise; /** * Trigger the CLI-managed upgrade flow for this agent host using the * method name advertised by the server (typically * {@link VSCODE_UPGRADE_METHOD}). Callable before {@link connect} has * completed — typically used when the host has just rejected our * `initialize` with an `UnsupportedProtocolVersion` error. The * transport stays open after the rejection, so the extension request * rides over it without a special out-of-band path. * * The result mirrors the CLI's HTTP response: ok flag, whether the * upgrade is needed / started, running/latest commits. */ triggerVscodeUpgrade(method: string): Promise; private _handleMessage; private _handleClose; private _raceClose; /** * Handles reverse RPC requests from the server (e.g. resourceList, * resourceRead). Thin wire adapter — dispatches each frame to * {@link IAgentHostResourceService} (which owns gating, virtual reads, * and the user-prompt flow) and translates results / errors back into * JSON-RPC frames. */ private _handleReverseRequest; /** Send a typed JSON-RPC notification for a protocol-defined method. */ private _sendNotification; /** Send a typed JSON-RPC request for a protocol-defined method. */ private _sendRequest; /** Send a JSON-RPC request for a VS Code extension method (not in the protocol spec). */ private _sendExtensionRequest; private _updateTelemetryLevel; private _updateSessionSyncEnabled; private _updateTerminalAutoApproveEnabled; private _updateGlobalAutoApproveEnabled; private _updateAutoReplyEnabled; private _updateCodexEnabled; private _updateTerminalAutoApproveRules; /** * Common path for outgoing JSON-RPC requests: gate on any in-flight * reconnect (unless explicitly bypassed for the `reconnect` RPC itself), * assign an id, register the pending deferred, and write to the wire. * * The bypass option is the single special case that exists: the * `reconnect` request is sent from inside `_attemptReconnect` while the * gate is engaged, so it can't wait on its own resolution. */ private _dispatchRequest; private _shouldLogFailedRequest; private _toProtocolError; private _rejectPendingRequests; /** * Reset the liveness timers. Called once at construction (after the * initial transport is installed), once on every received message * (which is itself proof the remote is alive), and once after a * successful soft reconnect. * * Two timers cooperate: * * 1. {@link _pingTimer} fires after {@link PING_INTERVAL_MS} of silence * and sends an application-level `ping` so the close timer has * something to time out on. Tolerates servers that don't implement * `ping` — the error response still resets both timers. * * 2. {@link _closeTimer} fires after {@link PING_INTERVAL_MS}+ * {@link LIVENESS_TIMEOUT_MS} of continued silence and force-closes * the transport so the renderer's reconnect logic kicks in. Catches * silently-dead transports (e.g. SSH/tunnel after laptop sleep + * network change) that don't emit a socket close event of their own. * * After laptop sleep + wake the JS event loop is paused, so a timer * armed before sleep fires immediately after wake. That's fine — * any inbound message processed during the wake catch-up resets it * before the close handler runs. * * No-op while {@link _state.kind} is {@link AgentHostClientState.Reconnecting} * or {@link AgentHostClientState.Closed}: the reconnect machinery * owns scheduling in those states, and we don't want a stray inbound * message during reconnect to re-arm the timers behind its back. */ private _resetLivenessTimers; private _cancelLivenessTimers; private _onPingTimer; private _onCloseTimer; /** * Get the next client sequence number for optimistic dispatch. */ nextClientSeq(): number; }