src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts

628 LOC · 579 covered · 49 uncovered · 81 ranges · 903 concepts · 30 introducers · 421 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

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 related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- copilotSessionWrapper.ts ×51
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, ExitPlanModeRequest, ExitPlanModeResult, NamedProviderConfig, PermissionRequestResult, ProviderModelConfig, ResumeSessionConfig, SessionConfig, Tool } from '@github/copilot-sdk';
7 > import { coalesce } from '../../../../base/common/arrays.js';
8 > import { Schemas } from '../../../../base/common/network.js';
9 > import { URI } from '../../../../base/common/uri.js';
10 > import { IFileService } from '../../../files/common/files.js';
11 > import { ILogService, LogLevel } from '../../../log/common/log.js';
12 > import { CopilotCliConfigKey, applyModelFamilyAlias, copilotCliConfigSchema } from '../../common/copilotCliConfig.js';
13 > import { agentHostModelSupportsToolSearch, CLIENT_TOOL_SEARCH_REFERENCE_NAME } from './toolSearchDeferral.js';
14 > import { AgentHostSessionSyncEnabledConfigKey, platformRootSchema, type AgentHostMcpServers } from '../../common/agentHostSchema.js';
15 > import { AgentHostSandboxConfigKey, sandboxConfigSchema } from '../../common/sandboxConfigSchema.js';
16 > import { IAgentConfigurationService } from '../agentConfigurationService.js';
17 > import { IAgentHostTerminalManager } from '../agentHostTerminalManager.js';
18 > import { IByokLmBridgeRegistry } from '../byokLmBridgeRegistry.js';
19 > import { IByokLmProxyService, type IByokLmProxyHandle } from './byokLmProxyService.js';
20 > import type { IByokLmModelInfo } from '../../common/agentHostByokLm.js';
21 > import type { ModelSelection, ToolDefinition } from '../../common/state/protocol/state.js';
22 > import type { ActiveClientToolSet } from '../activeClientState.js';
23 > import { CopilotSessionWrapper } from './copilotSessionWrapper.js';
24 > import { ShellManager, createShellTools, type IUnsandboxedCommandConfirmationRequest } from './copilotShellTools.js';
25 > import { toSdkHooks, toSdkInstructionDirectories, toSdkMcpServers, toSdkMcpServersFromConfigMap, toSdkSessionCustomAgents, toSdkSkillDirectories } from './copilotPluginConverters.js';
26 > import { buildSandboxConfigForSdk, type ISdkSandboxConfig } from './sandboxConfigForSdk.js';
27 > import type { ITypedPermissionRequest } from './copilotToolDisplay.js';
28 > import type { ICopilotPluginInfo } from './copilotAgent.js';
29 > import { agentHostPromptRegistry, type IAgentHostPromptContext } from './prompts/promptRegistry.js';
30 > import { describeSystemMessageConfig } from './prompts/systemMessage.js';
31 > import './prompts/allPrompts.js';
32 > import { StopWatch } from '../../../../base/common/stopwatch.js';
33 >
34 > export const ThinkingLevelConfigKey = 'thinkingLevel';
35 > /**
36 > * Config key for the numeric "Context Size" selection (a context-window token count). Mapped to the
37 > * SDK's two-valued {@link SessionConfig.contextTier} by {@link getCopilotContextTier}.
38 > */
39 > export const ContextSizeConfigKey = 'contextSize';
40 > /**
41 > * @deprecated Legacy config key that stored the resolved tier string (`'default'` / `'long_context'`)
42 > * directly. Replaced by the numeric {@link ContextSizeConfigKey}; still read from persisted sessions
43 > * for backward compatibility.
44 > */
45 > export const ContextTierConfigKey = 'contextTier';
46 >
47 > const ReasoningEfforts = ['low', 'medium', 'high', 'xhigh'] as const;
48 > type ReasoningEffort = NonNullable<SessionConfig['reasoningEffort']>;
49 >
50 > const ContextTiers = ['default', 'long_context'] as const;
51 > type ContextTier = NonNullable<SessionConfig['contextTier']>;
52 > const AGENT_HOST_COPILOT_CLIENT_NAME = 'vscode-agent-host';
53 >
54 > type UserInputHandler = NonNullable<SessionConfig['onUserInputRequest']>;
55 > type UserInputRequest = Parameters<UserInputHandler>[0];
56 > type UserInputInvocation = Parameters<UserInputHandler>[1];
57 > type UserInputResponse = Awaited<ReturnType<UserInputHandler>>;
58 > type ElicitationHandler = NonNullable<SessionConfig['onElicitationRequest']>;
59 > type ElicitationContext = Parameters<ElicitationHandler>[0];
60 > type ElicitationResult = Awaited<ReturnType<ElicitationHandler>>;
61 > type McpAuthHandler = NonNullable<SessionConfig['onMcpAuthRequest']>;
62 > type McpAuthRequest = Parameters<McpAuthHandler>[0];
63 > type McpAuthContext = Parameters<McpAuthHandler>[1];
64 > type McpAuthResponse = Awaited<ReturnType<McpAuthHandler>>;
65 > type SessionHooks = NonNullable<SessionConfig['hooks']>;
66 > type PreToolUseHookInput = Parameters<NonNullable<SessionHooks['onPreToolUse']>>[0];
67 > type PostToolUseHookInput = Parameters<NonNullable<SessionHooks['onPostToolUse']>>[0];
68 > type CopilotSessionLaunchConfig = ResumeSessionConfig & {
69 > readonly pluginDirectories?: string[];
70 > readonly remoteSession?: 'export';
71 > };
72 >
73 > /**
74 > * Immutable snapshot of the active client's structural contributions at
75 > * session creation time. Used to detect when the session needs to be
76 > * refreshed. Root MCP servers participate in restart detection because they
77 > * are merged into the SDK session config. The owning `clientId`s are
78 > * deliberately NOT part of this snapshot: client identity is tracked live via
79 > * {@link ActiveClientToolSet} so a window
80 > * reload (new `clientId`, identical tools/plugins) does not force a restart.
81 > */
82 > export interface IActiveClientSnapshot {
83 > readonly tools: readonly ToolDefinition[];
84 > readonly plugins: readonly ICopilotPluginInfo[];
85 > readonly mcpServers: AgentHostMcpServers;
86 > }
87 >
88 > /**
89 > * The set of client-tool names the agent sees for a snapshot — each tool's
90 > * `ToolDefinition.name` (the camelCase `toolReferenceName`). Used both to gate
91 > * tool-specific prompt sections at launch and to route client tool calls during
92 > * the session, so the two stay derived from one definition.
93 > */
94 > export function clientToolNamesFromSnapshot(snapshot: IActiveClientSnapshot): ReadonlySet<string> {
95 > return new Set(snapshot.tools.map(tool => tool.name)); copilotSessionLauncher.ts ×1
96 > }
98 > export interface ICopilotSessionRuntime {
99 > handlePermissionRequest(request: ITypedPermissionRequest): Promise<PermissionRequestResult>;
100 > handleExitPlanModeRequest(request: ExitPlanModeRequest, invocation: { sessionId: string }): Promise<ExitPlanModeResult>;
101 > handleUserInputRequest(request: UserInputRequest, invocation: UserInputInvocation): Promise<UserInputResponse>;
102 > handleElicitationRequest(context: ElicitationContext): Promise<ElicitationResult>;
103 > handleMcpAuthRequest(request: McpAuthRequest, context: McpAuthContext): Promise<McpAuthResponse>;
104 > requestUnsandboxedCommandConfirmation(request: IUnsandboxedCommandConfirmationRequest): Promise<boolean>;
105 > handlePreToolUse(input: PreToolUseHookInput): Promise<void>;
106 > handlePostToolUse(input: PostToolUseHookInput): Promise<void>;
107 > // eslint-disable-next-line @typescript-eslint/no-explicit-any
108 > createClientSdkTools(): Tool<any>[];
109 > // eslint-disable-next-line @typescript-eslint/no-explicit-any
110 > createServerSdkTools(): Tool<any>[];
111 > }
112 >
113 > export interface ICopilotSessionLauncher {
114 > /**
115 > * Creates an unowned SDK session wrapper. The caller is responsible for
116 > * registering or disposing the returned wrapper.
117 > */
118 > launch(plan: CopilotSessionLaunchPlan, runtime: ICopilotSessionRuntime): Promise<CopilotSessionWrapper>;
119 > }
120 >
121 > type CopilotSessionClient = Pick<CopilotClient, 'createSession' | 'resumeSession'>;
122 >
123 > interface ICopilotSessionLaunchBase {
124 > readonly client: CopilotSessionClient;
125 > readonly sessionId: string;
126 > readonly workingDirectory: URI | undefined;
127 > readonly resolvedAgentName: string | undefined;
128 > readonly snapshot: IActiveClientSnapshot;
129 > /**
130 > * Live, long-lived registry of every active client's tool contributions.
131 > * Read at tool-call stamp time so a window reload (new `clientId`,
132 > * identical tools) stamps subsequent client tool calls with the current
133 > * owning id rather than the one frozen into {@link snapshot} at creation,
134 > * and so a tool call is attributed to whichever client contributed it.
135 > */
136 > readonly activeClientToolSet: ActiveClientToolSet;
137 > readonly shellManager: ShellManager | undefined;
138 > readonly githubToken: string | undefined;
139 >
140 > /**
141 > * Whether this is a workspace-less session. Threaded into the
142 > * prompt context so the resolved system message gets the scratch/repoless
143 > * variant. Named to match the `workspaceless` marker used throughout the AH
144 > * layer (session `_meta`, stored metadata) that this value flows from.
145 > */
146 > readonly workspaceless?: boolean;
147 > }
148 >
149 > export interface ICopilotCreateSessionLaunchPlan extends ICopilotSessionLaunchBase {
150 > readonly kind: 'create';
151 > readonly model: ModelSelection | undefined;
152 > readonly longContextWindow?: number;
153 > readonly freeLongContext?: boolean;
154 > }
155 >
156 > export interface ICopilotResumeSessionLaunchPlan extends ICopilotSessionLaunchBase {
157 > readonly kind: 'resume';
158 > readonly workingDirectory: URI;
159 > readonly fallback: {
160 > readonly model: ModelSelection | undefined;
161 > readonly longContextWindow?: number;
162 > readonly freeLongContext?: boolean;
163 > };
164 > }
165 >
166 > export type CopilotSessionLaunchPlan = ICopilotCreateSessionLaunchPlan | ICopilotResumeSessionLaunchPlan;
167 >
168 > function isReasoningEffort(value: unknown): value is ReasoningEffort { copilotSessionLauncher.ts ×3
169 > return ReasoningEfforts.some(reasoningEffort => reasoningEffort === value);
170 > }
172 > function isContextTier(value: unknown): value is ContextTier { copilotSessionLauncher.ts ×7
173 > return ContextTiers.some(contextTier => contextTier === value);
174 > }
176 > function getCopilotSdkErrorCode(err: unknown): number | undefined { copilotSessionLauncher.ts ×4
177 > if (typeof err !== 'object' || err === null) {
178 return undefined;
179 }
180 > const code = Object.getOwnPropertyDescriptor(err, 'code')?.value; copilotSessionLauncher.ts ×4
181 > return typeof code === 'number' ? code : undefined;
182 > }
184 > function getErrorMessage(err: unknown): string { copilotSessionLauncher.ts ×4
185 > if (err instanceof Error) {
186 > return err.message;
187 > }
188 > if (typeof err === 'object' && err !== null) {
189 const message = Object.getOwnPropertyDescriptor(err, 'message')?.value;
190 if (typeof message === 'string') {
191 return message;
192 }
193 }
194 return String(err);
195 }
197 > /**
198 > * Decide whether a Copilot SDK `resumeSession` failure should fall back to
199 > * `createSession({ sessionId })`. We want to preserve the original
200 > * recovery for empty / truncated sessions (e.g. after the user invoked
201 > * "Start Over", which calls `truncateSession` and leaves the on-disk
202 > * session with zero events - the SDK then refuses to resume it), but we
203 > * must NOT silently swallow corruption / schema-validation / parse
204 > * failures: those should surface so the user sees the real error and the
205 > * original session contents are not masked by a fresh empty session.
206 > *
207 > * Heuristic: any `-32603` Internal Error is treated as the empty-session
208 > * case UNLESS the message clearly indicates corruption, schema
209 > * validation, parse failure, or malformed input.
210 > */
211 > function shouldCreateEmptySessionAfterResumeError(err: unknown): boolean { copilotSessionLauncher.ts ×2
212 > if (getCopilotSdkErrorCode(err) !== -32603) {
213 > return false; copilotAgent.ts ×3
214 > }
216 > const message = getErrorMessage(err);
217 > return !/\b(corrupt|corrupted|invalid|validation|schema|must be|parse|malformed|unexpected token)\b/i.test(message);
218 > }
220 > function isCustomAgentNotFoundError(err: unknown): boolean { copilotSessionLauncher.ts ×3
221 > return getCopilotSdkErrorCode(err) === -32603 && /\bCustom agent '.+' not found\b/i.test(getErrorMessage(err));
222 > }
224 > /**
225 > * Resolves the reasoning effort: a recognized override level wins over the
226 > * model picker's thinking level; an unrecognized override is ignored (degrades
227 > * to the picker). Validation is against the known effort levels only — the
228 > * caller/operator is responsible for choosing a level the model supports.
229 > */
230 > export function getCopilotReasoningEffort(model: ModelSelection | undefined, effortOverride?: string): SessionConfig['reasoningEffort'] {
231 > if (isReasoningEffort(effortOverride)) { copilotSessionLauncher.ts ×3
232 > return effortOverride; copilotSessionLauncher.ts ×1
233 > }
234 > const thinkingLevel = model?.config?.[ThinkingLevelConfigKey]; copilotSessionLauncher.ts ×3
235 > return isReasoningEffort(thinkingLevel) ? thinkingLevel : undefined;
236 > }
238 > /**
239 > * Resolves the reasoning effort, applying the host-level override and logging
240 > * whether it applied. Shared by the launcher (create) and
241 > * `CopilotAgent._changeModel` (mid-session model change) for consistency.
242 > */
243 > export function resolveCopilotReasoningEffort(model: ModelSelection | undefined, configurationService: IAgentConfigurationService, logService: ILogService, sessionId: string): SessionConfig['reasoningEffort'] {
244 > const rawOverride = configurationService.getRootValue(copilotCliConfigSchema, CopilotCliConfigKey.ReasoningEffortOverride); copilotSessionLauncher.ts ×7
245 > // '' is the schema's unset marker, so an unset override reads as `undefined`.
246 > const override = rawOverride ? rawOverride : undefined;
247 > if (override !== undefined) {
248 if (isReasoningEffort(override)) {
249 logService.info(`[Copilot:${sessionId}] Applying reasoning-effort override '${override}'`);
250 } else {
251 logService.warn(`[Copilot:${sessionId}] Ignoring invalid reasoning-effort override '${override}'; expected one of [${ReasoningEfforts.join(', ')}]`);
252 }
253 }
254 > return getCopilotReasoningEffort(model, override); copilotSessionLauncher.ts ×7
255 > }
257 > export function getCopilotContextTier(model: ModelSelection | undefined, longContextWindow?: number, freeLongContext?: boolean): SessionConfig['contextTier'] {
258 > // Legacy persisted selections stored the resolved tier string directly under the deprecated key. copilotSessionLauncher.ts ×7
259 > const legacyTier = model?.config?.[ContextTierConfigKey];
260 > if (isContextTier(legacyTier)) {
261 > return legacyTier; copilotSessionLauncher.ts ×1
262 > }
263 > // The "Context Size" picker exposes numeric token-count enum values, so a current selection arrives copilotSessionLauncher.ts ×1
264 > // under `contextSize` as a token count. Map it to the SDK's two-valued tier using the model's
265 > // long-context window: only a selection that reaches that window opts into `long_context`. Without
266 > // the window (model exposes no picker, or the model list isn't loaded) leave the SDK on its default
267 > // tier.
268 > const contextSize = model?.config?.[ContextSizeConfigKey];
269 > if (contextSize === undefined) { copilotSessionLauncher.ts ×7
270 > // When the model's long-context tier costs the same as the default tier, copilotSessionLauncher.ts ×1
271 > // always opt into long_context — no picker is shown and the user gets the
272 > // larger window for free.
273 > return freeLongContext ? 'long_context' : undefined;
274 > }
275 > const selectedWindow = Number(contextSize); copilotSessionLauncher.ts ×1
276 > if (!Number.isFinite(selectedWindow) || typeof longContextWindow !== 'number') { copilotSessionLauncher.ts ×7
277 > return undefined; copilotSessionLauncher.ts ×1
278 > }
279 > return selectedWindow >= longContextWindow ? 'long_context' : 'default'; copilotSessionLauncher.ts ×7
280 > }
282 > /**
283 > * Resolve the BYOK provider/model session config for `sessionId` from the
284 > * renderer's active bridge. Returns empty — the session launches without BYOK
285 > * models — when BYOK is gated off (no active bridge), when the renderer reports
286 > * no BYOK models, or when enumeration fails; `startProxy` is invoked only once
287 > * at least one model is present.
288 > *
289 > * Each vendor maps to one `type: 'openai'` / `wireApi: 'completions'` provider
290 > * whose `baseUrl` points at the proxy and authenticates with the session-scoped
291 > * `Bearer <nonce>.<sessionId>`; each model is surfaced under the
292 > * provider-qualified selection id `vendor/id`, matching what the renderer's
293 > * `AgentHostByokLmHandler` resolves.
294 > *
295 > * Extracted from {@link CopilotSessionLauncher} so the synthesis and gating are
296 > * unit-testable without instantiating the launcher; the launcher passes a
297 > * `startProxy` thunk that memoizes the single shared proxy handle.
298 > */
299 > export async function resolveByokSessionConfig( copilotSessionLauncher.ts ×2
300 > sessionId: string,
301 > bridgeRegistry: IByokLmBridgeRegistry,
302 > startProxy: () => Promise<IByokLmProxyHandle>,
303 > logService: ILogService,
304 > ): Promise<{ providers?: NamedProviderConfig[]; models?: ProviderModelConfig[] }> {
305 > // Surface the serving window's BYOK models. The registry does not union
306 > // windows' model sets — all serving windows expose the same set, so it picks
307 > // one (see `IByokLmBridgeRegistry`) and the proxy routes inference there.
308 > let byokModels: IByokLmModelInfo[];
309 > try {
310 > byokModels = [...bridgeRegistry.getModels()];
311 > } catch (err) {
312 logService.warn(`[Copilot:${sessionId}] Failed to enumerate BYOK models from renderer bridges`, err);
313 return {};
314 }
315 > if (byokModels.length === 0) { copilotSessionLauncher.ts ×2
317 > }
318 > // Deduplicate by selection id (`vendor/id`). The same BYOK model can be copilotSessionLauncher.ts ×3
319 > // reported more than once — e.g. when two renderer bridges are transiently
320 > // serving during a window hand-off (continuing a chat into a new session) —
321 > // and the runtime rejects a session config with duplicate BYOK model
322 > // selection ids ("Duplicate BYOK model selection id ...").
323 > const seenSelectionIds = new Set<string>();
324 > byokModels = byokModels.filter(m => {
325 > const selectionId = `${m.vendor}/${m.id}`;
326 > if (seenSelectionIds.has(selectionId)) {
327 return false;
328 }
329 > seenSelectionIds.add(selectionId); copilotSessionLauncher.ts ×3
330 > return true;
331 > });
332 > // `startProxy` binds a local loopback listener — unlikely to fail, but it
333 > // must never break session materialization (which fires the cross-window
334 > // `sessionAdded` broadcast). Degrade to no BYOK config on failure.
335 > let handle: IByokLmProxyHandle;
336 > try {
337 > handle = await startProxy();
338 > } catch (err) {
339 logService.warn(`[Copilot:${sessionId}] Failed to start BYOK loopback proxy`, err);
340 return {};
341 }
342 > const providers: NamedProviderConfig[] = [...new Set(byokModels.map(m => m.vendor))].map(vendor => ({ copilotSessionLauncher.ts ×3
343 > name: vendor,
344 > type: 'openai',
345 > wireApi: 'completions',
346 > baseUrl: handle.providerBaseUrl(vendor),
347 > bearerToken: `${handle.nonce}.${sessionId}`,
348 > }));
349 > const models: ProviderModelConfig[] = byokModels.map(m => ({
350 > id: m.id,
351 > provider: m.vendor,
352 > ...(m.name !== undefined ? { name: m.name } : {}),
353 > ...(m.maxContextWindowTokens !== undefined ? { maxContextWindowTokens: m.maxContextWindowTokens } : {}),
354 > }));
355 > logService.info(`[Copilot:${sessionId}] Wired ${models.length} BYOK model(s) across ${providers.length} provider(s) via loopback proxy ${handle.baseUrl}`);
356 > return { providers, models };
357 > }
359 > export class CopilotSessionLauncher implements ICopilotSessionLauncher {
360 >
361 > /**
362 > * Memoized handle for the single shared BYOK loopback proxy, started lazily
363 > * on the first session launch that surfaces BYOK models (see
364 > * {@link _resolveByokSessionConfig}). Held as a promise so concurrent
365 > * launches share one bind. Released and cleared by
366 > * {@link disposeByokProxyHandle} when the owning Copilot client/runtime is
367 > * stopped, so the next start mints a fresh nonce.
368 > */
369 > private _byokProxyHandle: Promise<IByokLmProxyHandle> | undefined;
370 >
371 > constructor(
372 > @IAgentConfigurationService private readonly _configurationService: IAgentConfigurationService, copilotSessionLauncher.ts ×3
373 > @IAgentHostTerminalManager private readonly _terminalManager: IAgentHostTerminalManager,
374 > @ILogService private readonly _logService: ILogService,
375 > @IFileService private readonly _fileService: IFileService,
376 > @IByokLmProxyService private readonly _byokLmProxyService: IByokLmProxyService,
377 > @IByokLmBridgeRegistry private readonly _byokLmBridgeRegistry: IByokLmBridgeRegistry,
378 > ) { }
380 > async launch(plan: CopilotSessionLaunchPlan, runtime: ICopilotSessionRuntime): Promise<CopilotSessionWrapper> {
381 > const config = await this._buildSessionConfig(plan, runtime); copilotSessionLauncher.ts ×9
382 > const sandboxConfig = this._computeSandboxConfig();
383 > if (plan.kind === 'create') {
384 > return this._createSession(plan, config, sandboxConfig); copilotSessionLauncher.ts ×1
385 > }
387 > let fallbackPlan = plan;
388 > let fallbackConfig = config;
389 > try {
390 > const stopWatch = new StopWatch();
391 > this._logService.trace(`[Copilot:${plan.sessionId}] Calling SDK resumeSession...`);
392 > const raw = await plan.client.resumeSession(plan.sessionId, config);
393 > this._logService.trace(`[Copilot:${plan.sessionId}] SDK resumeSession succeeded after ${stopWatch.elapsed()}ms`); copilotSessionLauncher.ts ×1
394 > await this._applySandboxConfig(raw, sandboxConfig, plan.sessionId);
395 > return new CopilotSessionWrapper(raw);
396 > } catch (err) { copilotSessionLauncher.ts ×2
397 > let resumeError = err; copilotSessionLauncher.ts ×4
398 > const errCode = getCopilotSdkErrorCode(resumeError);
399 > const errMsg = getErrorMessage(resumeError);
400 > this._logService.warn(`[Copilot:${plan.sessionId}] SDK resumeSession failed: code=${errCode}, message=${errMsg}`);
401 > if (plan.resolvedAgentName && isCustomAgentNotFoundError(resumeError)) {
402 > fallbackPlan = { ...plan, resolvedAgentName: undefined }; copilotSessionLauncher.ts ×3
403 > fallbackConfig = { ...config, agent: undefined };
404 > this._logService.warn(`[Copilot:${plan.sessionId}] Stored custom agent '${plan.resolvedAgentName}' was not found; retrying resume without a custom agent`);
405 > try {
406 > const raw = await fallbackPlan.client.resumeSession(fallbackPlan.sessionId, fallbackConfig);
407 > await this._applySandboxConfig(raw, sandboxConfig, plan.sessionId);
408 > return new CopilotSessionWrapper(raw);
409 > } catch (retryErr) {
410 resumeError = retryErr;
411 this._logService.warn(`[Copilot:${plan.sessionId}] SDK resumeSession without custom agent failed: code=${getCopilotSdkErrorCode(retryErr)}, message=${getErrorMessage(retryErr)}`);
412 }
414 > // The SDK fails to resume sessions that have no messages. copilotSessionLauncher.ts ×2
415 > // Fall back to creating a new session with the same ID,
416 > // seeding model & working directory from stored metadata.
417 > if (!shouldCreateEmptySessionAfterResumeError(resumeError)) {
418 > throw resumeError; copilotSessionLauncher.ts ×1
419 > }
421 > this._logService.warn(`[Copilot:${plan.sessionId}] Resume failed (code=-32603), falling back to createSession with same ID`);
422 > const wrapper = await this._createSession({
423 > ...fallbackPlan,
424 > kind: 'create',
425 > model: fallbackPlan.fallback.model,
426 > longContextWindow: fallbackPlan.fallback.longContextWindow,
427 > freeLongContext: fallbackPlan.fallback.freeLongContext,
428 > }, fallbackConfig, sandboxConfig);
429 > this._logService.info(`[Copilot:${plan.sessionId}] Fallback createSession succeeded`);
430 > return wrapper;
431 > }
434 > private async _createSession(plan: ICopilotCreateSessionLaunchPlan, config: CopilotSessionLaunchConfig, sandboxConfig: ISdkSandboxConfig | undefined): Promise<CopilotSessionWrapper> {
435 > const raw = await plan.client.createSession({ copilotSessionLauncher.ts ×2
436 > ...config,
437 > sessionId: plan.sessionId,
438 > streaming: true,
439 > model: plan.model?.id,
440 > reasoningEffort: resolveCopilotReasoningEffort(plan.model, this._configurationService, this._logService, plan.sessionId),
441 > contextTier: getCopilotContextTier(plan.model, plan.longContextWindow, plan.freeLongContext),
442 > ...(plan.resolvedAgentName ? { agent: plan.resolvedAgentName } : {}),
443 > workingDirectory: plan.workingDirectory?.fsPath,
444 > });
445 > await this._applySandboxConfig(raw, sandboxConfig, plan.sessionId); copilotSessionLauncher.ts ×1
446 > return new CopilotSessionWrapper(raw);
449 > /**
450 > * Compute the SDK-shaped sandbox policy to push to the runtime for the
451 > * SDK's built-in shell tool.
452 > *
453 > * Returns `undefined` when {@link CopilotCliConfigKey.EnableCustomTerminalTool}
454 > * is ON — in that case the AgentHost provides its own shell tools, which
455 > * wrap commands via the host terminal sandbox engine, so no SDK-side
456 > * sandbox policy is needed. Otherwise the policy is derived from the
457 > * host's `sandbox` config bag (forwarded from the workbench's
458 > * `chat.agent.sandbox.*` settings), mirroring what
459 > * `buildSandboxConfigForCLI` does for the Copilot extension's CLI path.
460 > */
461 > private _computeSandboxConfig(): ISdkSandboxConfig | undefined {
462 > const enableCustomTerminalTool = this._configurationService.getRootValue(copilotCliConfigSchema, CopilotCliConfigKey.EnableCustomTerminalTool) === true; copilotSessionLauncher.ts ×9
463 > if (enableCustomTerminalTool) {
464 return undefined;
465 }
466 > return buildSandboxConfigForSdk(process.platform, this._configurationService.getRootValue(sandboxConfigSchema, AgentHostSandboxConfigKey.Sandbox)); copilotSessionLauncher.ts ×9
467 > }
469 > /**
470 > * Forward the SDK-shaped sandbox policy to the runtime via
471 > * `session.options.update`, immediately after the session is created or
472 > * resumed. `SessionUpdateOptionsParams.sandboxConfig` is now typed by the
473 > * SDK (as `SandboxConfig`), and our {@link ISdkSandboxConfig} shape is
474 > * structurally assignable to it, so we forward it directly.
475 > *
476 > * No-op when {@link _computeSandboxConfig} returned `undefined` (custom
477 > * terminal tool enabled, or the host sandbox config evaluates to disabled).
478 > */
479 > private async _applySandboxConfig(session: CopilotSessionWrapper['session'], sandboxConfig: ISdkSandboxConfig | undefined, sessionId: string): Promise<void> {
480 > if (!sandboxConfig) { copilotSessionLauncher.ts ×2
481 > return;
482 > }
483 try {
484 await session.rpc.options.update({ sandboxConfig });
485 this._logService.info(`[Copilot:${sessionId}] Applied SDK sandboxConfig via session.options.update`);
486 } catch (err) {
487 this._logService.warn(`[Copilot:${sessionId}] Failed to apply SDK sandboxConfig`, err);
488 }
491 > /**
492 > * Launcher-bound wrapper over {@link resolveByokSessionConfig}: supplies the
493 > * active bridge registry and a `startProxy` thunk that memoizes the single
494 > * shared proxy handle for this launcher (started lazily on first use).
495 > */
496 > private _resolveByokSessionConfig(sessionId: string): Promise<{ providers?: NamedProviderConfig[]; models?: ProviderModelConfig[] }> {
497 > return resolveByokSessionConfig(sessionId, this._byokLmBridgeRegistry, () => { copilotSessionLauncher.ts ×2
498 > if (!this._byokProxyHandle) { copilotSessionLauncher.ts ×2
499 > this._byokProxyHandle = this._byokLmProxyService.start();
500 > }
501 > return this._byokProxyHandle;
502 > }, this._logService); copilotSessionLauncher.ts ×2
503 > }
505 > /**
506 > * Release the memoized BYOK loopback proxy handle (if any) and clear it so
507 > * the next session launch mints a fresh nonce. Idempotent.
508 > *
509 > * **Ownership invariant.** The caller MUST stop the Copilot client/runtime
510 > * subprocess before invoking this: disposing the handle drops the proxy's
511 > * refcount and may rebind it on a different port/nonce, so a still-running
512 > * subprocess would silently lose its endpoint — see {@link IByokLmProxyHandle}.
513 > * Invoked from `CopilotAgent._stopClient` / `CopilotAgent.shutdown` after the
514 > * client has stopped.
515 > */
516 > async disposeByokProxyHandle(): Promise<void> {
517 > const handle = this._byokProxyHandle; copilotSessionLauncher.ts ×3
518 > this._byokProxyHandle = undefined;
519 > if (!handle) {
520 > return;
521 > }
523 > (await handle).dispose();
524 > } catch {
525 // The lazy `start()` rejected; there is nothing to release.
526 }
529 > private async _buildSessionConfig(plan: CopilotSessionLaunchPlan, runtime: ICopilotSessionRuntime): Promise<CopilotSessionLaunchConfig> {
530 > const plugins = plan.snapshot.plugins; copilotSessionLauncher.ts ×9
531 > // Synthesize BYOK provider/model config (empty when BYOK is gated off or the
532 > // renderer reports no BYOK models), merged into the returned config so both
533 > // createSession and resumeSession advertise the models to the runtime.
534 > const byok = await this._resolveByokSessionConfig(plan.sessionId);
535 > const enableCustomTerminalTool = this._configurationService.getRootValue(copilotCliConfigSchema, CopilotCliConfigKey.EnableCustomTerminalTool) === true;
536 > let shellTools: Awaited<ReturnType<typeof createShellTools>> = [];
537 > if (enableCustomTerminalTool) {
538 if (!plan.shellManager) {
539 throw new Error(`ShellManager is required to launch Copilot session '${plan.sessionId}'`);
540 }
541 shellTools = await createShellTools(plan.shellManager, this._terminalManager, this._logService, request => runtime.requestUnsandboxedCommandConfirmation(request));
542 }
543 > // Rely on the SDK to discover most agents/skills/etc. from `pluginDirectories` copilotSessionLauncher.ts ×9
544 > // instead of feeding them explicitly, to avoid duplicates. Custom agents are the
545 > // exception: the SDK validates the session-start `agent:` against `customAgents`
546 > // by name, so the selected agent is force-included (see `toSdkSessionCustomAgents`).
547 > const pluginsWithoutDirs = plugins.filter(p => !p.pluginDir || p.pluginDir.scheme !== Schemas.file);
548 > const customAgents = await toSdkSessionCustomAgents(plugins, plan.resolvedAgentName, this._fileService);
549 > const skillDirectories = toSdkSkillDirectories(pluginsWithoutDirs.flatMap(p => p.skills));
550 > const instructionDirectories = toSdkInstructionDirectories(plugins.flatMap(p => p.instructions));
551 > const model = plan.kind === 'create' ? plan.model : plan.fallback.model;
552 > const clientToolNames = clientToolNamesFromSnapshot(plan.snapshot);
553 > // Prompt routing and capability decisions use the family-aliased
554 > // selection; the wire model id in _createSession comes from plan.model
555 > // and is unaffected.
556 > const effectiveModel = applyModelFamilyAlias(model, this._configurationService.getRootValue(copilotCliConfigSchema, CopilotCliConfigKey.ModelCapabilityOverrides));
557 > if (model && effectiveModel !== model) {
558 this._logService.info(`[Copilot:${plan.sessionId}] Model capability override: routing prompt for '${model.id}' as family '${effectiveModel?.id}'`);
559 }
560 > const toolSearchActive = this._configurationService.getRootValue(copilotCliConfigSchema, CopilotCliConfigKey.ToolSearchEnabled) === true copilotSessionLauncher.ts ×9
561 && agentHostModelSupportsToolSearch(effectiveModel?.id)
562 && clientToolNames.has(CLIENT_TOOL_SEARCH_REFERENCE_NAME);
563 > const promptContext: IAgentHostPromptContext = { copilotSessionLauncher.ts ×9
564 > getSetting: key => this._configurationService.getRootValue(copilotCliConfigSchema, key),
565 > hasClientTool: name => clientToolNames.has(name),
566 > workspaceless: plan.workspaceless === true,
567 > toolSearchActive,
568 > };
569 > // Resolved once per (re)launch — the SDK has no mid-session system-message
570 > // update, so this reflects the model/tools/settings at launch time. Log a
571 > // summary at info for prompt observability; the full config at trace.
572 > const systemMessage = agentHostPromptRegistry.resolveSystemMessageConfig(effectiveModel, promptContext);
573 > this._logService.info(`[Copilot:${plan.sessionId}] Resolved system message: ${describeSystemMessageConfig(systemMessage)}`);
574 > if (this._logService.getLevel() <= LogLevel.Trace) {
575 // Guarded: a `replace`-mode prompt's content can be multiple KB, so only
576 // serialize it when trace output is actually emitted.
577 this._logService.trace(`[Copilot:${plan.sessionId}] System message config: ${JSON.stringify(systemMessage, (_key, value) => typeof value === 'function' ? '[transform fn]' : value)}`);
578 }
580 > ...byok,
581 > clientName: AGENT_HOST_COPILOT_CLIENT_NAME,
582 > enableMcpApps: true,
583 > enableFileHooks: true,
584 > enableConfigDiscovery: true,
585 > requestExtensions: false, // force-disable copilot extension management tools (otherwise enabled in experimental mode)
586 > onPermissionRequest: request => runtime.handlePermissionRequest(request),
587 > onUserInputRequest: (request, invocation) => runtime.handleUserInputRequest(request, invocation),
588 > onElicitationRequest: context => runtime.handleElicitationRequest(context),
589 > onMcpAuthRequest: (request, context) => runtime.handleMcpAuthRequest(request, context),
590 > hooks: toSdkHooks(pluginsWithoutDirs.flatMap(p => p.hooks), {
591 > onPreToolUse: input => runtime.handlePreToolUse(input),
592 > onPostToolUse: input => runtime.handlePostToolUse(input),
593 > }),
594 > mcpServers: { ...toSdkMcpServersFromConfigMap(plan.snapshot.mcpServers), ...toSdkMcpServers(pluginsWithoutDirs.flatMap(p => p.mcpServers)) },
595 > onExitPlanModeRequest: (request, invocation) => runtime.handleExitPlanModeRequest(request, invocation),
596 > workingDirectory: plan.workingDirectory?.fsPath,
597 > customAgents,
598 > agent: plan.resolvedAgentName,
599 > skillDirectories,
600 > instructionDirectories,
601 > systemMessage,
602 > toolSearch: toolSearchActive ? { enabled: true, deferThreshold: 1 } : { enabled: false },
603 > pluginDirectories: coalesce(plugins.map(p => p.pluginDir))
604 > .filter(d => d.scheme === Schemas.file).map(d => d.fsPath),
605 > tools: [...shellTools, ...runtime.createClientSdkTools(), ...runtime.createServerSdkTools()],
606 > // Pass the GitHub token at the session level. The SDK's
607 > // client-level `gitHubToken` authenticates the CLI process,
608 > // but each session also needs its own token resolved into a
609 > // GitHub identity (login, Copilot plan, endpoints) to drive
610 > // model routing and quota — without this the session
611 > // errors with "Session was not created with authentication
612 > // info or custom provider" on first send. See #318693.
613 > gitHubToken: plan.githubToken,
614 > // Enable infinite sessions so the SDK provisions a workspace
615 > // directory (containing `plan.md`, `checkpoints/`, `files/`).
616 > // The workspace is required for plan mode to work — without
617 > // it, `rpc.plan.read()` returns `path: null` and the SDK
618 > // never emits `exit_plan_mode.requested`.
619 > infiniteSessions: { enabled: true },
620 > // Per-session remote export: the client-level `--remote` flag
621 > // (enableRemoteSessions) enables the CLI capability, but each
622 > // session must opt in via `remoteSession` to actually export
623 > // events. Without this, sessions default to "off".
624 > remoteSession: this._configurationService.getRootValue(platformRootSchema, AgentHostSessionSyncEnabledConfigKey) === true ? 'export' : undefined,
625 > enableManagedSettings: true,
626 > };
627 > }