copilotAgentSession.ts ×141

Frontier kind: Code frontier

unlabeled · c_40e9aba087ae

409 tests · 42083 LOC · 240 files · introduces 0 tests · 1263 LOC · 3 files

Introduces — evidence that enters the hierarchy at this concept

Code
152 ranges1263 lines · 3 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
3538 ranges42083 lines · 240 files · Browse complete extent
All tests (intent)
409 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.

3 files ranked by introduced lines: 1263 introduced LOC across 152 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts 1191 introduced LOC · 141 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- copilotAgentSession.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 { CopilotSession, CurrentToolMetadata, ExitPlanModeRequest, McpServersLoadedServer, MessageOptions, PermissionAllowAllMode, PermissionAutoApproval, PermissionRequestResult, SessionConfig, Tool, ToolResultObject, McpServerStatus as SdkMcpServerStatus } from '@github/copilot-sdk';
7 > import { DeferredPromise, Sequencer } from '../../../../base/common/async.js';
8 > import { encodeBase64, VSBuffer } from '../../../../base/common/buffer.js';
9 > import { Emitter } from '../../../../base/common/event.js';
10 > import { CancellationError, getErrorMessage } from '../../../../base/common/errors.js';
11 > import { escapeMarkdownSyntaxTokens } from '../../../../base/common/htmlContent.js';
12 > import { Disposable, IReference, toDisposable } from '../../../../base/common/lifecycle.js';
13 > import { Schemas } from '../../../../base/common/network.js';
14 > import { isAuthorizationProtectedResourceMetadata } from '../../../../base/common/oauth.js';
15 > import { safeStringify } from '../../../../base/common/objects.js';
16 > import { isAbsolute, join } from '../../../../base/common/path.js';
17 > import { extUriBiasedIgnorePathCase, normalizePath } from '../../../../base/common/resources.js';
18 > import { StopWatch } from '../../../../base/common/stopwatch.js';
19 > import { splitLinesIncludeSeparators } from '../../../../base/common/strings.js';
20 > import { hasKey, isDefined, isObject, isString, type Mutable } from '../../../../base/common/types.js';
21 > import { URI } from '../../../../base/common/uri.js';
22 > import { generateUuid } from '../../../../base/common/uuid.js';
23 > import { localize } from '../../../../nls.js';
24 > import { INativeEnvironmentService } from '../../../environment/common/environment.js';
25 > import { IFileService } from '../../../files/common/files.js';
26 > import { IInstantiationService } from '../../../instantiation/common/instantiation.js';
27 > import { ILogService, LogLevel } from '../../../log/common/log.js';
28 > import { ITelemetryService } from '../../../telemetry/common/telemetry.js';
29 > import { CopilotCliConfigKey, applyModelFamilyAlias, copilotCliConfigSchema } from '../../common/copilotCliConfig.js';
30 > import type { ChatInputRequestWithPlanReview, IAgentHostPlanReviewAction } from '../../common/agentHostPlanReview.js';
31 > import { gitHubMcpServerUrl } from '../../common/githubEndpoints.js';
32 > import { AgentHostSandboxConfigKey, sandboxConfigSchema } from '../../common/sandboxConfigSchema.js';
33 > import { AgentHostGlobalAutoApproveEnabledConfigKey, AgentHostAutoReplyEnabledConfigKey, AgentHostDisableRepoInfoTelemetryConfigKey, platformRootSchema, platformSessionSchema } from '../../common/agentHostSchema.js';
34 > import { AgentSession, AgentSignal, AuthenticateParams, IMcpNotification, IRestoredSubagentSession, subagentChatTitle } from '../../common/agentService.js';
35 > import { META_DIFF_BASE_BRANCH } from '../../common/agentHostGitService.js';
36 > import { stripRedundantCdPrefix } from '../../common/commandLineHelpers.js';
37 > import { readToolCallMeta, toToolCallMeta, type IToolCallMeta, type IToolCallUiMeta, type IToolSearchCandidate } from '../../common/meta/agentToolCallMeta.js';
38 > import { OtelData, type OtelAttributeValue } from '../../common/otlp/otlpLogEmitter.js';
39 > import { SessionConfigKey } from '../../common/sessionConfigKeys.js';
40 > import { resolveCopilotConfigSlashCommandOnSend } from '../../common/copilotConfigSlashCommands.js';
41 > import { isAgentFeedbackAnnotationsAttachment, renderAgentFeedbackAnnotationsAttachment } from '../../common/meta/agentFeedbackAttachments.js';
42 > import { ISessionDatabase, ISessionDataService, SESSION_ATTACHMENTS_DIRNAME } from '../../common/sessionDataService.js';
43 > import { MessageAttachmentKind, ToolCallContributorKind, type FileEdit, type MessageAttachment } from '../../common/state/protocol/state.js';
44 > import { ActionType, isChatAction, type ChatAction, type SessionAction } from '../../common/state/sessionActions.js';
45 > import { MessageKind, ResponsePartKind, ChatInputAnswerState, ChatInputAnswerValueKind, ChatInputQuestionKind, ChatInputResponseKind, ToolCallConfirmationReason, ToolCallRiskAssessmentKind, ToolCallRiskAssessmentStatus, ToolCallStatus, ToolResultContentType, buildSubagentSessionUri, getToolSubagentContent, isDefaultChatUri, isSubagentSession, withSessionPromptCacheState, type PendingMessage, type ChatInputAnswer, type ChatInputOption, type ChatInputQuestion, type ChatInputRequest, type ToolCallResult, type ToolResultContent, type ToolResultTerminalContent, type Turn, type UsageInfo, type UsageInfoMeta } from '../../common/state/sessionState.js';
46 > import { IAgentConfigurationService } from '../agentConfigurationService.js';
47 > import type { IExitPlanModeResponse } from './copilotAgent.js';
48 > import { CopilotSessionWrapper } from './copilotSessionWrapper.js';
49 > import { clientToolNamesFromSnapshot, type CopilotSessionLaunchPlan, type IActiveClientSnapshot, type ICopilotSessionLauncher, type ICopilotSessionRuntime } from './copilotSessionLauncher.js';
50 > import { agentHostModelSupportsToolSearch, CLIENT_TOOL_SEARCH_REFERENCE_NAME, NON_DEFERRED_CLIENT_TOOL_NAMES, RUNTIME_TOOL_SEARCH_TOOL_NAME } from './toolSearchDeferral.js';
51 > import { ActiveClientToolSet } from '../activeClientState.js';
52 > import { AgentHostTelemetryReporter } from '../agentHostTelemetryReporter.js';
53 > import { AgentHostRepoInfoTelemetry } from '../agentHostRepoInfoTelemetry.js';
54 > import { PendingRequestRegistry } from '../../common/pendingRequestRegistry.js';
55 > import { buildCopilotSystemNotification } from './copilotSystemNotification.js';
56 > import { parseLeadingSlashCommand } from '../../common/agentHostSlashCommand.js';
57 > import type { IUnsandboxedCommandConfirmationRequest, ShellManager } from './copilotShellTools.js';
58 > import { NonPtyShellTerminalStreams } from './copilotNonPtyShellTerminals.js';
59 > import { buildSandboxConfigForSdk, type ISdkSandboxConfig } from './sandboxConfigForSdk.js';
60 > import type { IAgentServerToolHost } from '../../common/agentServerTools.js';
61 > import { getEditFilePaths, getInvocationMessage, getPastTenseMessage, getPermissionDisplay, getShellIntention, getShellLanguage, getSubagentMetadata, getTaskCompleteMarkdown, getToolDisplayName, getToolInputString, getToolKind, isAgentCoordinationTool, isEditTool, isHiddenTool, isShellTool, isTaskCompleteTool, synthesizeSkillToolCall, tryStringify, type ITypedPermissionRequest } from './copilotToolDisplay.js';
62 > import { FileEditTracker } from '../shared/fileEditTracker.js';
63 > import { ICopilotApiService, type IRestrictedTelemetryContext } from '../shared/copilotApiService.js';
64 > import type { IAgentHostRestrictedTelemetryContext } from '../agentHostRestrictedTelemetry.js';
65 > import { stripProxyErrorMarker, tryBuildChatErrorMeta, tryBuildChatErrorMetaFromFields } from '../shared/forwardedChatError.js';
66 > import { getEffectiveMcpServerCustomizations, McpCustomizationController, type ISdkMcpServer } from '../shared/mcpCustomizationController.js';
67 > import { appendSdkToolResultContent, mapSessionEvents } from './mapSessionEvents.js';
68 > import { addSimpleAttachmentDisplayKindToMimeType } from './copilotAttachmentUtils.js';
69 > import { buildPendingEditContentUri } from './pendingEditContentStore.js';
70 > import { AgentHostStateManager, IAgentHostStateManager } from '../agentHostStateManager.js';
71 > import { McpAuthRequiredReason, McpServerStatus, type McpAuthRequirement, type McpServerState } from '../../common/state/protocol/channels-session/state.js';
72 > import type { ProtectedResourceMetadata } from '../../common/state/protocol/common/state.js';
73 > import { CopilotSlashCommandProvider } from './copilotSlashCommandProvider.js';
74 >
75 > /**
76 > * The full set of agent modes the Copilot SDK accepts. AHP now exposes the
77 > * same three modes (`interactive` / `plan` / `autopilot`) on its `mode` axis,
78 > * so the Copilot agent maps between the two views directly in
79 > * {@link CopilotAgentSession.send} and the `session.mode_changed` listener.
80 > */
81 > export type CopilotSdkMode = 'interactive' | 'plan' | 'autopilot';
82 > type CopilotSdkAttachment = Required<MessageOptions>['attachments'][number];
83 > type CopilotCommandInvocationResult = Awaited<ReturnType<CopilotSession['rpc']['commands']['invoke']>>;
84 > type RuntimeSlashCommandInfo = Awaited<ReturnType<CopilotSession['rpc']['commands']['list']>>['commands'][number];
85 > type McpAuthHandler = NonNullable<SessionConfig['onMcpAuthRequest']>;
86 > type McpAuthRequest = Parameters<McpAuthHandler>[0];
87 > type McpAuthResult = Awaited<ReturnType<McpAuthHandler>>;
88 >
89 > interface IPendingMcpAuthRequest {
90 > readonly serverName: string;
91 > readonly resource: ProtectedResourceMetadata;
92 > readonly requiredScopes: readonly string[];
93 > readonly toolCalls: IMcpAuthToolCall[];
94 > readonly deferred: DeferredPromise<McpAuthResult | null | undefined>;
95 > }
96 >
97 > interface IMcpAuthToolCall {
98 > readonly turnId: string;
99 > readonly toolCallId: string;
100 > readonly parentToolCallId: string | undefined;
101 > }
102 >
103 > const COPILOT_HOME_DIRECTORY = '.copilot';
104 > const SESSION_STATE_DIRECTORY = join(COPILOT_HOME_DIRECTORY, 'session-state');
105 > const EMPTY_TOOL_RESULT_TEXT = '<empty />';
106 >
107 function normalizeMcpServerUrl(value: string): string | undefined {
108 if (!URL.canParse(value)) {
114 return url.href;
115 }
117 > type IMappedSessionEvents = { turns: Turn[]; subagentTurnsByToolCallId: ReadonlyMap<string, Turn[]> };
118 >
119 function getEmptyToolResultText(binaryResults: readonly { readonly type: 'image' | 'resource' }[] | undefined): string {
120 if (!binaryResults?.length) {
132 return 'Tool produced the attached file';
133 }
135 > /**
136 > * Display labels and descriptions for the SDK's `exit_plan_mode` action ids.
137 > * Keys not present here fall back to the raw action id.
138 > */
139 function getPlanActionDescription(actionId: string): { label: string; description: string } | undefined {
140 switch (actionId) {
163 }
164 }
166 > type UserInputHandler = NonNullable<SessionConfig['onUserInputRequest']>;
167 > type UserInputRequest = Parameters<UserInputHandler>[0];
168 > type UserInputResponse = Awaited<ReturnType<UserInputHandler>>;
169 > type ElicitationHandler = NonNullable<SessionConfig['onElicitationRequest']>;
170 > type ElicitationContext = Parameters<ElicitationHandler>[0];
171 > type ElicitationResult = Awaited<ReturnType<ElicitationHandler>>;
172 > type ElicitationSchema = NonNullable<ElicitationContext['requestedSchema']>;
173 > type ElicitationSchemaField = ElicitationSchema['properties'][string];
174 > type ElicitationFieldValue = NonNullable<ElicitationResult['content']>[string];
175 > type SessionHooks = NonNullable<SessionConfig['hooks']>;
176 > type PreToolUseHookInput = Parameters<NonNullable<SessionHooks['onPreToolUse']>>[0];
177 > type PostToolUseHookInput = Parameters<NonNullable<SessionHooks['onPostToolUse']>>[0];
178 > type ToolUseHookInput = PreToolUseHookInput | PostToolUseHookInput;
179 >
180 function getToolCommand(input: ToolUseHookInput): string | undefined {
181 const command = isObject(input.toolArgs) ? Reflect.get(input.toolArgs, 'command') : undefined;
182 return isString(command) ? command : undefined;
183 }
185 function toCopilotSdkMode(mode: string | undefined): CopilotSdkMode | undefined {
186 mode = mode?.toLowerCase() === 'goal' ? 'plan' : mode;
194 }
195 }
197 > /**
198 > * Projects an {@link ElicitationSchema} field into a
199 > * {@link ChatInputQuestion}. The schema's property key becomes the
200 > * question id so we can route the answer back by field name.
201 > */
202 function elicitationFieldToQuestion(fieldName: string, field: ElicitationSchemaField, required: boolean): ChatInputQuestion {
203 const base = {
253 }
254 }
256 > /**
257 > * Projects a {@link ChatInputAnswer} back into the
258 > * {@link ElicitationFieldValue} shape expected by the SDK for the given
259 > * schema field. Returns `undefined` when the answer is missing/skipped or
260 > * cannot be coerced to the field's declared type.
261 > */
262 function elicitationAnswerToFieldValue(field: ElicitationSchemaField, answer: ChatInputAnswer | undefined): ElicitationFieldValue | undefined {
263 if (!answer || answer.state === ChatInputAnswerState.Skipped) {
302 return undefined;
303 }
305 function getCopilotCLISessionStateDir(userHome: string): string {
306 const xdgHome = process.env['XDG_STATE_HOME'];
307 return xdgHome ? join(xdgHome, SESSION_STATE_DIRECTORY) : join(userHome, SESSION_STATE_DIRECTORY);
308 }
310 > /**
311 > * Matches the temp file names the Copilot SDK uses when spilling large tool
312 > * results to disk. The SDK writes these into `os.tmpdir()` and references the
313 > * path back to the model so it can read the output in a follow-up turn.
314 > *
315 > * Two layouts are emitted by the SDK depending on the codepath:
316 > * - `<timestamp>-copilot-tool-output-<6-char-id>.txt` (large tool result)
317 > * - `copilot-tool-output-<timestamp>-<6-char-id>.txt` (streaming output buffer)
318 > *
319 > * Both live directly inside `os.tmpdir()`, so we additionally require the
320 > * file's parent directory to be the OS temp directory before auto-approving.
321 > */
322 > const COPILOT_SDK_TOOL_OUTPUT_BASENAME_RE = /^(?:\d{10,}-copilot-tool-output-[a-z0-9]{6}|copilot-tool-output-\d{10,}-[a-z0-9]{6})\.txt$/i;
323 >
324 function isCopilotSdkToolOutputTempFile(filePath: string, tmpDir: string): boolean {
325 const fileUri = normalizePath(URI.file(filePath));
333 return COPILOT_SDK_TOOL_OUTPUT_BASENAME_RE.test(basename);
334 }
336 > /**
337 > * Options for constructing a {@link CopilotAgentSession}.
338 > */
339 > export interface ICopilotAgentSessionOptions {
340 > readonly sessionUri: URI;
341 > readonly chatChannelUri: URI;
342 > readonly rawSessionId: string;
343 > readonly onDidSessionProgress: Emitter<AgentSignal>;
344 > readonly sessionLauncher: ICopilotSessionLauncher;
345 > readonly launchPlan: CopilotSessionLaunchPlan;
346 > readonly shellManager: ShellManager | undefined;
347 > /** Working directory associated with the session, used to strip redundant `cd` prefixes from shell commands. */
348 > readonly workingDirectory?: URI;
349 > /** Directory used to resolve workspace-scoped customizations for this session. */
350 > readonly customizationDirectory?: URI;
351 > /** Snapshot of the active client's tools and plugins at session creation time. */
352 > readonly clientSnapshot?: IActiveClientSnapshot;
353 > /**
354 > * Looks up the AHP id of an existing child MCP customization by
355 > * server name, so SDK MCP state events can target plugin-derived
356 > * entries narrowly. Returns `undefined` for SDK servers that have
357 > * no corresponding plugin entry — the session surfaces those as
358 > * bare top-level customizations via {@link CopilotAgentSession.topLevelMcpCustomizations}.
359 > */
360 > readonly resolveMcpChildId: (serverName: string) => string | undefined;
361 > /**
362 > * Live registry of every active client's tool contributions, shared by
363 > * reference with the agent's per-session {@link ActiveClient}. Read at
364 > * tool-call stamp time so a window reload (new `clientId`, identical
365 > * tools) stamps with the current owning id, and so each tool call is
366 > * attributed to whichever client contributed it. When omitted, a fresh
367 > * empty registry is used (test / standalone path) and client tool calls
368 > * are left unstamped.
369 > */
370 > readonly activeClientToolSet?: ActiveClientToolSet;
371 > /**
372 > * Server-side host for the agent host's server tools. When provided, the
373 > * session advertises the server tools (feedback "comments" today, more in
374 > * the future) and exposes SDK tool handlers that execute them in-process.
375 > */
376 > readonly serverToolHost?: IAgentServerToolHost;
377 > /** Returns whether the token that launched this session is still the active account token. */
378 > readonly isLaunchTokenCurrent?: () => boolean;
379 >
380 > /**
381 > * Platform used to compute the SDK sandbox policy. Defaults to
382 > * `process.platform`; injectable so tests can exercise the per-OS gating
383 > * (notably that the sandbox is ignored on Windows) deterministically.
384 > */
385 > readonly platform?: NodeJS.Platform;
386 > }
387 >
388 > /**
389 > * Lifecycle state of a {@link CopilotTurn}.
390 > *
391 > * - `pending` — the host has dispatched the message (`send()`), but the SDK
392 > * has not yet emitted any event for this turn's agentic loop.
393 > * - `running` — the SDK has emitted at least one event for this turn.
394 > * - `completed` — the turn finished normally (the loop went idle).
395 > * - `aborted` — the turn's loop was cancelled via an abort.
396 > */
397 > type CopilotTurnState = 'pending' | 'running' | 'completed' | 'aborted';
398 >
399 > /**
400 > * Encapsulates all per-turn bookkeeping for a single protocol turn, plus an
401 > * explicit lifecycle {@link CopilotTurn.state}. Holding this state on one
402 > * object (created fresh per turn) rather than as a handful of mutable session
403 > * fields means there is a single, atomic notion of "the current turn": there
404 > * is no set of counters/maps that must be reset in lockstep, and turn
405 > * transitions (running/completed/aborted) are explicit and checkable.
406 > *
407 > * The `pending → running` distinction guards turn completion against a stray
408 > * idle: an abort's terminal `session.idle` finds a queued message's turn still
409 > * `pending` (the SDK has not begun it) and leaves it open, rather than
410 > * completing it and orphaning its real response. A non-abort idle still
411 > * completes a `pending` turn defensively, so a degenerate no-op send cannot
412 > * hang the session.
413 > */
414 >
415 > /**
416 > * The token/model/cost context for a single model call, used to build a
417 > * `UsageInfo`. All fields are optional so a partial or empty context (e.g. a
418 > * subagent usage event seen before the parent's own context) is representable.
419 > */
420 > interface UsageContext {
421 > inputTokens?: number;
422 > outputTokens?: number;
423 > model?: string;
424 > cacheReadTokens?: number;
425 > cost?: number;
426 > }
427 >
428 > /** Which SDK source produced an MCP lifecycle log record. */
429 > type McpLifecycleOrigin = 'loaded' | 'statusChanged' | 'inventory';
430 >
431 > /**
432 > * SDK-neutral fields carried into a single MCP lifecycle log record. The
433 > * `session.mcp_servers_loaded` event, the `session.mcp_server_status_changed`
434 > * event, and the `rpc.mcp.list` inventory each populate the subset they carry.
435 > */
436 > interface IMcpLifecycleLogInfo {
437 > readonly name: string;
438 > readonly status: SdkMcpServerStatus;
439 > readonly error?: string;
440 > readonly source?: string;
441 > readonly transport?: string;
442 > readonly pluginName?: string;
443 > readonly pluginVersion?: string;
444 > }
445 >
446 > class CopilotTurn {
447 >
448 > private _state: CopilotTurnState = 'pending';
449 > private readonly _stopWatch = StopWatch.create(false);
450 >
451 > /**
452 > * Accumulated Copilot usage for this turn, in nano-AIU, keyed by scope.
453 > * Scope `''` is the parent turn aggregate (parent agent calls plus every
454 > * subagent call), so the parent turn's reported cost is the full turn
455 > * total. Each subagent additionally accumulates under its `parentToolCallId`
456 > * so its own component cost can be reported on the subagent's child session.
457 > */
458 > readonly copilotUsageTotalNanoAiuByScope = new Map<string, number>();
459 >
460 > /**
461 > * The parent (main-agent) turn's own last context usage — model plus token
462 > * counts and per-event cost. Subagent usage events are folded into the
463 > * parent aggregate for credit purposes only, so they must not overwrite the
464 > * parent turn's model/context-token usage. Retaining the parent's own last
465 > * values lets each subagent usage event refresh the parent aggregate's
466 > * credit total while preserving the model that produced the parent response.
467 > */
468 > parentContextUsage: UsageContext | undefined;
469 >
470 > /**
471 > * Current markdown response part IDs for this turn, keyed by
472 > * `parentToolCallId ?? ''`. Parent and subagent text stream through the
473 > * same SDK session but land in different AHP sessions, so their markdown
474 > * part state must not mask or append to each other.
475 > */
476 > readonly markdownPartIds = new Map<string, string>();
477 >
478 > /** Current reasoning response part IDs for this turn, keyed by `parentToolCallId ?? ''`. */
479 > readonly reasoningPartIds = new Map<string, string>();
480 >
481 > /**
482 > * Per-turn tool-call aggregate accumulated across the turn's `assistant.message` rounds (main
483 > * agent only), for the restricted `toolCallDetails` telemetry. `toolCounts` is keyed by tool name.
484 > */
485 > readonly toolCounts = new Map<string, number>();
486 > toolCallRounds = 0;
487 > totalToolCalls = 0;
488 > parallelToolCallRounds = 0;
489 > parallelToolCallsTotal = 0;
490 > /** Model of the most recent round, reported as the turn's model. */
491 > lastModel: string | undefined;
492 >
493 > constructor(readonly id: string, readonly ordinal: number, readonly senderClientId: string | undefined) { }
494 >
495 > get state(): CopilotTurnState { return this._state; }
496 > get isPending(): boolean { return this._state === 'pending'; }
497 > get isRunning(): boolean { return this._state === 'running'; }
498 > get duration(): number { return Math.max(0, this._stopWatch.elapsed()); }
499 >
500 > /** Transition `pending → running` on the first SDK event. No-op once running/finished. */
501 > markRunning(): void {
502 if (this._state === 'pending') {
503 this._state = 'running';
504 }
505 }
507 > markCompleted(): void { this._state = 'completed'; }
508 > markAborted(): void { this._state = 'aborted'; }
509 > }
510 >
511 > /**
512 > * Encapsulates a single Copilot SDK session and all its associated bookkeeping.
513 > *
514 > * Created by {@link CopilotAgent}, one instance per active session. Disposing
515 > * this class tears down all per-session resources (SDK wrapper, edit tracker,
516 > * database reference, pending permissions).
517 > */
518 > export class CopilotAgentSession extends Disposable {
519 > readonly sessionId: string;
520 > readonly sessionUri: URI;
521 > private readonly _chatChannelUri: URI;
522 >
523 > /** Working directory this session operates in, if any. */
524 > get workingDirectory(): URI | undefined { return this._workingDirectory; }
525 >
526 > /** Tracks active tool invocations so we can produce past-tense messages on completion. */
527 > private readonly _activeToolCalls = new Map<string, { toolName: string; displayName: string; parameters: Record<string, unknown> | undefined; content: ToolResultContent[]; parentToolCallId: string | undefined; mcpServerName: string | undefined; meta: IToolCallMeta | undefined }>();
528 > /**
529 > * Maps a running subagent's `agentId` to its parent tool call id. Session-
530 > * scoped rather than per-turn: a subagent's lifetime is bounded by its
531 > * `subagent.started` / `subagent.completed` events (and background
532 > * subagents can outlive the parent tool call), so this routing must not be
533 > * cleared on turn boundaries.
534 > */
535 > private readonly _parentToolCallIdsByAgentId = new Map<string, string>();
536 > private readonly _autoApprovals = new Map<string, PermissionAutoApproval | null>();
537 > private readonly _pendingAutoApprovals = new Map<string, DeferredPromise<PermissionAutoApproval | undefined>>();
538 > /** Pending permission requests awaiting a renderer-side decision. */
539 > private readonly _pendingPermissions = new Map<string, DeferredPromise<PermissionRequestResult>>();
540 > /**
541 > * Signatures ({@link safeStringify}) of user-approved `read`/`write`
542 > * permission requests, keyed by tool call id. The Copilot CLI runtime emits
543 > * two identical `permission.requested` events for a single file read or
544 > * write (an internal `path` prompt followed by a `read`/`write` prompt), so
545 > * without this the user would be asked to approve the same operation twice
546 > * (issue #324477). An entry is single-use: it auto-approves exactly one
547 > * subsequent request that is byte-identical to the approved one, then is
548 > * removed, so approval never carries across a different tool call, a changed
549 > * path/diff/contents, or a different kind.
550 > */
551 > private readonly _approvedDuplicablePermissionSignatures = new Map<string, string>();
552 > /** Pending user input requests awaiting a renderer-side answer. */
553 > private readonly _pendingUserInputs = new Map<string, { deferred: DeferredPromise<{ response: ChatInputResponseKind; answers?: Record<string, ChatInputAnswer> }>; questionId: string }>();
554 > /**
555 > * Pending elicitation requests awaiting a renderer-side answer. Keyed
556 > * by request id; the schema is retained so the completion handler can
557 > * project the submitted {@link ChatInputAnswer}s back into the
558 > * SDK's {@link ElicitationResult.content} shape.
559 > */
560 > private readonly _pendingElicitations = new Map<string, {
561 > readonly deferred: DeferredPromise<{ response: ChatInputResponseKind; answers?: Record<string, ChatInputAnswer> }>;
562 > readonly schema: ElicitationSchema | undefined;
563 > }>();
564 > /**
565 > * Pending plan-review requests originating from the CLI's
566 > * `exitPlanMode.request` RPC. Tracked separately from
567 > * {@link _pendingUserInputs} so the completion handler can resolve the
568 > * RPC with a structured {@link IExitPlanModeResponse} (which the CLI
569 > * forwards to `session.respondToExitPlanMode`) rather than feeding it
570 > * back through the SDK's `ask_user` callback.
571 > */
572 > private readonly _pendingPlanReviews = new Map<string, {
573 > readonly actions: readonly string[];
574 > readonly recommendedAction: string;
575 > readonly questionId: string;
576 > readonly deferred: DeferredPromise<IExitPlanModeResponse>;
577 > }>();
578 > /** File edit tracker for this session. */
579 > private readonly _editTracker: FileEditTracker;
580 > /** Session database reference. */
581 > private readonly _databaseRef: IReference<ISessionDatabase>;
582 > /** On-disk root for per-session data (database, attachments, …). */
583 > private readonly _sessionDataDir: URI;
584 > /**
585 > * The current protocol turn and its per-turn bookkeeping, or `undefined`
586 > * when the session is idle (no active turn). Replaces the former set of
587 > * loosely-coupled per-turn fields (`_turnId`, usage counter, streaming
588 > * part-id maps) with a single object carrying an explicit
589 > * {@link CopilotTurn.state} lifecycle. Created (`pending`) by
590 > * {@link resetTurnState}, finalized by {@link _completeActiveTurn}.
591 > */
592 > private _currentTurn: CopilotTurn | undefined;
593 > /** Monotonic 0-based ordinal assigned to each turn as it starts, for numeric `turnIndex` telemetry parity. */
594 > private _nextTurnOrdinal = 0;
595 > /**
596 > * Protocol turn ID of the active turn, or `''` when idle. Used by file
597 > * edit tracking and emitted on per-turn actions.
598 > */
599 > private get _turnId(): string { return this._currentTurn?.id ?? ''; }
600 > /** 0-based ordinal of the active turn within the session, or `0` when idle. */
601 > private get _turnOrdinal(): number { return this._currentTurn?.ordinal ?? 0; }
602 > /**
603 > * Whether the session currently has an in-flight turn. Used by
604 > * non-destructive idle release to avoid disconnecting mid-turn.
605 > */
606 > get hasActiveTurn(): boolean { return this._currentTurn !== undefined; }
607 > /**
608 > * Last model id seen on the SDK's per-LLM-call `Usage` event (or a
609 > * direct {@link setModel} call). We rely on the
610 > * `Usage` event rather than the tool-call event itself because
611 > * tool-call events don't carry the model id; the `Usage` event for
612 > * an LLM turn precedes that turn's `tool_use` events.
613 > */
614 > private _lastSeenModelId: string | undefined;
615 > /** SDK session wrapper, set by {@link initializeSession}. */
616 > private _wrapper!: CopilotSessionWrapper;
617 > private readonly _slashCommandProvider: CopilotSlashCommandProvider;
618 > /** Last agent mode pushed to the SDK via {@link applyMode}, to elide redundant `rpc.mode.set` calls. */
619 > private _lastAppliedMode: CopilotSdkMode | undefined;
620 > private _lastAppliedPermissionMode: PermissionAllowAllMode | undefined;
621 > private _autoApprovalExperimentalModeEnabled = false;
622 > private readonly _permissionModeSequencer = new Sequencer();
623 > private readonly _steeringMessagesInFlight = new Set<string>();
624 > /**
625 > * Steering messages that have been accepted by the SDK but not yet
626 > * surfaced to the chat UI as a separate user message. When the SDK
627 > * echoes a steering through a `user.message` event whose `content`
628 > * matches one of these entries, we finalize the in-flight turn and
629 > * dispatch a new {@link ActionType.ChatTurnStarted} whose
630 > * `userMessage` is the steering content. The reducer also removes
631 > * the pending steering via the action's `queuedMessageId`.
632 > *
633 > * Entries left here at abort/dispose time are flushed as
634 > * `steering_consumed` signals so the chat UI's pending state still
635 > * clears in cleanup paths where we never observe the echo.
636 > */
637 > private readonly _pendingSteeringFlips = new Map<string, PendingMessage>();
638 >
639 > /** Snapshot captured at session creation for refresh detection. */
640 > private readonly _appliedSnapshot: IActiveClientSnapshot;
641 > /**
642 > * Live owning-client identity, read at tool-call stamp time so a window
643 > * reload that re-pushes identical tools with a new `clientId` stamps
644 > * subsequent client tool calls with the current id rather than the one
645 > * frozen into {@link _appliedSnapshot}.
646 > */
647 > private readonly _activeClientToolSet: ActiveClientToolSet;
648 > /** Tool names that are client-provided, derived from snapshot. */
649 > private readonly _clientToolNames: ReadonlySet<string>;
650 > /** Launch-time tool-search decision; kept stable for the lifetime of the SDK session. */
651 > private readonly _toolSearchActive: boolean;
652 > /** Deferred promises for pending client tool calls, keyed by toolCallId. */
653 > private readonly _pendingClientToolCalls = new PendingRequestRegistry<ToolResultObject>();
654 > /** Pending SDK MCP auth handler promises, keyed by SDK auth request id. */
655 > private readonly _pendingMcpAuthRequests = new Map<string, IPendingMcpAuthRequest>();
656 > /** `pending-edit-content:` URIs written during permission requests, keyed
657 > * by toolCallId. Cleaned up when the permission resolves or the session
658 > * is disposed. */
659 > private readonly _pendingEditContentUris = new Map<string, URI>();
660 >
661 > private readonly _onDidSessionProgress: Emitter<AgentSignal>;
662 > private readonly _sessionLauncher: ICopilotSessionLauncher;
663 > private readonly _launchPlan: CopilotSessionLaunchPlan;
664 > private readonly _isLaunchTokenStillCurrent: () => boolean;
665 > private readonly _shellManager: ShellManager | undefined;
666 > /** Streams runtime-executed shell output into output-only (non-pty) terminal channels. */
667 > private readonly _nonPtyShellTerminals: NonPtyShellTerminalStreams;
668 > private readonly _workingDirectory: URI | undefined;
669 > private readonly _customizationDirectory: URI | undefined;
670 > private readonly _serverToolHost: IAgentServerToolHost | undefined;
671 > /** Bridges SDK-reported MCP server state into AHP customization actions. */
672 > private readonly _mcpCustomizations: McpCustomizationController;
673 >
674 > private get _storageUri(): URI {
675 return isDefaultChatUri(this._chatChannelUri) ? this.sessionUri : this._chatChannelUri;
676 }
678 > /**
679 > * Fans MCP server notifications (today: `notifications/tools/list_changed`)
680 > * up to the agent and on to the protocol server. Fired by the
681 > * `onToolsUpdated` listener once per ready MCP channel.
682 > */
683 > private readonly _onMcpNotification = this._register(new Emitter<IMcpNotification>());
684 > readonly onMcpNotification = this._onMcpNotification.event;
685 >
686 > /**
687 > * Pending MCP `sampling/createMessage` requests received over the
688 > * AHP `mcp://` channel, keyed by the cancellation handle we passed
689 > * into {@link rpc.mcp.executeSampling}. Tracked so that session
690 > * teardown can issue a best-effort
691 > * {@link rpc.mcp.cancelSamplingExecution} for each one instead of
692 > * leaving the SDK-side promise (and the upstream App) hanging.
693 > */
694 > private readonly _pendingMcpSamplings = new Set<string>();
695 >
696 > /** Tracks whether a non-empty activity has been published, so we only emit a clear when needed. */
697 > private _hasActivity = false;
698 >
699 > /**
700 > * Last SDK-reported MCP status logged for each server (keyed by server
701 > * name). Used to suppress duplicate lifecycle log records when the SDK
702 > * re-reports an unchanged status — the `rpc.mcp.list` seed and the
703 > * `session.mcp_servers_loaded` event routinely carry the same snapshot.
704 > */
705 > private readonly _lastLoggedMcpStatus = new Map<string, SdkMcpServerStatus>();
706 >
707 > /** Platform used to compute the SDK sandbox policy (injectable for tests). */
708 > private readonly _platform: NodeJS.Platform;
709 >
710 > get mcpServerStates() {
711 return this._mcpCustomizations.runtimeStates;
712 }
714 > /** Stateless reporter used to emit restricted GH/MSFT telemetry for this session's model calls. */
715 > private readonly _telemetryReporter: AgentHostTelemetryReporter;
716 > private readonly _repoInfoTelemetry: AgentHostRepoInfoTelemetry;
717 > private _activeRepoInfoTurn: {
718 > readonly telemetryMessageId: string;
719 > cancelled: boolean;
720 > begin: Promise<{ readonly context: IAgentHostRestrictedTelemetryContext; readonly baseBranch: string | undefined } | undefined>;
721 > } | undefined;
722 >
723 > constructor(
724 options: ICopilotAgentSessionOptions,
725 @IInstantiationService private readonly _instantiationService: IInstantiationService,
822 }));
823 }
825 > // ---- AgentSignal helpers ------------------------------------------------
826 >
827 > /** Wraps a {@link SessionAction} in an {@link AgentSignal} envelope and emits it. */
828 > /** todo@connor4312: AHP is missing a chat activity update action which is needed to drop `SessionAction` here */
829 > private _emitAction(action: SessionAction | ChatAction, parentToolCallId?: string): void {
830 this._onDidSessionProgress.fire({
831 kind: 'action',
835 });
836 }
838 > /**
839 > * Promotes a pending steering message into its own protocol turn:
840 > * closes the in-flight turn (so its responseParts settle into history)
841 > * and dispatches {@link ActionType.ChatTurnStarted} for a fresh
842 > * turn whose user message is the steering content. The action's
843 > * `queuedMessageId` atomically clears the corresponding pending
844 > * steering message from the session state.
845 > *
846 > * All subsequent SDK events (message deltas, tool calls, …) emitted
847 > * by the agent now reference the new `_turnId`, so the steering
848 > * response lands in the new turn rather than being folded into the
849 > * original.
850 > *
851 > * Returns the new turn id so callers (notably the `user.message`
852 > * handler) can associate the SDK event id with the steering turn for
853 > * history.truncate / sessions.fork mapping.
854 > */
855 > private _beginSteeringTurn(steering: PendingMessage): string {
856 const previousTurnId = this._turnId;
857 if (previousTurnId) {
882 return newTurnId;
883 }
885 > /**
886 > * Drains any steering messages we acknowledged to the SDK but never
887 > * promoted to their own turn (e.g. on abort or session dispose). Fires
888 > * `steering_consumed` so the chat UI removes the lingering pending
889 > * steering bubble even when no fresh `user.message` arrives.
890 > */
891 > private _drainPendingSteeringFlips(): void {
892 if (this._pendingSteeringFlips.size === 0) {
893 return;
903 }
904 }
906 > /**
907 > * Pops the buffered steering message whose text matches the SDK
908 > * `user.message` content we just observed. Matching by content (rather
909 > * than just popping FIFO) keeps us robust against the SDK reordering
910 > * or coalescing entries — concurrent steering messages with different
911 > * texts are still matched to the correct one. Returns `undefined` if
912 > * no buffered entry matches; the caller treats the `user.message` as
913 > * an ordinary echo and skips the turn flip.
914 > */
915 > private _takeMatchingPendingSteering(content: string): PendingMessage | undefined {
916 if (this._pendingSteeringFlips.size === 0) {
917 return undefined;
925 return undefined;
926 }
928 > private _parentToolCallIdForSubagentEvent(e: { readonly agentId?: string }): string | undefined {
929 return e.agentId ? this._parentToolCallIdsByAgentId.get(e.agentId) : undefined;
930 }
932 > private _shouldDropUnmappedSubagentEvent(e: { readonly agentId?: string }, eventName: string): boolean {
933 const parentToolCallId = this._parentToolCallIdForSubagentEvent(e);
934 if (!parentToolCallId && e.agentId) {
938 return false;
939 }
941 > /**
942 > * Starts a fresh `pending` turn, discarding any per-turn streaming state
943 > * from a previous turn so the next text/reasoning chunk allocates a new
944 > * response part. The turn becomes `running` on the first SDK event.
945 > */
946 > resetTurnState(turnId: string, senderClientId?: string): void {
947 this._currentTurn = new CopilotTurn(turnId, this._nextTurnOrdinal++, senderClientId);
948 }
950 > private _completeActiveTurn(): void {
951 const turn = this._currentTurn;
952 if (!turn) {
976 this._currentTurn = undefined;
977 }
979 > private _getEditFilePaths(parameters: unknown): string[] {
980 return getEditFilePaths(parameters).map(path => this._resolveEditFilePath(path));
981 }
983 > private _resolveEditFilePath(path: string): string {
984 if (isAbsolute(path) || !this._workingDirectory || this._workingDirectory.scheme !== Schemas.file) {
985 return path;
987 return join(this._workingDirectory.fsPath, path);
988 }
990 > /**
991 > * Emits a synthetic markdown content block for the active turn and
992 > * makes it the current markdown response part so that subsequent SDK
993 > * deltas append to it. Used by the agent to surface one-shot host
994 > * messages (e.g. the worktree-created announcement) at the top of the
995 > * first response.
996 > */
997 > emitInitialMarkdown(content: string): void {
998 this._emitMarkdownDelta(content);
999 }
1001 > /**
1002 > * Emits a streaming text delta. The first delta of a turn allocates a
1003 > * markdown response part; subsequent deltas append to it.
1004 > */
1005 > private _emitMarkdownDelta(content: string, parentToolCallId?: string): void {
1006 const turn = this._currentTurn;
1007 if (!turn) {
1032 }, parentToolCallId);
1033 }
1035 > /** Emits a reasoning delta, similar to {@link _emitMarkdownDelta} but for reasoning parts. */
1036 > private _emitReasoningDelta(content: string, parentToolCallId?: string): void {
1037 const turn = this._currentTurn;
1038 if (!turn) {
1059 }, parentToolCallId);
1060 }
1062 > /**
1063 > * The snapshot of client contributions captured when this session was
1064 > * created. Used by the agent to detect when the session is 1stale.
1065 > */
1066 > get appliedSnapshot(): IActiveClientSnapshot {
1067 return this._appliedSnapshot;
1068 }
1070 > get customizationDirectory(): URI | undefined {
1071 return this._customizationDirectory;
1072 }
1074 > /**
1075 > * Creates SDK {@link Tool} objects for the client-provided tools in the
1076 > * applied snapshot. The handler parks a request in
1077 > * {@link _pendingClientToolCalls} and waits for the client to dispatch
1078 > * `session/toolCallComplete`.
1079 > */
1080 > // eslint-disable-next-line @typescript-eslint/no-explicit-any
1081 > private _createClientSdkTools(): Tool<any>[] {
1082 const tools = this._appliedSnapshot.tools;
1083 if (tools.length === 0) {
1132 });
1133 }
1135 > private _isToolSearchActive(): boolean {
1136 return this._toolSearchActive;
1137 }
1139 > private _clientToolName(toolName: string): string {
1140 return this._isToolSearchActive()
1141 && toolName === RUNTIME_TOOL_SEARCH_TOOL_NAME
1143 : toolName;
1144 }
1146 > private _toToolSearchCandidates(availableTools: readonly CurrentToolMetadata[] | undefined): readonly IToolSearchCandidate[] {
1147 return (availableTools ?? [])
1148 .filter(tool => tool.deferLoading)
1152 }));
1153 }
1155 > private _emitToolSearchReady(toolCallId: string, candidates: readonly IToolSearchCandidate[]): void {
1156 const tracked = this._activeToolCalls.get(toolCallId);
1157 if (!tracked) {
1168 }, tracked.parentToolCallId);
1169 }
1171 > private _toolSearchFailure(message: string): ToolResultObject {
1172 return { textResultForLlm: message, resultType: 'failure', error: message, toolReferences: [] };
1173 }
1175 > private _toToolSearchResult(clientResult: ToolResultObject, availableTools: readonly CurrentToolMetadata[] | undefined): ToolResultObject {
1176 const deferred = new Set<string>();
1177 for (const tool of availableTools ?? []) {
1188 return { ...clientResult, toolReferences };
1189 }
1191 > private _parseToolSearchNames(text: string): string[] {
1192 try {
1193 const parsed = JSON.parse(text);
1197 }
1198 }
1200 > /**
1201 > * Builds SDK tool handlers for the agent host's server tools. Each handler
1202 > * executes the tool against this session's state via the
1203 > * {@link IAgentServerToolHost} and returns its textual result. Returns an
1204 > * empty list when no server-tool host is wired (e.g. test / standalone
1205 > * construction).
1206 > */
1207 > // eslint-disable-next-line @typescript-eslint/no-explicit-any
1208 > private _createServerSdkTools(): Tool<any>[] {
1209 const host = this._serverToolHost;
1210 if (!host) {
1228 }));
1229 }
1231 > /**
1232 > * Resolves a pending client tool call. If the SDK handler has not yet
1233 > * registered for `toolCallId`, the result is buffered so the handler
1234 > * resolves immediately once it does.
1235 > */
1236 > handleClientToolCallComplete(toolCallId: string, result: ToolCallResult) {
1237 this._approvedDuplicablePermissionSignatures.delete(toolCallId);
1238 if (!result.success && this._cancelMcpAuthenticationForToolCall(toolCallId)) {
1269 this.respondToPermissionRequest(toolCallId, true);
1270 }
1272 > private _cancelMcpAuthenticationForToolCall(toolCallId: string): boolean {
1273 for (const [requestId, pending] of this._pendingMcpAuthRequests) {
1274 const toolCallIndex = pending.toolCalls.findIndex(toolCall => toolCall.toolCallId === toolCallId);
1285 return false;
1286 }
1288 > /**
1289 > * Creates (or resumes) the SDK session via the injected launcher and
1290 > * wires up all event listeners. Must be called exactly once after
1291 > * construction before using the session.
1292 > */
1293 > async initializeSession(): Promise<void> {
1294 const wrapper = await this._sessionLauncher.launch(this._launchPlan, this._createRuntimeAdapter());
1295 // The session may have been disposed while we were awaiting the
1312 this._serverToolHost?.advertise(this._storageUri.toString());
1313 }
1315 > private _setPromptCacheState(promptCache: { readonly modelId: string; readonly cacheExpiresAt: string } | undefined): void {
1316 const currentMeta = this._stateManager.getSessionSummary(this.sessionUri.toString())?._meta;
1317 this._stateManager.setSessionMeta(this.sessionUri.toString(), withSessionPromptCacheState(currentMeta, promptCache));
1318 }
1320 > private _createRuntimeAdapter(): ICopilotSessionRuntime {
1321 return {
1322 handlePermissionRequest: request => this._handlePermissionRequest(request),
1332 };
1333 }
1335 > async resolveMcpAuthentication(params: AuthenticateParams): Promise<boolean> {
1336 let resolved = false;
1337 for (const [requestId, pending] of this._pendingMcpAuthRequests) {
1352 return resolved;
1353 }
1355 > private async _handleMcpAuthRequest(request: McpAuthRequest): Promise<McpAuthResult | null | undefined> {
1356 const githubToken = request.reason === 'initial' && this._scopesFromChallenge(request.wwwAuthenticateParams?.scope).length === 0
1357 ? await this._initialGitHubMcpToken(request)
1402 return deferred.p.finally(() => this._pendingMcpAuthRequests.delete(request.requestId));
1403 }
1405 > private _activeMcpToolCalls(serverName: string): IMcpAuthToolCall[] {
1406 if (!this._turnId) {
1407 return [];
1415 return result;
1416 }
1418 > private async _initialGitHubMcpToken(request: McpAuthRequest): Promise<string | undefined> {
1419 const githubToken = this._launchPlan.githubToken;
1420 const requestUrl = normalizeMcpServerUrl(request.serverUrl);
1434 return configuredUrls.some(u => u && requestUrl === normalizeMcpServerUrl(u)) ? githubToken : undefined;
1435 }
1437 > private _protectedResourceFromMcpAuthRequest(request: McpAuthRequest): ProtectedResourceMetadata {
1438 if (request.resourceMetadata) {
1439 try {
1454 };
1455 }
1457 > private _scopesFromChallenge(scope: string | undefined): readonly string[] {
1458 return scope?.split(/\s+/).map(s => s.trim()).filter(s => s.length > 0) ?? [];
1459 }
1461 > private _mcpAuthRequiredReason(reason: McpAuthRequest['reason']): McpAuthRequiredReason {
1462 switch (reason) {
1463 case 'refresh':
1471 }
1472 }
1474 > private _scopesSatisfy(provided: readonly string[] | undefined, required: readonly string[]): boolean {
1475 if (required.length === 0 || provided === undefined) {
1476 return true;
1479 return required.every(scope => providedSet.has(scope));
1480 }
1482 > private _cancelPendingMcpAuthRequests(): void {
1483 for (const pending of this._pendingMcpAuthRequests.values()) {
1484 pending.deferred.complete({ kind: 'cancelled' });
1486 this._pendingMcpAuthRequests.clear();
1487 }
1489 > private _cancelPendingMcpAuthRequestsForServer(serverName: string): void {
1490 for (const [requestId, pending] of this._pendingMcpAuthRequests) {
1491 if (pending.serverName !== serverName) {
1503 }
1504 }
1506 > // ---- session operations -------------------------------------------------
1507 >
1508 > async send(prompt: string, attachments?: readonly MessageAttachment[], turnId?: string, mode?: CopilotSdkMode, senderClientId?: string): Promise<void> {
1509 if (turnId && this._currentTurn?.id !== turnId) {
1510 // Establish the `pending` turn for this message. Callers normally
1643 this._logService.info(`[Copilot:${this.sessionId}] session.send() returned`);
1644 }
1646 > async hasRuntimeSlashCommand(command: string): Promise<boolean> {
1647 try {
1648 return !!(await this._slashCommandProvider.resolveSlashCommand(command));
1652 }
1653 }
1655 > async getRuntimeSlashCommands(options?: { readonly maxWaitMs?: number }): Promise<readonly RuntimeSlashCommandInfo[]> {
1656 try {
1657 return await this._slashCommandProvider.getSlashCommands(options);
1661 }
1662 }
1664 > /**
1665 > * Translate a protocol {@link MessageAttachment} into the Copilot CLI SDK's `attachments` payload shape. Resource
1666 > * attachments map to the SDK's reference-style `file`/`directory`/`selection` variants (the
1667 > * {@link MessageAttachmentBase.displayKind} advisory hint controls which one). Embedded resources (e.g. inline
1668 > * image bytes, or unsaved editor content) map to the SDK's `blob` variant, and simple attachments with a model
1669 > * representation map to `text/plain` blob attachments.
1670 > *
1671 > * Any Resource attachment carrying a {@link TextSelection} (e.g. `displayKind === 'selection'` or `'symbol'`) is
1672 > * mapped to the SDK's `selection` variant so the range survives the round-trip — keying off the `selection` field
1673 > * rather than just `displayKind` avoids symbol attachments degrading to a plain file reference (#315193). For those
1674 > * we read the resource content from disk and slice it by the carried range (the protocol's {@link TextSelection}
1675 > * only carries the range, not the inline text); on read failure the selection downgrades to a plain file reference.
1676 > * A textual embedded resource already carries the exact inline text to send (the whole live buffer for a document,
1677 > * or just the selected text for a selection), so it is forwarded as-is without further slicing.
1678 > */
1679 > private async _toSdkAttachment(attachment: MessageAttachment): Promise<CopilotSdkAttachment | undefined> {
1680 if (isAgentFeedbackAnnotationsAttachment(attachment)) {
1681 const rendered = renderAgentFeedbackAnnotationsAttachment(attachment);
1725 return { type, path, displayName };
1726 }
1728 > private async _readSelectedText(uri: URI, range: { readonly start: { readonly line: number; readonly character: number }; readonly end: { readonly line: number; readonly character: number } }): Promise<string> {
1729 const content = await this._fileService.readFile(uri);
1730 const text = content.value.toString();
1736 return text.substring(start, Math.max(start, end));
1737 }
1739 > private _getOffsetAt(lines: readonly string[], position: { readonly line: number; readonly character: number }): number {
1740 const line = Math.max(0, Math.min(position.line, lines.length - 1));
1741 let offset = 0;
1746 return offset + Math.max(0, Math.min(position.character, lineText.length));
1747 }
1749 > /**
1750 > * Pushes `mode` to the SDK via `rpc.mode.set` if it differs from the
1751 > * last applied value. Failures are logged and swallowed so that mode
1752 > * propagation does not block the turn.
1753 > */
1754 > async applyMode(mode: CopilotSdkMode | undefined): Promise<void> {
1755 if (!mode || mode === this._lastAppliedMode) {
1756 return;
1764 }
1765 }
1767 > /**
1768 > * `true` when the session's effective `mode` is `autopilot` — the
1769 > * autonomous, continue-until-done mode in which no user is available to
1770 > * answer questions or fill in elicitation forms.
1771 > */
1772 > private _isAutopilotMode(): boolean {
1773 return this._configurationService.getEffectiveValue(this._storageUri.toString(), platformSessionSchema, SessionConfigKey.Mode) === 'autopilot';
1774 }
1776 > /**
1777 > * Whether VS Code's auto-reply setting is enabled in the root config.
1778 > */
1779 > private _isAutoReplyEnabled(): boolean {
1780 return this._configurationService.getRootValue(platformRootSchema, AgentHostAutoReplyEnabledConfigKey) === true;
1781 }
1783 > async sendSteering(steeringMessage: PendingMessage): Promise<void> {
1784 if (this._steeringMessagesInFlight.has(steeringMessage.id) || this._pendingSteeringFlips.has(steeringMessage.id)) {
1785 return;
1800 }
1801 }
1803 > async getMessages(): Promise<readonly Turn[]> {
1804 const result = await this._getMappedEvents();
1805 return result.turns;
1806 }
1808 > async getSubagentMessages(parentToolCallId: string): Promise<readonly Turn[]> {
1809 const result = await this._getMappedEvents();
1810 const turns = result.subagentTurnsByToolCallId.get(parentToolCallId) ?? [];
1811 return turns;
1812 }
1814 > /**
1815 > * Returns the subagent child sessions discoverable in this session's event
1816 > * log, derived from the same {@link mapSessionEvents} reconstruction used
1817 > * for {@link getMessages}/{@link getSubagentMessages}. Lets a parent
1818 > * restore register every child up-front instead of each child re-fetching
1819 > * and re-reconstructing the full parent event log.
1820 > */
1821 > async getSubagentSessions(): Promise<readonly IRestoredSubagentSession[]> {
1822 const result = await this._getMappedEvents();
1823 if (result.subagentTurnsByToolCallId.size === 0) {
1853 return out;
1854 }
1856 > /**
1857 > * Memoized `getEvents()` + {@link mapSessionEvents} result, shared by
1858 > * {@link getMessages}, {@link getSubagentMessages} and
1859 > * {@link getSubagentSessions}. A single session open reads and
1860 > * reconstructs the full parent event log once instead of once per
1861 > * subagent. The memo is scoped to the resume/restore wave: it is dropped
1862 > * whenever the persisted event log could change (see
1863 > * {@link _invalidateMappedEvents}) and on dispose, so it never serves
1864 > * stale turns for an actively-running session.
1865 > */
1866 > private _mappedEventsMemo: Promise<IMappedSessionEvents> | undefined;
1867 >
1868 > private _getMappedEvents(): Promise<IMappedSessionEvents> {
1869 if (!this._mappedEventsMemo) {
1870 const pending = this._computeMappedEvents();
1879 return this._mappedEventsMemo;
1880 }
1882 > private async _computeMappedEvents(): Promise<IMappedSessionEvents> {
1883 const events = await this._wrapper.session.getEvents();
1884 let db: ISessionDatabase | undefined;
1896 return result;
1897 }
1899 > /** Drop the memoized event reconstruction; the next read rebuilds it. */
1900 > private _invalidateMappedEvents(): void {
1901 this._mappedEventsMemo = undefined;
1902 }
1904 > async abort(): Promise<void> {
1905 this._logService.info(`[Copilot:${this.sessionId}] Aborting session...`);
1906 this._denyPendingPermissions();
1908 await this._wrapper.session.abort();
1909 }
1911 > /**
1912 > * Explicitly destroys the underlying SDK session and waits for cleanup
1913 > * to complete. Call this before {@link dispose} when you need to ensure
1914 > * the session's on-disk data is no longer locked (e.g. before
1915 > * truncation or fork operations that modify the session files).
1916 > */
1917 > async destroySession(): Promise<void> {
1918 await this._wrapper.session.disconnect();
1919 }
1921 > async setModel(model: string, reasoningEffort?: SessionConfig['reasoningEffort'], contextTier?: SessionConfig['contextTier']): Promise<void> {
1922 this._logService.info(`[Copilot:${this.sessionId}] Changing model to: ${model}`);
1923 this._lastSeenModelId = model;
1924 await this._wrapper.session.setModel(model, { reasoningEffort, contextTier });
1925 }
1927 > /**
1928 > * Dispatches an MCP JSON-RPC method received on the `mcp://` side
1929 > * channel to the Copilot SDK's `session.rpc.mcp.*` surface.
1930 > *
1931 > * Mapping:
1932 > * - `tools/list` → `rpc.mcp.apps.listTools`
1933 > * - `tools/call` → `rpc.mcp.apps.callTool`
1934 > * - `resources/read` → `rpc.mcp.apps.readResource`
1935 > * - `resources/list` → `rpc.mcp.apps.listResources` (empty list fallback)
1936 > * - `resources/templates/list` → `rpc.mcp.apps.listResourceTemplates` (empty list fallback)
1937 > * - `sampling/createMessage` → `rpc.mcp.executeSampling`
1938 > *
1939 > * Other MCP methods are rejected with `Method not found` (the caller
1940 > * translates that into a JSON-RPC `-32601`).
1941 > */
1942 > async handleMcpRequest(serverName: string, method: string, params: Record<string, unknown> | undefined): Promise<unknown> {
1943 const apps = this._wrapper.session.rpc.mcp.apps;
1944 switch (method) {
1975 }
1976 }
1978 > async startMcpServer(id: string): Promise<void> {
1979 const serverName = this._mcpCustomizations.serverNameForCustomizationId(id);
1980 if (!serverName) {
1997 }
1998 }
2000 > private async _reconcileMcpServerEnablement(): Promise<void> {
2001 const desiredCustomizations = this._stateManager.getSessionState(this.sessionUri.toString())?.customizations ?? [];
2002 const desiredServers = getEffectiveMcpServerCustomizations(desiredCustomizations);
2033 }
2034 }
2036 > /**
2037 > * Optimistically marks the session's enabled MCP servers as Starting for the
2038 > * turn that is about to begin. Sending a message makes the SDK connect
2039 > * enabled servers in the background with no live "starting" event, so without
2040 > * this a connecting server would read as its last settled state (e.g. the
2041 > * seeded Stopped) until the connection resolves. Already-running servers are
2042 > * left untouched by the controller; the subsequent SDK status settles each
2043 > * server.
2044 > */
2045 > private _markEnabledMcpServersStarting(): void {
2046 const customizations = this._stateManager.getSessionState(this.sessionUri.toString())?.customizations ?? [];
2047 const enabled = getEffectiveMcpServerCustomizations(customizations).filter(server => server.enabled);
2050 }
2051 }
2053 > private async _disableMcpServer(serverName: string): Promise<void> {
2054 // disable() hangs until pending auth requests have resolved.
2055 // reported to the SDK folks though arguable whether it's a bug or not...
2057 await this._wrapper.session.rpc.mcp.disable({ serverName });
2058 }
2060 > async stopMcpServer(id: string): Promise<void> {
2061 const serverName = this._mcpCustomizations.serverNameForCustomizationId(id);
2062 if (!serverName) {
2067 this._mcpCustomizations.applyOne({ name: serverName, state: { kind: McpServerStatus.Stopped } });
2068 }
2070 > /**
2071 > * Forwards an App→host `sampling/createMessage` request received
2072 > * over the AHP `mcp://` channel to `rpc.mcp.executeSampling`. The
2073 > * Copilot runtime owns the MCP→chat-completion conversion and the
2074 > * sampling response shape, so we pass the raw MCP params through
2075 > * untouched and return the SDK's result directly.
2076 > *
2077 > * Resolves the JSON-RPC request with the `CreateMessageResult` on
2078 > * success and rejects on failure/cancellation, mirroring the
2079 > * `sampling/createMessage` MCP contract.
2080 > */
2081 > private async _handleSamplingCreateMessage(serverName: string, params: Record<string, unknown> | undefined): Promise<unknown> {
2082 if (!params) {
2083 throw new Error(`sampling/createMessage missing params`);
2103 }
2104 }
2106 > /**
2107 > * Selects (or clears) a custom agent on the live SDK session.
2108 > * Mirrors the SDK's `rpc.agent.select` / `rpc.agent.deselect` pair.
2109 > */
2110 > async setAgent(agentName?: string): Promise<void> {
2111 if (agentName) {
2112 const name = agentName;
2128 }
2129 }
2131 > // ---- permission handling ------------------------------------------------
2132 >
2133 > /**
2134 > * Handles a permission request from the SDK by firing a `tool_ready` event
2135 > * (which transitions the tool to PendingConfirmation) and waiting for the
2136 > * side-effects layer to respond via {@link respondToPermissionRequest}.
2137 > */
2138 > private async _handlePermissionRequest(
2139 request: ITypedPermissionRequest,
2140 ): Promise<PermissionRequestResult> {
2336 }
2337 }
2339 > private _getInternalSessionResourcePath(request: ITypedPermissionRequest): string | undefined {
2340 let permissionPath: string | undefined;
2341 if (request.kind === 'read') {
2358 return extUriBiasedIgnorePathCase.isEqualOrParent(permissionUri, sessionDir) ? permissionPath : undefined;
2359 }
2361 > /**
2362 > * Returns true when `permissionPath` lives under this session's
2363 > * `<sessionDataDir>/attachments` directory — i.e. the bytes were
2364 > * written by the agent host's user-message attachment rewriter and so
2365 > * are already user-supplied content that does not need to be
2366 > * re-confirmed via a permission prompt.
2367 > */
2368 > private _isSessionAttachmentPath(permissionPath: string): boolean {
2369 const attachmentsDir = normalizePath(URI.joinPath(this._sessionDataDir, SESSION_ATTACHMENTS_DIRNAME));
2370 const permissionUri = normalizePath(URI.file(permissionPath));
2371 return extUriBiasedIgnorePathCase.isEqualOrParent(permissionUri, attachmentsDir);
2372 }
2374 > /**
2375 > * Returns true when shell commands run inside a sandbox by default — either
2376 > * through the AgentHost's own {@link TerminalSandboxEngine} (when the custom
2377 > * terminal tool is enabled) or through the SDK's built-in shell tool wrapped
2378 > * by the `sandboxConfig` we pushed via `session.options.update`.
2379 > *
2380 > * Callers use this to auto-approve shell permission prompts that the sandbox
2381 > * already contains. Commands that explicitly opt out of the sandbox
2382 > * (`requestSandboxBypass`) are excluded by the caller, since the
2383 > * sandbox no longer contains them.
2384 > *
2385 > * Returns false when neither sandbox path is configured, so the standard
2386 > * confirmation flow is preserved.
2387 > */
2388 > private async _isShellSandboxedByDefault(): Promise<boolean> {
2389 if (this._isCustomTerminalToolEnabled()) {
2390 if (!this._shellManager) {
2398 return this._computeSdkSandboxConfig() !== undefined;
2399 }
2401 > /**
2402 > * `true` when the AgentHost's own shell tools (wrapped by
2403 > * {@link TerminalSandboxEngine}) replace the SDK's built-in shell. In that
2404 > * mode the SDK sandbox config is unused, so we neither forward nor toggle it.
2405 > */
2406 > private _isCustomTerminalToolEnabled(): boolean {
2407 return this._configurationService.getRootValue(copilotCliConfigSchema, CopilotCliConfigKey.EnableCustomTerminalTool) === true;
2408 }
2410 > /**
2411 > * The SDK-shaped sandbox policy for this session, mirroring
2412 > * {@link CopilotSessionLauncher}'s computation: `undefined` when the custom
2413 > * terminal tool is enabled (the host's own terminal sandbox engine handles
2414 > * containment) or when the host sandbox config evaluates to disabled
2415 > * (including on Windows, where the sandbox is not supported).
2416 > */
2417 > private _computeSdkSandboxConfig(): ISdkSandboxConfig | undefined {
2418 if (this._isCustomTerminalToolEnabled()) {
2419 return undefined;
2422 return buildSandboxConfigForSdk(this._platform, sandbox);
2423 }
2425 > /**
2426 > * `true` when the session runs with bypass approvals — either the global
2427 > * auto-approve setting or the session's `autoApprove` ("Allow All")
2428 > * level. Agent mode is an orthogonal axis and does not affect approvals.
2429 > */
2430 > private _isBypassApprovals(): boolean {
2431 if (this._configurationService.getRootValue(platformRootSchema, AgentHostGlobalAutoApproveEnabledConfigKey) === true) {
2432 return true;
2434 return this._configurationService.getEffectiveValue(this._storageUri.toString(), platformSessionSchema, SessionConfigKey.AutoApprove) === 'autoApprove';
2435 }
2437 > private _getSdkPermissionMode(): PermissionAllowAllMode {
2438 if (this._isBypassApprovals()) {
2439 return 'on';
2443 : 'off';
2444 }
2446 > private _getConfiguredApprovalLevel(): string {
2447 return this._configurationService.getEffectiveValue(this._storageUri.toString(), platformSessionSchema, SessionConfigKey.AutoApprove) ?? 'default';
2448 }
2450 > private _getConfiguredAgentMode(): string {
2451 return this._configurationService.getEffectiveValue(this._storageUri.toString(), platformSessionSchema, SessionConfigKey.Mode) ?? 'interactive';
2452 }
2454 > private _subscribeToPermissionConfigChanges(): void {
2455 this._register(this._configurationService.onDidRootConfigChange(() => {
2456 void this._syncPermissionModeAfterConfigChange();
2462 }));
2463 }
2465 > private async _syncPermissionModeAfterConfigChange(): Promise<void> {
2466 try {
2467 await this.syncPermissionMode('config-change');
2475 }
2476 }
2478 > private async _takeAutoApproval(toolCallId: string): Promise<PermissionAutoApproval | undefined> {
2479 if (this._autoApprovals.has(toolCallId)) {
2480 const autoApproval = this._autoApprovals.get(toolCallId) ?? undefined;
2490 }
2491 }
2493 > private _recordAutoApproval(toolCallId: string, autoApproval: PermissionAutoApproval | undefined): void {
2494 const pending = this._pendingAutoApprovals.get(toolCallId);
2495 if (pending) {
2499 this._autoApprovals.set(toolCallId, autoApproval ?? null);
2500 }
2502 > syncPermissionMode(source: 'config-change' | 'turn-start'): Promise<void> {
2503 return this._permissionModeSequencer.queue(async () => {
2504 const mode = this._getSdkPermissionMode();
2524 });
2525 }
2527 > /**
2528 > * Apply the SDK sandbox policy for the request that is about to be sent.
2529 > *
2530 > * Skips the SDK sandbox entirely when the custom terminal tool is enabled
2531 > * (the host's own terminal sandbox engine handles containment and the SDK's
2532 > * built-in shell is unused). Otherwise it always pushes the effective state
2533 > * so the SDK never retains a stale or auto-discovered sandbox: the
2534 > * configured policy unless the request runs with bypass approvals, or an
2535 > * explicitly disabled sandbox when no sandbox is configured (setting off,
2536 > * or Windows).
2537 > */
2538 > private async _applyEffectiveSandboxConfig(): Promise<void> {
2539 if (this._isCustomTerminalToolEnabled()) {
2540 return;
2549 }
2550 }
2552 > /**
2553 > * Builds an {@link FileEdit} preview for a write permission request.
2554 > *
2555 > * The `before` side references the existing file on disk directly (if it
2556 > * exists); the `after` side is written to the `pending-edit-content:`
2557 > * in-memory filesystem so the client can fetch it via `resourceRead`.
2558 > *
2559 > * Returns `undefined` for permission kinds that don't describe file
2560 > * edits or when the request is missing the fields needed to build a
2561 > * preview. If the permission request is no longer pending by the time
2562 > * the in-memory write completes (e.g. the session was aborted), the
2563 > * just-written entry is deleted so it cannot leak.
2564 > */
2565 > private async _buildEditsForPermission(request: ITypedPermissionRequest, toolCallId: string): Promise<{ items: FileEdit[] } | undefined> {
2566 if (request.kind !== 'write') {
2567 return undefined;
2611 return { items: [edit] };
2612 }
2614 > respondToPermissionRequest(requestId: string, approved: boolean): boolean {
2615 const deferred = this._pendingPermissions.get(requestId);
2616 if (deferred) {
2622 return false;
2623 }
2625 > private async _requestUnsandboxedCommandConfirmation(request: IUnsandboxedCommandConfirmationRequest): Promise<boolean> {
2626 const deferred = new DeferredPromise<PermissionRequestResult>();
2627 this._pendingPermissions.set(request.toolCallId, deferred);
2662 return (await deferred.p).kind === 'approve-once';
2663 }
2665 > // ---- user input handling ------------------------------------------------
2666 >
2667 > /**
2668 > * Handles a user input request from the SDK (ask_user tool). Auto-answers when the user is unavailable; otherwise waits for the renderer to respond via {@link respondToUserInputRequest}.
2669 > */
2670 > private async _handleUserInputRequest(
2671 request: UserInputRequest,
2672 _invocation: { sessionId: string },
2746 }
2747 }
2749 > /**
2750 > * Handles an elicitation request from the SDK (MCP server / tool prompt)
2751 > * by firing a `session/inputRequested` action and waiting for the
2752 > * renderer to respond via {@link respondToUserInputRequest}.
2753 > *
2754 > * - `form` mode requests are projected from the SDK's
2755 > * {@link ElicitationSchema} into a list of
2756 > * {@link ChatInputQuestion}s.
2757 > * - `url` mode requests surface as a question-less input request whose
2758 > * {@link ChatInputRequest.url} drives the renderer's "open URL"
2759 > * affordance.
2760 > *
2761 > * Under autopilot the request is auto-cancelled — there is no user
2762 > * available to fill in a form, and accepting with empty content would
2763 > * be misleading to the MCP server.
2764 > */
2765 > private async _handleElicitationRequest(context: ElicitationContext): Promise<ElicitationResult> {
2766 const isAutopilot = this._isAutopilotMode();
2767 if (isAutopilot) {
2829 }
2830 }
2832 > respondToUserInputRequest(requestId: string, response: ChatInputResponseKind, answers?: Record<string, ChatInputAnswer>): boolean {
2833 const pendingPlanReview = this._pendingPlanReviews.get(requestId);
2834 if (pendingPlanReview) {
2853 return false;
2854 }
2856 > /**
2857 > * Maps an `exit_plan_mode` input response back to an
2858 > * {@link IExitPlanModeResponse} that the CLI can feed into
2859 > * `session.respondToExitPlanMode`. Mapping rules:
2860 > *
2861 > * - Decline / Cancel / no answer → `{ approved: false }` (model gets a
2862 > * rejection result and stays in plan mode).
2863 > * - Accept + freeform feedback → `{ approved: false, feedback, selectedAction? }`
2864 > * (the SDK treats this as a revision request and re-emits
2865 > * `exit_plan_mode.requested` after revising the plan).
2866 > * - Accept + selected option → `{ approved: true, selectedAction, autoApproveEdits }`
2867 > * where `autoApproveEdits` is set for the autopilot variants.
2868 > *
2869 > * `selectedAction` is validated against the SDK's offered `actions`; an
2870 > * unknown value is treated as a decline so the SDK isn't fed a value it
2871 > * cannot handle.
2872 > */
2873 > private _resolveExitPlanMode(
2874 pending: { actions: readonly string[]; recommendedAction: string; questionId: string },
2875 response: ChatInputResponseKind,
2942 };
2943 }
2945 > /**
2946 > * Translates an approved `exit_plan_mode` action into the AHP `mode` axis
2947 > * and writes it so the mode picker reflects the choice immediately:
2948 > *
2949 > * - `autopilot` / `autopilot_fleet` → `mode='autopilot'`.
2950 > * - `interactive` → `mode='interactive'`.
2951 > * - `exit_only` (approve plan without executing) leaves the mode untouched.
2952 > */
2953 > private _syncAhpModeFromExitPlanAction(selectedAction: string): void {
2954 switch (selectedAction) {
2955 case 'autopilot':
2962 }
2963 }
2965 > private async _handlePreToolUse(input: PreToolUseHookInput): Promise<void> {
2966 try {
2967 if (isEditTool(input.toolName, getToolCommand(input))) {
2974 }
2975 }
2977 > private async _handlePostToolUse(input: PostToolUseHookInput): Promise<void> {
2978 try {
2979 if (isEditTool(input.toolName, getToolCommand(input))) {
2986 }
2987 }
2989 > private async _beginRepoInfoTelemetry(telemetryMessageId: string, isCurrent: () => boolean): Promise<{ readonly context: IAgentHostRestrictedTelemetryContext; readonly baseBranch: string | undefined } | undefined> {
2990 let resolved: { readonly context: IAgentHostRestrictedTelemetryContext; readonly baseBranch: string | undefined } | undefined;
2991 try {
3001 return resolved;
3002 }
3004 > private async _endRepoInfoTelemetry(telemetryMessageId: string, resolved: { readonly context: IAgentHostRestrictedTelemetryContext; readonly baseBranch: string | undefined } | undefined, isCurrent: () => boolean): Promise<void> {
3005 if (!resolved || this._store.isDisposed || !isCurrent()) {
3006 return;
3008 await this._repoInfoTelemetry.reportEnd(resolved.context, this.sessionUri.toString(), telemetryMessageId, this._workingDirectory, resolved.baseBranch, isCurrent);
3009 }
3011 > private _completeActiveRepoInfoTelemetry(): void {
3012 const turn = this._activeRepoInfoTurn;
3013 if (!turn) {
3018 void turn.begin.then(resolved => this._endRepoInfoTelemetry(turn.telemetryMessageId, resolved, isCurrent));
3019 }
3021 > private _cancelActiveRepoInfoTelemetry(): void {
3022 const turn = this._activeRepoInfoTurn;
3023 if (!turn) {
3028 void turn.begin.finally(() => this._repoInfoTelemetry.clearTurn(turn.telemetryMessageId));
3029 }
3031 > private async _resolveRepoInfoTelemetryContext(): Promise<{ readonly context: IAgentHostRestrictedTelemetryContext; readonly baseBranch: string | undefined } | undefined> {
3032 if (this._configurationService.getRootValue(platformRootSchema, AgentHostDisableRepoInfoTelemetryConfigKey) === true) {
3033 return undefined;
3046 return { context: this._toRepoInfoTelemetryContext(rawContext), baseBranch };
3047 }
3049 > private _isLaunchTokenCurrent(): boolean {
3050 return this._launchPlan.githubToken !== undefined && this._isLaunchTokenStillCurrent();
3051 }
3053 > private _toRepoInfoTelemetryContext(context: IRestrictedTelemetryContext): IAgentHostRestrictedTelemetryContext {
3054 return {
3055 restrictedTelemetryEnabled: context.restrictedTelemetryEnabled,
3062 };
3063 }
3065 > // ---- event wiring -------------------------------------------------------
3066 >
3067 > private _subscribeToEvents(): void {
3068 const wrapper = this._wrapper;
3069 const sessionId = this.sessionId;
3870 this._seedMcpServersFromRpc();
3871 }
3873 > /**
3874 > * One-shot fetch of `rpc.mcp.list` at subscription time. Best-effort:
3875 > * any failure is logged and the inventory simply stays empty until the
3876 > * next live event arrives.
3877 > */
3878 > private _seedMcpServersFromRpc(): void {
3879 this._refreshMcpServersFromRpc().catch(err => {
3880 this._logService.warn(`[Copilot:${this.sessionId}] Failed to seed MCP server inventory`, err);
3881 });
3882 }
3884 > private async _refreshMcpServersFromRpc(): Promise<void> {
3885 const mcpRpc = this._wrapper.session.rpc?.mcp;
3886 if (!mcpRpc) {
3900 }
3901 }
3903 > private _applyMcpServerList(servers: readonly { readonly name: string; readonly status: SdkMcpServerStatus; readonly error?: string }[]): void {
3904 const sdkServers = servers
3905 .map(s => this._toSdkMcpServer(s.name, s.status, s.error));
3906 this._mcpCustomizations.applyAll(sdkServers);
3907 }
3909 > /**
3910 > * Logs a full MCP inventory snapshot ({@link _logMcpServerLifecycle} per
3911 > * server), then forgets the dedup entry for any server that dropped out of
3912 > * the snapshot so a later re-add re-logs its arrival.
3913 > */
3914 > private _logMcpServersSnapshot(servers: readonly IMcpLifecycleLogInfo[], origin: McpLifecycleOrigin): void {
3915 const seen = new Set<string>();
3916 for (const server of servers) {
3924 }
3925 }
3927 > /**
3928 > * Emits a single structured MCP lifecycle log record for `server`,
3929 > * deduplicated by SDK status so an unchanged re-report stays quiet. Failed
3930 > * servers log at `error` (carrying the failure text in the body and an
3931 > * `errorType` attribute); every other transition logs at `info`. Records
3932 > * flow through {@link ILogService} to the agent host's OTLP log stream.
3933 > */
3934 > private _logMcpServerLifecycle(server: IMcpLifecycleLogInfo & { readonly origin: McpLifecycleOrigin }): void {
3935 if (this._lastLoggedMcpStatus.get(server.name) === server.status) {
3936 return;
3959 }
3960 }
3962 > private _setToolCallUiMeta(meta: Mutable<IToolCallMeta>, resourceUri: string | undefined, mcpServerName: string | undefined): void {
3963 if (!resourceUri) {
3964 return;
3973 meta.ui = ui;
3974 }
3976 > /**
3977 > * Broadcasts `notifications/tools/list_changed` for every MCP server
3978 > * currently in the `Ready` state. The SDK's `session.tools_updated`
3979 > * event is a coarse "tools refreshed" hint that doesn't identify
3980 > * which server changed, so we fan out to all ready channels. Clients
3981 > * are expected to refetch `tools/list` on each notification.
3982 > */
3983 > private _fireMcpToolsListChanged(): void {
3984 for (const { channel } of this._mcpCustomizations.readyChannels()) {
3985 this._onMcpNotification.fire({
3989 }
3990 }
3992 > /** Snapshot of MCP servers that have no plugin-derived child entry. */
3993 > topLevelMcpCustomizations() {
3994 return this._mcpCustomizations.topLevelCustomizations();
3995 }
3997 > /**
3998 > * Translates the SDK's flat MCP status string into AHP's discriminated
3999 > * {@link McpServerState} union.
4000 > */
4001 > private _toSdkMcpServer(name: string, status: SdkMcpServerStatus, error?: string): ISdkMcpServer {
4002 return {
4003 name,
4006 };
4007 }
4009 > private _translateSdkMcpStatus(name: string, status: SdkMcpServerStatus, error?: string): McpServerState {
4010 switch (status) {
4011 case 'connected':
4034 }
4035 }
4037 > /**
4038 > * Translates the SDK's three-mode space (`interactive` / `plan` /
4039 > * `autopilot`) to AHP's `mode` axis directly:
4040 > *
4041 > * - SDK `plan` → AHP `mode='plan'`.
4042 > * - SDK `interactive` → AHP `mode='interactive'`.
4043 > * - SDK `autopilot` → AHP `mode='autopilot'`.
4044 > *
4045 > * Autopilot lives on the `mode` axis; the orthogonal `autoApprove` axis
4046 > * (Default / Bypass) is left untouched so the user's chosen
4047 > * approval level is preserved across SDK mode transitions.
4048 > *
4049 > * Patches that already match the current AHP values are still
4050 > * dispatched (the reducer is a no-op in that case) but written values
4051 > * propagate to all subscribed clients via `session/configChanged`.
4052 > */
4053 > private _syncAhpConfigFromSdkMode(sdkMode: CopilotSdkMode): void {
4054 const sessionUri = this._storageUri.toString();
4055 const patch: Record<string, unknown> = {};
4067 this._configurationService.updateSessionConfig(sessionUri, patch);
4068 }
4070 > /**
4071 > * Handles the CLI's `exitPlanMode.request` RPC by surfacing it as a
4072 > * {@link ChatInputRequest} and awaiting the client's response. The
4073 > * resolved {@link IExitPlanModeResponse} flows back to the CLI, which
4074 > * calls `session.respondToExitPlanMode` internally — that resumes the
4075 > * paused `exit_plan_mode` tool call and (on accept) updates the SDK's
4076 > * `currentMode` so the model can continue with implementation.
4077 > */
4078 > private async _handleExitPlanModeRequest(data: ExitPlanModeRequest, _invocation: { sessionId: string }): Promise<IExitPlanModeResponse> {
4079 const turnId = this._currentTurn?.id;
4080 if (!turnId) {
4160 }
4161 }
4163 > /**
4164 > * Drop the memoized event reconstruction whenever the persisted event log
4165 > * could have changed, so {@link _getMappedEvents} never serves stale turns
4166 > * once the session resumes activity. While the session is idle (e.g. during
4167 > * a historical session open) none of these fire, so the whole restore wave
4168 > * coalesces to a single reconstruction.
4169 > */
4170 > private _subscribeForMemoInvalidation(): void {
4171 const wrapper = this._wrapper;
4172 const invalidate = () => this._invalidateMappedEvents();
4186 this._register(wrapper.onSessionSnapshotRewind(invalidate));
4187 }
4189 > /**
4190 > * Emits `instructionsCollected` per user message.
4191 > * Attempts to match local chat's `ComputeAutomaticInstructions`
4192 > * emitter (`src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts`)
4193 > */
4194 > private _subscribeForInstructionsCollectedTelemetry(): void {
4195 const wrapper = this._wrapper;
4196 const sessionId = this.sessionId;
4273 }));
4274 }
4276 > private _subscribeForLogging(): void {
4277 const wrapper = this._wrapper;
4278 const sessionId = this.sessionId;
4493 }));
4494 }
4496 > // ---- SDK event ID tracking & truncation ---------------------------------
4497 >
4498 > /**
4499 > * Returns the SDK event ID for the turn inserted after the given turn,
4500 > * or `undefined` if it's the last turn.
4501 > */
4502 > getNextTurnEventId(turnId: string): Promise<string | undefined> {
4503 return this._databaseRef.object.getNextTurnEventId(turnId);
4504 }
4506 > /**
4507 > * Returns the SDK event ID of the earliest turn.
4508 > */
4509 > getFirstTurnEventId(): Promise<string | undefined> {
4510 return this._databaseRef.object.getFirstTurnEventId();
4511 }
4513 > /**
4514 > * Truncates the session history via the SDK's RPC and cleans up
4515 > * stale turns from the session database.
4516 > *
4517 > * @param eventId The SDK event ID at which to truncate. This event
4518 > * and all events after it are removed.
4519 > * @param keepTurnId If provided, turns inserted after this turn are
4520 > * deleted from the DB. If omitted, all turns are deleted.
4521 > */
4522 > async truncateAtEventId(eventId: string, keepTurnId?: string): Promise<void> {
4523 this._logService.info(`[Copilot:${this.sessionId}] Truncating via SDK RPC at eventId=${eventId}`);
4524 const result = await this._wrapper.session.rpc.history.truncate({ eventId });
4533 }
4534 }
4536 > /**
4537 > * Bulk-remaps turn IDs in this session's database.
4538 > * Used after file-copying a source session's database for a fork.
4539 > */
4540 > async remapTurnIds(mapping: ReadonlyMap<string, string>): Promise<void> {
4541 await this._databaseRef.object.remapTurnIds(mapping);
4542 }
4544 > // ---- cleanup ------------------------------------------------------------
4545 >
4546 > private _denyPendingPermissions(): void {
4547 for (const [toolCallId, deferred] of this._pendingPermissions) {
4548 this._deletePendingEditContent(toolCallId);
4552 this._approvedDuplicablePermissionSignatures.clear();
4553 }
4555 > /**
4556 > * Removes any `pending-edit-content:` entries associated with a resolved
4557 > * (approved, denied, or cancelled) permission request.
4558 > */
4559 > private _deletePendingEditContent(toolCallId: string): void {
4560 const uri = this._pendingEditContentUris.get(toolCallId);
4561 if (!uri) {
4567 });
4568 }
4570 > private _cancelPendingUserInputs(): void {
4571 for (const [, pending] of this._pendingUserInputs) {
4572 pending.deferred.complete({ response: ChatInputResponseKind.Cancel });
4574 this._pendingUserInputs.clear();
4575 }
4577 > private _cancelPendingElicitations(): void {
4578 for (const [, pending] of this._pendingElicitations) {
4579 pending.deferred.complete({ response: ChatInputResponseKind.Cancel });
4581 this._pendingElicitations.clear();
4582 }
4584 > private _cancelPendingPlanReviews(): void {
4585 for (const [, pending] of this._pendingPlanReviews) {
4586 pending.deferred.complete({ approved: false });
4588 this._pendingPlanReviews.clear();
4589 }
4591 > private _cancelPendingMcpSamplings(): void {
4592 const pending = Array.from(this._pendingMcpSamplings);
4593 this._pendingMcpSamplings.clear();
4598 }
4599 }
4601 > private _cancelPendingClientToolCalls(): void {
4602 this._pendingClientToolCalls.denyAll({ textResultForLlm: 'Tool call cancelled: session ended', resultType: 'failure', error: 'Session ended' });
4603 }
4605 >
4606 > /**
4607 > * Counts added/removed lines in a unified diff string. Ignores the `+++` and
4608 > * `---` header rows and any non-hunk context.
4609 > */
4610 function countUnifiedDiffLines(diff: string): { added: number; removed: number } | undefined {
4611 let added = 0;
4626 return { added, removed };
4627 }
4629 > /**
4630 > * Normalizes the SDK's internal `quotaSnapshots` field — present on the `assistant.usage` event at
4631 > * runtime but absent from the generated `AssistantUsageData` type — into the serializable shape
4632 > * carried on {@link UsageInfoMeta.quotaSnapshots}. Returns `undefined` when no usable snapshot is present.
4633 > */
4634 function normalizeQuotaSnapshots(raw: unknown): UsageInfoMeta['quotaSnapshots'] | undefined {
4635 if (!raw || typeof raw !== 'object') {
src/vs/platform/agentHost/node/copilot/copilotSlashCommandProvider.ts 40 introduced LOC · 9 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- copilotSlashCommandProvider.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 { CopilotClient } from '@github/copilot-sdk';
7 > import { ILogService } from '../../../log/common/log.js';
8 > import { raceTimeout } from '../../../../base/common/async.js';
9 >
10 > type RuntimeSlashCommandCatalog = {
11 > readonly commands: readonly RuntimeSlashCommandInfo[];
12 > readonly byName: ReadonlyMap<string, RuntimeSlashCommandInfo>;
13 > readonly byAlias: ReadonlyMap<string, RuntimeSlashCommandInfo>;
14 > };
15 >
16 > type RuntimeSlashCommandCache = {
17 > value?: RuntimeSlashCommandCatalog;
18 > inFlight?: Promise<RuntimeSlashCommandCatalog>;
19 > };
20 >
21 > type RuntimeSlashCommandInfo = Awaited<ReturnType<CopilotClient['rpc']['commands']['list']>>['commands'][number];
22 >
23 > export class CopilotSlashCommandProvider {
24 > private _runtimeSlashCommandCache: RuntimeSlashCommandCache | undefined;
25 > constructor(
26 private readonly listCommands: () => Promise<RuntimeSlashCommandInfo[]>,
27 @ILogService private readonly _logService: ILogService,
28 ) { }
30 > async getSlashCommands(options?: { readonly maxWaitMs?: number }): Promise<readonly RuntimeSlashCommandInfo[]> {
31 try {
32 const maxWaitMs = options?.maxWaitMs;
38 }
39 }
41 > public async resolveSlashCommand(command: string, maxWaitMs: number | undefined = undefined): Promise<RuntimeSlashCommandInfo | undefined> {
42 const key = this._normalizeSlashCommandKey(command);
43 if (!key) {
47 return catalog.byName.get(key) ?? catalog.byAlias.get(key);
48 }
50 > public clearCache(): void {
51 if (this._runtimeSlashCommandCache) {
52 // Keep in-flight promises isolated from fresh lookups after invalidation.
54 }
55 }
57 > private async _getRuntimeSlashCommandCatalog(maxWaitMs: number | undefined = undefined): Promise<RuntimeSlashCommandCatalog> {
58 const cache = this._runtimeSlashCommandCache ??= {};
59 if (cache.value) {
78 };
79 }
81 > private async _refreshRuntimeSlashCommandCatalog(cache: RuntimeSlashCommandCache): Promise<RuntimeSlashCommandCatalog> {
82 if (cache.inFlight) {
83 return cache.inFlight;
101 return inFlight;
102 }
104 > private _toRuntimeSlashCommandCatalog(commands: readonly RuntimeSlashCommandInfo[]): RuntimeSlashCommandCatalog {
105 const byName = new Map<string, RuntimeSlashCommandInfo>();
106 const byAlias = new Map<string, RuntimeSlashCommandInfo>();
127 return { commands: deduped, byName, byAlias };
128 }
130 > private _normalizeSlashCommandKey(command: string): string | undefined {
131 const trimmed = command.trim();
132 if (!trimmed) {
src/vs/platform/agentHost/node/copilot/pendingEditContentStore.ts 32 introduced LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- pendingEditContentStore.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 { encodeHex, VSBuffer } from '../../../../base/common/buffer.js';
7 > import { IDisposable } from '../../../../base/common/lifecycle.js';
8 > import { URI } from '../../../../base/common/uri.js';
9 > import { IFileService } from '../../../files/common/files.js';
10 > import { InMemoryFileSystemProvider } from '../../../files/common/inMemoryFilesystemProvider.js';
11 >
12 > /**
13 > * URI scheme for transient file content backing tool-call write-permission
14 > * previews. Files under this scheme live in an in-memory provider registered
15 > * on the agent host's file service; content can be read/written through the
16 > * file service just like any other resource.
17 > */
18 > export const PENDING_EDIT_CONTENT_SCHEME = 'pending-edit-content';
19 >
20 > /**
21 > * Builds a `pending-edit-content:` URI identifying the proposed "after"
22 > * content for a write permission request. The authority is a hex-encoded
23 > * session URI so multiple concurrent sessions don't collide.
24 > */
25 > export function buildPendingEditContentUri(sessionUri: string, toolCallId: string, filePath: string): URI {
26 return URI.from({
27 scheme: PENDING_EDIT_CONTENT_SCHEME,
30 });
31 }
33 > /**
34 > * Registers a fresh {@link InMemoryFileSystemProvider} for the
35 > * `pending-edit-content:` scheme on the given file service. Callers use the
36 > * returned disposable to unregister the provider.
37 > */
38 > export function registerPendingEditContentProvider(fileService: IFileService): IDisposable {
39 const provider = new InMemoryFileSystemProvider();
40 const registration = fileService.registerProvider(PENDING_EDIT_CONTENT_SCHEME, provider);