protocolServerHandler.ts ×78

Frontier kind: Code frontier

unlabeled · c_555eac94906d

77 tests · 28284 LOC · 148 files · introduces 0 tests · 774 LOC · 5 files

Introduces — evidence that enters the hierarchy at this concept

Code
85 ranges774 lines · 5 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
2635 ranges28284 lines · 148 files · Browse complete extent
All tests (intent)
77 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

5 files ranked by introduced lines: 774 introduced LOC across 85 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/node/protocolServerHandler.ts 599 introduced LOC · 78 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- protocolServerHandler.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { disposableTimeout } from '../../../base/common/async.js';
7 > import { Emitter } from '../../../base/common/event.js';
8 > import { isJsonRpcResponse } from '../../../base/common/jsonRpcProtocol.js';
9 > import { Disposable, DisposableMap, DisposableStore } from '../../../base/common/lifecycle.js';
10 > import { hasKey } from '../../../base/common/types.js';
11 > import { URI } from '../../../base/common/uri.js';
12 > import { ILogService } from '../../log/common/log.js';
13 > import { AHPFileSystemProvider } from '../common/agentHostFileSystemProvider.js';
14 > import { AgentSession, type IAgentCreateChatOptions, type IAgentService, type IMcpNotification } from '../common/agentService.js';
15 > import { isActionEnvelopeRelevantToSubscriptionUris } from '../common/state/agentSubscription.js';
16 > import { ChatSourceKind } from '../common/state/protocol/channels-chat/commands.js';
17 > import type { CommandMap } from '../common/state/protocol/messages.js';
18 > import { ActionEnvelope, ActionType, INotification, isAnnotationsAction, isChangesetAction, isChatAction, isSessionAction, isTerminalAction, type ChatAction, type ClientAnnotationsAction, type ClientChangesetAction, type IRootConfigChangedAction, type SessionAction, type TerminalAction } from '../common/state/sessionActions.js';
19 > import { PROTOCOL_VERSION } from '../common/state/protocol/version/registry.js';
20 > import { negotiateProtocolVersion } from '../common/state/protocol/version/negotiation.js';
21 > import { VSCODE_UPGRADE_METHOD, type UnsupportedProtocolVersionErrorDataEx } from '../common/state/protocolUpgrade.js';
22 > import { getAgentHostManagementSocketPath, requestAgentHostUpgrade } from './agentHostUpgradeChannel.js';
23 > import {
24 > AHP_AUTH_REQUIRED,
25 > AhpErrorCodes,
26 > AHP_PROVIDER_NOT_FOUND,
27 > AHP_SESSION_NOT_FOUND,
28 > AHP_UNSUPPORTED_PROTOCOL_VERSION,
29 > JsonRpcRequest,
30 > isJsonRpcNotification,
31 > isJsonRpcRequest,
32 > JSON_RPC_INTERNAL_ERROR,
33 > JsonRpcErrorCodes,
34 > ProtocolError,
35 > type AhpServerNotification,
36 > type InitializeParams,
37 > type JsonRpcResponse,
38 > type ReconnectParams,
39 > type IStateSnapshot,
40 > type SubscribeResult,
41 > type ListSessionsResult,
42 > } from '../common/state/sessionProtocol.js';
43 > import { isAhpResourceWatchChannel, isAhpRootChannel, ResponsePartKind, SessionStatus, ToolCallConfirmationReason, ToolCallContributorKind, ToolCallStatus, ToolResultContentType, buildDefaultChatUri, isAhpChatChannel, parseChatUri, parseRequiredSessionUriFromChatUri, type ISessionWithDefaultChat, type SessionState } from '../common/state/sessionState.js';
44 > import type { IProtocolServer, IProtocolTransport } from '../common/state/sessionTransport.js';
45 > import { AgentHostStateManager } from './agentHostStateManager.js';
46 > import {
47 > buildOtlpLogsChannelUri,
48 > extractLevelFromOtlpLogsUri,
49 > levelToSeverityNumber,
50 > OTLP_CHANNEL_SCHEME,
51 > OTLP_LOGS_CHANNEL_TEMPLATE,
52 > OtlpLogEmitter,
53 > toResourceLogsPayload,
54 > type IOtlpLogRecord,
55 > type OtlpLogLevelName,
56 > } from '../common/otlp/otlpLogEmitter.js';
57 > import { isFileResourceRead } from '../common/resourceReadLogging.js';
58 >
59 > /** Default capacity of the server-side action replay buffer. */
60 > const REPLAY_BUFFER_CAPACITY = 1000;
61 >
62 > const CLIENT_TOOL_CALL_DISCONNECT_TIMEOUT = 30_000;
63 >
64 > /**
65 > * Client-dispatchable actions that are declared in the protocol but not yet
66 > * operational in this build. The multiroot working-directory mutations
67 > * (`session|chat/workingDirectorySet|Removed`) would mutate the synchronized
68 > * working-directory set without reconfiguring the agent's actual directory
69 > * access, so they are rejected in the dispatch path until capability-backed
70 > * multiroot support lands.
71 > */
72 > const UNSUPPORTED_CLIENT_ACTION_TYPES: ReadonlySet<ActionType> = new Set([
73 > ActionType.SessionWorkingDirectorySet,
74 > ActionType.SessionWorkingDirectoryRemoved,
75 > ActionType.ChatWorkingDirectorySet,
76 > ActionType.ChatWorkingDirectoryRemoved,
77 > ]);
78 >
79 > /** A client tool call in any of these statuses is still awaiting its result. */
80 function isPendingToolCallStatus(status: ToolCallStatus): boolean {
81 return status === ToolCallStatus.Streaming
83 || status === ToolCallStatus.PendingConfirmation;
84 }
86 > /** Build a JSON-RPC success response suitable for transport.send(). */
87 function jsonRpcSuccess(id: number, result: unknown): JsonRpcResponse {
88 return { jsonrpc: '2.0', id, result };
89 }
91 > /** Build a JSON-RPC error response suitable for transport.send(). */
92 function jsonRpcError(id: number, code: number, message: string, data?: unknown): JsonRpcResponse {
93 return { jsonrpc: '2.0', id, error: { code, message, ...(data !== undefined ? { data } : {}) } };
94 }
96 > /** Build a JSON-RPC error response from an unknown thrown value, preserving {@link ProtocolError} fields. */
97 function jsonRpcErrorFrom(id: number, err: unknown): JsonRpcResponse {
98 if (err instanceof ProtocolError) {
102 return jsonRpcError(id, JSON_RPC_INTERNAL_ERROR, message);
103 }
105 function shouldLogFailedRequest(method: string, params: unknown, err: unknown): boolean {
106 if (!(err instanceof ProtocolError) || err.code !== AhpErrorCodes.NotFound || !isFileResourceRead(method, params)) {
109 return false;
110 }
112 > /** True when `value` is a non-null params object (as opposed to an array or primitive). */
113 function isParamsObject(value: unknown): value is Record<string, unknown> {
114 return typeof value === 'object' && value !== null && !Array.isArray(value);
115 }
117 > /**
118 > * Returns the `channel` URI carried on a request's params when it is an
119 > * `mcp://` channel — the AHP routing envelope for raw MCP requests
120 > * tunnelled over the JSON-RPC connection. Returns `undefined` for any
121 > * other params shape.
122 > */
123 function readMcpChannel(params: unknown): string | undefined {
124 if (!isParamsObject(params)) {
131 return channel;
132 }
134 > /**
135 > * Methods handled by the request dispatcher. Excludes `initialize`,
136 > * `reconnect`, and `ping`, which are handled directly during message
137 > * dispatch without requiring an established client context.
138 > */
139 > type RequestMethod = Exclude<keyof CommandMap, 'initialize' | 'reconnect' | 'ping'>;
140 >
141 > /**
142 > * Typed handler map: each key is a request method, each value is a handler
143 > * that receives the correctly-typed params and must return the correctly-typed
144 > * result. The compiler will error if a handler returns the wrong shape.
145 > */
146 > type RequestHandlerMap = {
147 > [M in RequestMethod]: (client: IConnectedClient, params: CommandMap[M]['params']) => Promise<CommandMap[M]['result']>;
148 > };
149 >
150 > /**
151 > * Discriminant for {@link ChannelSubscription}. Distinguishes a regular
152 > * state-bearing channel (root, session, terminal, changeset) from the
153 > * stateless OTLP signal channels so each subscribe/unsubscribe path can
154 > * dispatch through a single typed lookup.
155 > */
156 > const enum ChannelKind {
157 > /**
158 > * Subscribed via {@link IAgentService.subscribe} and tracked by the
159 > * server-side refcount. Carries replayable state, participates in
160 > * action broadcasts ({@link _broadcastAction}) and reconnect
161 > * snapshot/replay.
162 > */
163 > State = 'state',
164 > /**
165 > * Resource-watch channels (`ahp-resource-watch:/<id>`). Tracked
166 > * separately so subscribe/unsubscribe routes through the agent
167 > * service's per-watch refcount + grace timer rather than the
168 > * session-shaped {@link IAgentService.subscribe} path.
169 > */
170 > ResourceWatch = 'resource-watch',
171 > /**
172 > * Subscribed against the OTLP logs channel template advertised in
173 > * {@link InitializeResult.telemetry}. Stateless — no snapshot, no
174 > * agent-service refcount. The `level` field records the minimum
175 > * severity the client asked to receive.
176 > */
177 > OtlpLogs = 'otlp-logs',
178 > }
179 >
180 > /**
181 > * Per-channel server-side subscription record. Stored on every
182 > * {@link IConnectedClient} so each subscribed channel can be routed by
183 > * its `kind` without re-deriving it from the URI on every dispatch.
184 > *
185 > * `uri` is the canonical channel URI string used everywhere a subscription
186 > * is referenced — the same string is broadcast on outbound notifications
187 > * and persists across reconnects.
188 > */
189 > type ChannelSubscription =
190 > | { readonly kind: ChannelKind.State; readonly uri: string }
191 > | { readonly kind: ChannelKind.ResourceWatch; readonly uri: string }
192 > | { readonly kind: ChannelKind.OtlpLogs; readonly uri: string; readonly level: OtlpLogLevelName };
193 >
194 > /**
195 > * Represents a connected protocol client with its subscription state.
196 > */
197 > interface IConnectedClient {
198 > readonly clientId: string;
199 > readonly protocolVersion: string;
200 > readonly transport: IProtocolTransport;
201 > /**
202 > * Every channel the client is currently subscribed to, keyed by the
203 > * canonical channel URI. OTLP channel URIs are canonicalised to
204 > * `buildOtlpLogsChannelUri(level)` so URI variants that resolve to
205 > * the same logical channel collapse to one entry.
206 > */
207 > readonly subscriptions: Map<string, ChannelSubscription>;
208 > readonly disposables: DisposableStore;
209 > }
210 >
211 > /**
212 > * Per-client server-side record, keyed by clientId in
213 > * {@link ProtocolServerHandler._clients}. Unlike {@link IConnectedClient},
214 > * the record OUTLIVES individual transports: multiple overlapping transports
215 > * for the same logical client are held oldest-first, with the active transport
216 > * at the end. When the last transport disconnects, the record is retained
217 > * (until pruned) so the tool-call disconnect-grace machinery can compute the
218 > * remaining window and hold any armed timeouts.
219 > *
220 > * A client is in exactly one of two states, which makes the core invariant
221 > * unrepresentable in the wrong shape: a client either has one or more live
222 > * transports ({@link IActiveClientRecord}, never any disconnect-grace timers)
223 > * or has no transport and is within its disconnect-grace window
224 > * ({@link IGraceClientRecord}, never any connections). Transitions happen only
225 > * in {@link ProtocolServerHandler._attachConnection} (→ active, which disposes
226 > * any grace timers) and the transport `onClose` handler (→ grace, once the last
227 > * transport is gone).
228 > */
229 > type IClientRecord = IActiveClientRecord | IGraceClientRecord;
230 >
231 > interface IActiveClientRecord {
232 > readonly state: 'active';
233 > /**
234 > * Live transports for this client, oldest first. The active connection is
235 > * the last entry (most recent wins). Older entries are kept so that if a
236 > * reconnecting client registers `A`, then `B`, then `B` closes first, we can
237 > * fall back to `A` instead of treating the client as disconnected. Never
238 > * empty: removing the last transport promotes the record to a grace record.
239 > */
240 > readonly connections: IConnectedClient[];
241 > }
242 >
243 > interface IGraceClientRecord {
244 > readonly state: 'grace';
245 > /**
246 > * Epoch ms when the client last had a live transport, or when this record
247 > * was created for a never-connected orphan tool-call stamp. Pins the grace
248 > * clock so re-arms triggered by later orphaned tool calls shrink the
249 > * remaining window instead of resetting it. Drives the disconnect-timeout
250 > * delay (residual window from this instant).
251 > */
252 > lastSeenAt: number;
253 > /**
254 > * Pending tool-call disconnect timeouts owned by this client, keyed by
255 > * session URI. Armed when the client owns a pending client tool call but is
256 > * not connected; fires a failing completion if it does not (re)connect
257 > * within the grace window. Reconnecting promotes the record to active and
258 > * disposes these timers (the grace window no longer applies once a transport
259 > * is live). Disposing an entry (or the whole map) clears the timer.
260 > */
261 > readonly disconnectTimeouts: DisposableMap<string>;
262 > }
263 >
264 > /**
265 > * Classifies a raw channel URI string into its {@link ChannelKind} and
266 > * returns the canonical URI to key subscriptions by. Returns `undefined`
267 > * when the channel is OTLP-flavoured but the URI does not parse into a
268 > * supported shape (unknown level, missing path) so the caller can
269 > * silently drop the subscribe rather than installing a broken entry.
270 > *
271 > * For state channels the canonical URI is just the input verbatim — the
272 > * agent service is the authoritative deduplication point and tolerates
273 > * whatever URI form the client sent.
274 > */
275 function classifyChannel(channel: string): ChannelSubscription | undefined {
276 if (channel.toLowerCase().startsWith(`${OTLP_CHANNEL_SCHEME}:`)) {
286 return { kind: ChannelKind.State, uri: channel };
287 }
289 > /**
290 > * Configuration for protocol-level concerns outside of IAgentService.
291 > */
292 > export interface IProtocolServerConfig {
293 > /** Default directory returned to clients during the initialize handshake. */
294 > readonly defaultDirectory?: string;
295 > /**
296 > * Whether to expose VS Code extension methods outside the Agent Host Protocol.
297 > * Defaults to `true` for existing remote listeners.
298 > */
299 > readonly allowExtensionMethods?: boolean;
300 > /**
301 > * Characters that, when typed in a {@link UserMessage} input, SHOULD
302 > * cause the client to issue a `completions` request. Announced to
303 > * clients in the `initialize` response.
304 > */
305 > readonly completionTriggerCharacters?: readonly string[];
306 > /**
307 > * Prefix that marks a user message as a host terminal command.
308 > */
309 > readonly terminalCommandPrefix?: string;
310 > /**
311 > * Optional emitter to use as the source for the OTLP logs channel
312 > * advertised via `InitializeResult.telemetry.logs`. When present, this
313 > * handler will route `subscribe`/`unsubscribe` requests on
314 > * `ahp-otlp:` channels to its internal OTLP subscription registry and
315 > * broadcast every record fed into the emitter as an
316 > * `otlp/exportLogs` notification. When absent, the OTLP channel is
317 > * not advertised and any inbound `ahp-otlp:` subscribe request is
318 > * rejected.
319 > */
320 > readonly otlpLogEmitter?: OtlpLogEmitter;
321 > }
322 >
323 > /**
324 > * Server-side handler that manages protocol connections, routes JSON-RPC
325 > * messages to the agent service, and broadcasts actions/notifications
326 > * to subscribed clients.
327 > */
328 > export class ProtocolServerHandler extends Disposable {
329 >
330 > /**
331 > * Per-client records keyed by clientId. Holds both connected clients
332 > * (`connections` non-empty) and recently-disconnected ones retained for the
333 > * tool-call disconnect-grace window (`connections.length === 0`). See
334 > * {@link IClientRecord}.
335 > */
336 > private readonly _clients = new Map<string, IClientRecord>();
337 > private readonly _replayBuffer: ActionEnvelope[] = [];
338 >
339 > private readonly _onDidChangeConnectionCount = this._register(new Emitter<number>());
340 >
341 > /** Fires with the current client count whenever a client connects or disconnects. */
342 > readonly onDidChangeConnectionCount = this._onDidChangeConnectionCount.event;
343 >
344 > constructor(
345 > private readonly _agentService: IAgentService,
346 > private readonly _stateManager: AgentHostStateManager,
347 > private readonly _server: IProtocolServer,
348 > private readonly _config: IProtocolServerConfig,
349 > private readonly _clientFileSystemProvider: AHPFileSystemProvider,
350 > @ILogService private readonly _logService: ILogService,
351 > ) {
352 > super();
353 >
354 > this._register(this._server.onConnection(transport => {
355 this._handleNewConnection(transport);
357 >
358 > this._register(this._stateManager.onDidEmitEnvelope(envelope => {
359 this._replayBuffer.push(envelope);
360 if (this._replayBuffer.length > REPLAY_BUFFER_CAPACITY) {
376 this._checkOrphanedClientToolCalls(parseRequiredSessionUriFromChatUri(envelope.channel), envelope.channel);
377 }
379 >
380 > this._register(this._stateManager.onDidEmitNotification(notification => {
381 this._broadcastNotification(notification);
383 >
384 > this._register(this._agentService.onMcpNotification(notification => {
385 this._broadcastMcpNotification(notification);
387 >
388 > if (this._config.otlpLogEmitter) {
389 this._register(this._config.otlpLogEmitter.onDidLog(record => this._broadcastOtlpLog(record)));
390 }
392 >
393 > // ---- Connection handling -------------------------------------------------
394 >
395 > private _handleNewConnection(transport: IProtocolTransport): void {
396 const disposables = new DisposableStore();
397 let client: IConnectedClient | undefined;
525 disposables.add(transport);
526 }
528 > // ---- Handshake handlers ----------------------------------------------------
529 >
530 > private _handleInitialize(
531 params: InitializeParams,
532 transport: IProtocolTransport,
591 };
592 }
594 > /**
595 > * Helper for `initialize` and `reconnect` initial-subscription
596 > * processing: classify `channel`, install the matching subscription
597 > * on the client, and return the snapshot to include in the handshake
598 > * response (or `undefined` for stateless channels and missing state).
599 > *
600 > * Side effects:
601 > * - State channels: register with the agent service and clear any
602 > * pending tool-call disconnect timeout.
603 > * - OTLP channels: install the canonical entry on the client's
604 > * {@link IConnectedClient.subscriptions} map.
605 > *
606 > * Channels with unsupported shapes (e.g. `ahp-otlp://logs/verbose`
607 > * with no recognised level, or a state channel the state manager
608 > * does not know about) are silently dropped.
609 > */
610 > private _addInitialSubscription(client: IConnectedClient, channel: string): IStateSnapshot | undefined {
611 const sub = classifyChannel(channel);
612 if (!sub) {
630 return snapshot;
631 }
633 > /**
634 > * Forwards a client's upgrade request to the hosting VS Code CLI's
635 > * HTTP management API (advertised via the {@link VSCODE_AGENT_HOST_MANAGEMENT_SOCKET_ENV}).
636 > * Returns the CLI's parsed response verbatim so the client can render
637 > * a meaningful status (already up-to-date, restart scheduled, etc.).
638 > *
639 > * When the server was not spawned by a managing CLI, responds with
640 > * `MethodNotFound` — the upgrade method is only meaningfully callable
641 > * on CLI-hosted servers.
642 > */
643 > private _handleVscodeUpgrade(id: number, transport: IProtocolTransport): void {
644 const socketPath = getAgentHostManagementSocketPath();
645 if (!socketPath) {
659 );
660 }
662 > private _handleReconnect(
663 params: ReconnectParams,
664 transport: IProtocolTransport,
692 return { client, responsePromise };
693 }
695 > /**
696 > * Wires the reverse-RPC filesystem callbacks for `clientId` and binds
697 > * the unregister to `disposables` (the transport's per-connection
698 > * store). The callbacks dispatch through {@link _sendReverseRequest},
699 > * which looks up the *current* connected client by id — so re-binding
700 > * after a reconnect picks up the new transport without rebuilding the
701 > * closures.
702 > */
703 > private _registerClientFileSystemAuthority(clientId: string, disposables: DisposableStore): void {
704 disposables.add(this._clientFileSystemProvider.registerAuthority(clientId, {
705 resourceList: (uri) => this._sendReverseRequest(clientId, 'resourceList', { uri: uri.toString() }),
714 }));
715 }
717 > /**
718 > * Re-establish each of the client's prior subscriptions on the server side.
719 > * Uses {@link IAgentService.subscribe} (rather than a bare `addSubscriber`
720 > * + `getSnapshot`) so any session state that was evicted while the client
721 > * was disconnected is restored. Returns the appropriate reconnect response
722 > * payload — `replay` actions when the client's last-seen seq is still in
723 > * the buffer, otherwise fresh `snapshot`s.
724 > */
725 > private async _restoreReconnectSubscriptions(
726 client: IConnectedClient,
727 params: ReconnectParams,
785 return { type: 'snapshot', snapshots: snapshots.filter((s): s is IStateSnapshot => s !== undefined) };
786 }
788 > /**
789 > * Release a client from every session where it is still an active client
790 > * but did not resubscribe during a reconnect. The set of resubscribed
791 > * sessions is gathered from every live connection the client currently
792 > * holds (not just the reconnecting one) so an overlapping connection that
793 > * still subscribes to a session keeps the client active there.
794 > */
795 > private _reconcileActiveClientsAfterReconnect(client: IConnectedClient): void {
796 const record = this._clients.get(client.clientId);
797 const resubscribed = new Set<string>();
814 }
815 }
817 > private _handleClientDisconnected(clientId: string): void {
818 for (const session of this._stateManager.getSessionUris()) {
819 const state = this._stateManager.getSessionState(session);
832 }
833 }
835 > /** Whether `clientId` is one of the session's active clients. */
836 > private _isActiveClient(state: SessionState, clientId: string): boolean {
837 return state.activeClients.some(c => c.clientId === clientId);
838 }
840 > /**
841 > * Remove `clientId` from a session's active clients, if present. Dispatched
842 > * as a server action so the removal is reflected in state and broadcast to
843 > * the remaining subscribers.
844 > */
845 > private _removeActiveClient(session: string, clientId: string): void {
846 const state = this._stateManager.getSessionState(session);
847 if (state && this._isActiveClient(state, clientId)) {
852 }
853 }
855 > /**
856 > * Release a client from a session: clear its pending disconnect timeout,
857 > * fail any client tool calls it still owns, and remove it from the active
858 > * clients. Used by the explicit-unsubscribe and reconnect-reconciliation
859 > * paths to drop a client that has left a session.
860 > */
861 > private _releaseActiveClientForSession(session: string, clientId: string, chatChannel: string): void {
862 this._clearClientToolCallDisconnectTimeout(clientId, chatChannel);
863 this._completeDisconnectedClientToolCalls(clientId, session, chatChannel);
864 this._removeActiveClient(session, clientId);
865 }
867 > /**
868 > * Yields every still-pending client-contributed tool call in `state`'s
869 > * active turn, paired with its owning `clientId`. Single source of truth
870 > * for the disconnect-grace machinery: detect ownership
871 > * ({@link _hasPendingClientToolCall}), arm timeouts
872 > * ({@link _checkOrphanedClientToolCalls}), and fail orphaned calls
873 > * ({@link _completeDisconnectedClientToolCalls}).
874 > */
875 > private *_pendingClientToolCalls(state: ISessionWithDefaultChat | undefined) {
876 const activeTurn = state?.activeTurn;
877 if (!activeTurn) {
889 }
890 }
892 > private _hasPendingClientToolCall(state: ISessionWithDefaultChat | undefined, clientId: string): boolean {
893 for (const pending of this._pendingClientToolCalls(state)) {
894 if (pending.clientId === clientId) {
898 return false;
899 }
901 > private _hasReplacementActiveClientTool(state: SessionState, clientId: string, toolName: string): boolean {
902 return state.activeClients.some(client =>
903 client.clientId !== clientId
904 && client.tools.some(tool => tool.name === toolName));
905 }
907 > /**
908 > * Arm (or re-arm) the per-(clientId, session) timeout that fails pending
909 > * client tool calls owned by `clientId` if it does not reconnect before the
910 > * grace window elapses. Only meaningful for a client with no live transport:
911 > * a connected client is handled by {@link _attachConnection}, which disposes
912 > * any armed timers, so this is a no-op when the client is active. The delay
913 > * is the remaining grace measured from when the client disconnected — so a
914 > * client that disconnected a while before the call was issued gets the
915 > * residual window rather than a fresh one, and a stamp from a long-disconnected
916 > * client fails promptly. Re-arms triggered by later orphaned tool calls in the
917 > * same session shrink the remaining window instead of resetting it.
918 > */
919 > private _startClientToolCallDisconnectTimeout(clientId: string, session: string, chatChannel: string): void {
920 const record = this._ensureGraceRecord(clientId);
921 if (!record) {
930 }, delay));
931 }
933 > /**
934 > * Scan a chat for pending client tool calls owned by a disconnected client
935 > * of this protocol server, and arm the disconnect timeout for each owner.
936 > * Called when a `ChatToolCallStart` / `ChatToolCallReady` envelope is
937 > * observed — covering calls issued for an already-gone client, which the
938 > * live disconnect path never sees. Ownerless client tool calls (no client
939 > * connected at stamp time) are failed immediately by the provider, so they
940 > * never reach a pending state here. Unknown client ids are ignored because
941 > * they may belong to another transport such as local IPC.
942 > */
943 > private _checkOrphanedClientToolCalls(session: string, chatChannel: string): void {
944 const state = this._stateManager.getSessionState(chatChannel);
945 const orphanOwners = new Set<string>();
954 }
955 }
957 > /**
958 > * Register a freshly connected (or reconnected) transport for `clientId`,
959 > * promoting the record to {@link IActiveClientRecord}. Promoting a grace
960 > * record back to active disposes its pending disconnect timers: the
961 > * disconnect-grace window only applies while the client has no live
962 > * transport. This is the single place that maintains the "active records
963 > * hold no grace timers" invariant.
964 > */
965 > private _attachConnection(clientId: string, client: IConnectedClient): void {
966 const existing = this._clients.get(clientId);
967 if (existing?.state === 'active') {
974 this._onDidChangeConnectionCount.fire(this._connectedClientCount);
975 }
977 > /**
978 > * Return the existing grace record for `clientId`, creating one for a
979 > * never-connected client (an orphan tool-call stamp). Returns `undefined`
980 > * when the client is currently active — the grace machinery does not apply
981 > * to a connected client. A newly created record pins its grace clock to now.
982 > */
983 > private _ensureGraceRecord(clientId: string): IGraceClientRecord | undefined {
984 const record = this._clients.get(clientId);
985 if (record?.state === 'active') {
993 return created;
994 }
996 > private _getActiveClient(clientId: string): IConnectedClient | undefined {
997 return this._getActiveClientFromRecord(this._clients.get(clientId));
998 }
1000 > private _getActiveClientFromRecord(record: IClientRecord | undefined): IConnectedClient | undefined {
1001 if (record?.state !== 'active') {
1002 return undefined;
1004 return record.connections[record.connections.length - 1];
1005 }
1007 > private _releaseClientSubscriptions(client: IConnectedClient, record: IActiveClientRecord): void {
1008 for (const sub of client.subscriptions.values()) {
1009 if (sub.kind === ChannelKind.State) {
1018 client.subscriptions.clear();
1019 }
1021 > private _hasSubscriptionInOtherConnection(record: IClientRecord, client: IConnectedClient, uri: string): boolean {
1022 if (record.state !== 'active') {
1023 return false;
1030 return false;
1031 }
1033 > /** Number of clients that currently have a live connection. */
1034 > private get _connectedClientCount(): number {
1035 let count = 0;
1036 for (const record of this._clients.values()) {
1041 return count;
1042 }
1044 > /**
1045 > * Drop grace records whose timers have all fired and whose last-seen time is
1046 > * stale beyond the retention window (10× the disconnect timeout). This
1047 > * covers both genuinely-disconnected clients and never-connected orphan
1048 > * stamps. Bounds {@link _clients} without tracking liveness precisely — a
1049 > * pruned-then-resurfacing stamp simply falls back to the full grace window.
1050 > * Active records are never pruned; they persist until their last transport
1051 > * closes.
1052 > */
1053 > private _pruneClientRecords(): void {
1054 const cutoff = Date.now() - CLIENT_TOOL_CALL_DISCONNECT_TIMEOUT * 10;
1055 for (const [clientId, record] of this._clients) {
1061 }
1062 }
1064 > private _clearClientToolCallDisconnectTimeout(clientId: string, channel: string): void {
1065 const record = this._clients.get(clientId);
1066 if (record?.state === 'grace') {
1068 }
1069 }
1071 > private _completeDisconnectedClientToolCalls(clientId: string, session: string, chatChannel: string): void {
1072 const state = this._stateManager.getSessionState(chatChannel);
1073 const activeTurn = state?.activeTurn;
1102 }
1103 }
1105 > // ---- Requests (expect a response) ---------------------------------------
1106 >
1107 > /**
1108 > * Methods handled by the request dispatcher (excludes initialize/reconnect
1109 > * which are handled during the handshake phase).
1110 > */
1111 > private readonly _requestHandlers: RequestHandlerMap = {
1112 > subscribe: async (client, params) => {
1113 const classified = classifyChannel(params.channel);
1114 if (!classified) {
1155 }
1156 },
1157 > createSession: async (_client, params) => { protocolServerHandler.ts
1158 let createdSession: URI;
1159 // Resolve fork turnId to a 0-based index using the source session's
1198 return null;
1199 },
1200 > disposeSession: async (_client, params) => { protocolServerHandler.ts
1201 await this._agentService.disposeSession(URI.parse(params.channel));
1202 return null;
1203 },
1204 > createChat: async (_client, params) => { protocolServerHandler.ts
1205 const state = this._stateManager.getSessionState(params.channel);
1206 if (!state) {
1240 return null;
1241 },
1242 > disposeChat: async (_client, params) => { protocolServerHandler.ts
1243 const chat = URI.parse(params.channel);
1244 const parsed = parseChatUri(chat);
1249 return null;
1250 },
1251 > resourceWrite: async (_client, params) => { protocolServerHandler.ts
1252 return this._agentService.resourceWrite(params);
1253 },
1254 > listSessions: async () => { protocolServerHandler.ts
1255 const sessions = await this._agentService.listSessions();
1256 const items = sessions.map(s => {
1282 return { items };
1283 },
1284 > resolveSessionConfig: async (_client, params) => { protocolServerHandler.ts
1285 return this._agentService.resolveSessionConfig({
1286 provider: params.provider,
1289 });
1290 },
1291 > sessionConfigCompletions: async (_client, params) => { protocolServerHandler.ts
1292 return this._agentService.sessionConfigCompletions({
1293 provider: params.provider,
1298 });
1299 },
1300 > completions: async (_client, params) => { protocolServerHandler.ts
1301 return this._agentService.completions(params);
1302 },
1303 > fetchTurns: async (_client, params) => { protocolServerHandler.ts
1304 const state = this._stateManager.getChatState(params.channel);
1305 if (!state) {
1315 return {};
1316 },
1317 > resourceList: async (_client, params) => { protocolServerHandler.ts
1318 return this._agentService.resourceList(URI.parse(params.uri));
1319 },
1320 > resourceRead: async (_client, params) => { protocolServerHandler.ts
1321 return this._agentService.resourceRead(URI.parse(params.uri));
1322 },
1323 > resourceCopy: async (_client, params) => { protocolServerHandler.ts
1324 return this._agentService.resourceCopy(params);
1325 },
1326 > resourceDelete: async (_client, params) => { protocolServerHandler.ts
1327 return this._agentService.resourceDelete(params);
1328 },
1329 > resourceMove: async (_client, params) => { protocolServerHandler.ts
1330 return this._agentService.resourceMove(params);
1331 },
1332 > resourceResolve: async (_client, params) => { protocolServerHandler.ts
1333 return this._agentService.resourceResolve(params);
1334 },
1335 > resourceMkdir: async (_client, params) => { protocolServerHandler.ts
1336 return this._agentService.resourceMkdir(params);
1337 },
1338 > createResourceWatch: async (_client, params) => { protocolServerHandler.ts
1339 return this._agentService.createResourceWatch(params);
1340 },
1341 > resourceRequest: async (_client, _params) => { protocolServerHandler.ts
1342 // The local agent host does not yet enforce per-resource grants
1343 // for client → server access. Always grant; receivers MAY rescind
1345 return {};
1346 },
1347 > authenticate: async (_client, params) => { protocolServerHandler.ts
1348 const result = await this._agentService.authenticate(params);
1349 if (!result.authenticated) {
1352 return {};
1353 },
1354 > createTerminal: async (_client, params) => { protocolServerHandler.ts
1355 await this._agentService.createTerminal(params);
1356 return null;
1357 },
1358 > disposeTerminal: async (_client, params) => { protocolServerHandler.ts
1359 await this._agentService.disposeTerminal(URI.parse(params.channel));
1360 return null;
1361 },
1362 > invokeChangesetOperation: async (_client, params) => { protocolServerHandler.ts
1363 return this._agentService.invokeChangesetOperation(params);
1364 },
1366 >
1367 >
1368 > // ---- Reverse RPC (server → client requests) ----------------------------
1369 >
1370 > private _reverseRequestId = 0;
1371 > private readonly _pendingReverseRequests = new Map<number, { client: IConnectedClient; resolve: (value: unknown) => void; reject: (reason: unknown) => void }>();
1372 >
1373 > /**
1374 > * Sends a JSON-RPC request to a connected client and waits for the response.
1375 > * Used for reverse-RPC operations like reading client-side files.
1376 > * Rejects if the client disconnects or the server is disposed.
1377 > */
1378 > private _sendReverseRequest<T>(clientId: string, method: string, params: unknown): Promise<T> {
1379 const client = this._getActiveClient(clientId);
1380 if (!client) {
1388 });
1389 }
1391 > /**
1392 > * Rejects and clears all pending reverse-RPC requests sent over a given
1393 > * connection.
1394 > */
1395 > private _rejectPendingReverseRequestsForConnection(client: IConnectedClient): void {
1396 for (const [id, pending] of this._pendingReverseRequests) {
1397 if (pending.client === client) {
1401 }
1402 }
1404 > private _handleRequest(client: IConnectedClient, method: string, params: unknown, id: number): void {
1405 const handler = this._requestHandlers.hasOwnProperty(method) ? this._requestHandlers[method as RequestMethod] : undefined;
1406 if (handler) {
1452 client.transport.send(jsonRpcError(id, JsonRpcErrorCodes.MethodNotFound, `Method not found: ${method}`));
1453 }
1455 > /**
1456 > * Handle VS Code extension methods that are not yet part of the typed
1457 > * protocol. Returns a Promise if the method was recognized, undefined
1458 > * otherwise.
1459 > */
1460 > private _handleExtensionRequest(method: string, params: unknown): Promise<unknown> | undefined {
1461 if (this._config.allowExtensionMethods === false) {
1462 return undefined;
1476 }
1477 }
1479 > // ---- Broadcasting -------------------------------------------------------
1480 >
1481 > private _broadcastAction(envelope: ActionEnvelope): void {
1482 this._logService.trace(`[ProtocolServer] Broadcasting action: ${envelope.action.type}`);
1483 const msg: AhpServerNotification<'action'> = { jsonrpc: '2.0', method: 'action', params: envelope };
1489 }
1490 }
1492 > private _broadcastNotification(notification: INotification): void {
1493 // Each protocol notification now ships as its own top-level method. The
1494 // `type` discriminant on our local {@link ProtocolNotification} union is
1501 }
1502 }
1504 > /**
1505 > * Forward an MCP server-originated notification (e.g.
1506 > * `notifications/tools/list_changed`) over the AHP transport. The
1507 > * `channel` field on `params` is the AHP routing envelope; the
1508 > * receiving client demultiplexes by it. Notifications are broadcast
1509 > * to every connected client — per-channel subscription filtering is
1510 > * left to the client, since MCP notifications are cheap and the
1511 > * client already knows which channels it cares about.
1512 > */
1513 > private _broadcastMcpNotification(notification: IMcpNotification): void {
1514 const params: Record<string, unknown> = { ...(notification.params ?? {}), channel: notification.channel };
1515 // MCP notifications don't share a discriminated `method` literal
1522 }
1523 }
1525 > /**
1526 > * Drop a subscription identified by `channel` from `client`. Handles
1527 > * canonicalisation for OTLP URIs (so an `unsubscribe` with a URI
1528 > * variant collapses to the same entry as the original `subscribe`)
1529 > * and tears down the agent-service refcount for state channels.
1530 > */
1531 > private _removeSubscription(client: IConnectedClient, channel: string): void {
1532 const classified = classifyChannel(channel);
1533 if (!classified) {
1559 }
1560 }
1562 > /**
1563 > * Fan out an OTLP log record to every connected client that has
1564 > * subscribed to a logs channel whose `{level}` band includes the
1565 > * record's `severityNumber`. The notification's `channel` field is
1566 > * the canonical URI the client subscribed against — clients can
1567 > * route by URI without re-deriving the level.
1568 > */
1569 > private _broadcastOtlpLog(record: IOtlpLogRecord): void {
1570 const payload = toResourceLogsPayload(record);
1571 for (const clientRecord of this._clients.values()) {
1590 }
1591 }
1593 > private _isRelevantToClient(client: IConnectedClient, envelope: ActionEnvelope): boolean {
1594 const sub = client.subscriptions.get(envelope.channel);
1595 if (sub?.kind === ChannelKind.State || sub?.kind === ChannelKind.ResourceWatch) {
1601 return isActionEnvelopeRelevantToSubscriptionUris(envelope, this._stateAndResourceWatchUris(client));
1602 }
1604 > private *_stateAndResourceWatchUris(client: IConnectedClient): Iterable<string> {
1605 for (const sub of client.subscriptions.values()) {
1606 if (sub.kind === ChannelKind.State || sub.kind === ChannelKind.ResourceWatch) {
1609 }
1610 }
1612 > override dispose(): void {
1613 > for (const record of this._clients.values()) {
1614 if (record.state === 'active') {
1615 for (const connection of [...record.connections]) {
1620 }
1621 }
1622 > this._clients.clear(); protocolServerHandler.ts
1623 > for (const [, pending] of this._pendingReverseRequests) {
1624 pending.reject(new Error('ProtocolServerHandler disposed'));
1625 }
1626 > this._pendingReverseRequests.clear(); protocolServerHandler.ts
1627 > this._replayBuffer.length = 0;
1628 > super.dispose();
1629 > }
1630 > }
src/vs/platform/agentHost/common/state/protocolUpgrade.ts 94 introduced LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- protocolUpgrade.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import type { UnsupportedProtocolVersionErrorData } from './protocol/errors.js';
7 > import type { Mutable } from '../../../../base/common/types.js';
8 >
9 > /**
10 > * Name of the JSON-RPC method that, when invoked on an agent host spawned
11 > * by the VS Code CLI, asks the CLI to check for a server upgrade and
12 > * restart the running server if a newer build is available.
13 > *
14 > * Servers advertise this method through {@link UnsupportedProtocolVersionErrorMeta.vscodeUpgradeMethod}
15 > * in the `_meta` payload of an `UnsupportedProtocolVersion` error so the
16 > * client can offer an "Update server" action without hard-coding the method
17 > * name on the renderer side.
18 > */
19 > export const VSCODE_UPGRADE_METHOD = '_vscodeUpgrade' as const;
20 >
21 > /**
22 > * Status payload returned by the {@link VSCODE_UPGRADE_METHOD} RPC. The
23 > * agent host server forwards the CLI's `POST /upgrade` response back to
24 > * the client unchanged, so the UI can describe what happened (e.g.
25 > * "already on the latest version" vs "upgrade scheduled, please reconnect").
26 > */
27 > export interface IVscodeUpgradeResult {
28 > /** Whether the CLI accepted the request without an internal error. */
29 > readonly ok: boolean;
30 > /** Whether the running server is older than the latest known release. */
31 > readonly upgradeNeeded?: boolean;
32 > /**
33 > * Whether the CLI committed to actually performing the upgrade. When
34 > * `true`, the client SHOULD reconnect after at least
35 > * {@link restartDelayMs} milliseconds; the existing transport will be
36 > * torn down by the CLI.
37 > */
38 > readonly upgradeStarted?: boolean;
39 > /** Commit of the currently running server, or `null` if no server is running. */
40 > readonly runningCommit?: string | null;
41 > /** Latest known release commit at the time of the call. */
42 > readonly latestCommit?: string;
43 > /**
44 > * Milliseconds the client should wait before attempting to reconnect.
45 > * The CLI deliberately delays the kill+restart so this HTTP response
46 > * can drain back through the proxy first; reconnecting immediately
47 > * would land on the still-running pre-upgrade server. Populated only
48 > * when {@link upgradeStarted} is `true`.
49 > */
50 > readonly restartDelayMs?: number;
51 > /** Human-readable error when {@link ok} is `false`. */
52 > readonly error?: string;
53 > }
54 >
55 > /**
56 > * Optional `_meta` side-channel on the `UnsupportedProtocolVersion` error
57 > * data payload. Servers that are spawned by the VS Code CLI populate this
58 > * to let the renderer offer a one-click upgrade flow; servers without a
59 > * managing CLI omit it.
60 > *
61 > * The MCP `_meta` convention is followed: this is an open-ended record so
62 > * future versions can carry additional fields without bumping the wire
63 > * shape of the error.
64 > */
65 > export interface UnsupportedProtocolVersionErrorMeta {
66 > /**
67 > * JSON-RPC method name the client MAY invoke on the same transport to
68 > * ask the server to upgrade itself. Currently always {@link VSCODE_UPGRADE_METHOD}.
69 > */
70 > readonly vscodeUpgradeMethod?: string;
71 > }
72 >
73 > /**
74 > * `UnsupportedProtocolVersionErrorData` augmented with the local `_meta`
75 > * extension used by VS Code-hosted agent hosts.
76 > *
77 > * Kept out of the auto-generated `errors.ts` so the synced types stay
78 > * untouched. Read at runtime as a best-effort field — older servers
79 > * simply won't set it.
80 > */
81 > export interface UnsupportedProtocolVersionErrorDataEx extends UnsupportedProtocolVersionErrorData {
82 > readonly _meta?: UnsupportedProtocolVersionErrorMeta;
83 > }
84 >
85 > /**
86 > * Reads the well-known {@link UnsupportedProtocolVersionErrorMeta} from the open
87 > * `_meta` bag on an `UnsupportedProtocolVersion` error's data payload. `data` is
88 > * the raw, untrusted `ProtocolError.data` (typed `unknown`); this validates that
89 > * it carries a `_meta` object with a string {@link
90 > * UnsupportedProtocolVersionErrorMeta.vscodeUpgradeMethod} and returns
91 > * `undefined` otherwise. Always read the upgrade `_meta` through this helper
92 > * rather than casting the error data to a shape that includes `_meta`.
93 > */
94 > export function readUnsupportedProtocolVersionErrorMeta(data: unknown): UnsupportedProtocolVersionErrorMeta | undefined {
95 if (!data || typeof data !== 'object') {
96 return undefined;
src/vs/platform/agentHost/node/agentHostUpgradeChannel.ts 49 introduced LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentHostUpgradeChannel.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > /**
7 > * Environment variable populated by the VS Code CLI when it spawns the
8 > * agent host server. Its value is the path of a unix-domain socket
9 > * (POSIX) or named pipe (Windows) on which the CLI is serving its HTTP
10 > * management API. Presence of the variable also serves as the "I was
11 > * spawned by a managing CLI" marker that decides whether the server
12 > * advertises an in-band upgrade method to clients.
13 > */
14 > export const VSCODE_AGENT_HOST_MANAGEMENT_SOCKET_ENV = 'VSCODE_AGENT_HOST_MANAGEMENT_SOCKET';
15 >
16 > /**
17 > * Status payload returned by the CLI's `POST /upgrade` endpoint. Sent
18 > * back verbatim to the agent host client so the UI can surface it.
19 > */
20 > export interface IUpgradeRequestResponse {
21 > readonly ok: boolean;
22 > /** Whether the running server is older than the latest known release. */
23 > readonly upgradeNeeded?: boolean;
24 > /** Whether the CLI committed to performing the upgrade (true => kill+respawn was scheduled). */
25 > readonly upgradeStarted?: boolean;
26 > /** Commit hash of the currently running server, or `null` if none. */
27 > readonly runningCommit?: string | null;
28 > /** Commit hash of the latest known release at the time of the call. */
29 > readonly latestCommit?: string;
30 > /** Milliseconds the client should wait before reconnecting (only set when upgrade started). */
31 > readonly restartDelayMs?: number;
32 > /** Human-readable error message when `ok` is false. */
33 > readonly error?: string;
34 > }
35 >
36 > /**
37 > * Returns the management socket path advertised by the hosting CLI, or
38 > * `undefined` when the current process was not spawned by one.
39 > */
40 > export function getAgentHostManagementSocketPath(): string | undefined {
41 const value = process.env[VSCODE_AGENT_HOST_MANAGEMENT_SOCKET_ENV];
42 return value && value.length > 0 ? value : undefined;
43 }
45 > /**
46 > * Ask the hosting CLI to check for an update and (if needed) restart this
47 > * server. Sends `POST /upgrade` to the management socket and returns the
48 > * CLI's parsed JSON response.
49 > *
50 > * Rejects when no management socket is advertised, when the connection
51 > * fails, on non-2xx responses, or when the response body cannot be parsed.
52 > */
53 export async function requestAgentHostUpgrade(socketPath = getAgentHostManagementSocketPath()): Promise<IUpgradeRequestResponse> {
54 const http = await import('http');
src/vs/platform/agentHost/node/compositeProtocolServer.ts 21 introduced LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- compositeProtocolServer.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { Emitter } from '../../../base/common/event.js';
7 > import { Disposable } from '../../../base/common/lifecycle.js';
8 > import type { IProtocolServer, IProtocolTransport } from '../common/state/sessionTransport.js';
9 >
10 > /**
11 > * Presents multiple protocol listeners as one server.
12 > */
13 > export class CompositeProtocolServer extends Disposable implements IProtocolServer {
14 >
15 > private readonly _onConnection = this._register(new Emitter<IProtocolTransport>());
16 > readonly onConnection = this._onConnection.event;
17 >
18 > readonly address = undefined;
19 >
20 > constructor(servers: readonly IProtocolServer[]) {
21 super();
22
src/vs/platform/agentHost/common/resourceReadLogging.ts 11 introduced LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- resourceReadLogging.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { Schemas } from '../../../base/common/network.js';
7 > import { hasKey } from '../../../base/common/types.js';
8 > import { URI } from '../../../base/common/uri.js';
9 >
10 > export function isFileResourceRead(method: string, params: unknown): boolean {
11 if (method !== 'resourceRead' || !hasUriParam(params)) {
12 return false;
22 }
23 }
25 function hasUriParam(params: unknown): params is { readonly uri: unknown } {
26 return typeof params === 'object' && params !== null && hasKey(params, { uri: true });