claudeAgent.ts ×91

Frontier kind: Code frontier

unlabeled · c_57c71dd95ded

200 tests · 53380 LOC · 346 files · introduces 0 tests · 1570 LOC · 5 files

Introduces — evidence that enters the hierarchy at this concept

Code
145 ranges1570 lines · 5 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
4770 ranges53380 lines · 346 files · Browse complete extent
All tests (intent)
200 testsBrowse complete intent

Neighbourhood graph

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

Introduced files, introduced tests, and structurally relevant concept specialization

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

Graph controls are ready.

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

Native relationship evidence

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

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

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

Introduced code

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

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

src/vs/platform/agentHost/node/claude/claudeAgent.ts 831 introduced LOC · 91 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- claudeAgent.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 { CCAModel } from '@vscode/copilot-api';
7 > import type { ModelInfo, OnElicitation, Options, SDKSessionInfo, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
8 > import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
9 > import { SequencerByKey } from '../../../../base/common/async.js';
10 > import { CancellationToken } from '../../../../base/common/cancellation.js';
11 > import { CancellationError } from '../../../../base/common/errors.js';
12 > import { Emitter, Event } from '../../../../base/common/event.js';
13 > import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js';
14 > import { IObservable, observableValue } from '../../../../base/common/observable.js';
15 > import { URI } from '../../../../base/common/uri.js';
16 > import { generateUuid } from '../../../../base/common/uuid.js';
17 > import { localize } from '../../../../nls.js';
18 > import { IInstantiationService } from '../../../instantiation/common/instantiation.js';
19 > import { INativeEnvironmentService } from '../../../environment/common/environment.js';
20 > import { ILogService } from '../../../log/common/log.js';
21 > import { IProductService } from '../../../product/common/productService.js';
22 > import { IAgentPluginManager, ISyncedCustomization } from '../../common/agentPluginManager.js';
23 > import { AgentSessionEntry, buildSideChatSourceContext, decodeProviderData, encodeProviderData, prepareSideChatPrompt, stripSideChatContext, type IPersistedChat } from '../agentPeerChats.js';
24 > import { AgentHostConfigKey, agentHostCustomizationConfigSchema } from '../../common/agentHostCustomizationConfig.js';
25 > import { createSchema, platformSessionSchema, schemaProperty } from '../../common/agentHostSchema.js';
26 > import { ClaudePermissionMode, ClaudeSessionConfigKey, narrowClaudePermissionMode } from '../../common/claudeSessionConfigKeys.js';
27 > import { createClaudeThinkingLevelSchema, isClaudeEffortLevel } from '../../common/claudeModelConfig.js';
28 > import { SessionConfigKey } from '../../common/sessionConfigKeys.js';
29 > import { AgentProvider, AgentSession, AgentSignal, CLAUDE_AGENT_PROVIDER_ID, IActiveClient, IAgent, IAgentChatDataChange, IAgentChats, IAgentCreateChatForkSource, IAgentCreateChatOptions, IAgentCreateChatResult, IAgentCreateSessionConfig, IAgentCreateSessionResult, IAgentDescriptor, IAgentMaterializeSessionEvent, IAgentModelInfo, IAgentResolveSessionConfigParams, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IAgentSessionProjectInfo, IAgentSpawnChatEvent, SubagentChatSignal } from '../../common/agentService.js';
30 > import { ensureWorkspacelessScratchDir } from '../workspacelessScratchDir.js';
31 > import { ActionType, AuthRequiredReason, type AuthRequiredParams } from '../../common/state/sessionActions.js';
32 > import type { ResolveSessionConfigResult, SessionConfigCompletionsResult } from '../../common/state/protocol/commands.js';
33 > import { AHP_AUTH_REQUIRED, ProtocolError } from '../../common/state/sessionProtocol.js';
34 > import { PolicyState, ProtectedResourceMetadata, type AgentSelection, type ModelSelection, type ToolDefinition } from '../../common/state/protocol/state.js';
35 > import { isSubagentSession, parseSubagentSessionUri, buildDefaultChatUri, parseChatUri, parseRequiredSessionUriFromChatUri, isDefaultChatUri, ChatInputResponseKind, type ChatState, type ClientPluginCustomization, type Customization, type MessageAttachment, type PendingMessage, type ChatInputAnswer, type ToolCallResult, type Turn } from '../../common/state/sessionState.js';
36 > import { IAgentConfigurationService } from '../agentConfigurationService.js';
37 > import { IAgentHostGitHubEndpointService } from '../agentHostGitHubEndpointService.js';
38 > import { IAgentHostGitService } from '../../common/agentHostGitService.js';
39 > import { PendingRequestRegistry } from '../../common/pendingRequestRegistry.js';
40 > import { projectFromCopilotContext } from '../copilot/copilotGitProject.js';
41 > import { ICopilotApiService } from '../shared/copilotApiService.js';
42 > import { IClaudeAgentSdkService } from './claudeAgentSdkService.js';
43 > import { buildModelEnumerationOptions } from './claudeSdkOptions.js';
44 > import { mapSessionMessagesToTurns, resolveForkAnchorUuid } from './claudeReplayMapper.js';
45 > import { getSubagentTranscript } from './claudeSubagentResolver.js';
46 > import { ClaudeAgentSession } from './claudeAgentSession.js';
47 > import { handleCanUseTool } from './claudeCanUseTool.js';
48 > import { handleElicitation } from './claudeElicitationBridge.js';
49 > import type { IAgentServerToolHost } from '../../common/agentServerTools.js';
50 > import { createPricingMetaFromBilling, normalizeCAPIBilling } from '../../common/agentModelPricing.js';
51 > import { tryParseClaudeModelId } from './claudeModelId.js';
52 > import { resolvePromptToContentBlocks } from './claudePromptResolver.js';
53 > import { IClaudeProxyHandle, IClaudeProxyService, type ClaudeTransport } from './claudeProxyService.js';
54 > import { readClaudePermissionMode } from './claudeSessionPermissionMode.js';
55 > import { ClaudeSessionMetadataStore, IClaudeSessionOverlay } from './claudeSessionMetadataStore.js';
56 > import { AgentHostStateManager, IAgentHostStateManager } from '../agentHostStateManager.js';
57 >
58 > const USER_AGENT_PREFIX = 'vscode_claude_code';
59 >
60 > /**
61 > * Returns true if `m` is a Claude-family model that should be advertised
62 > * to clients picking a model for the Claude provider.
63 > *
64 > * Combines the same surface checks the extension uses (vendor, picker
65 > * eligibility, tool-call support, `/v1/messages` endpoint) with a parse
66 > * of the model id via {@link tryParseClaudeModelId}, which excludes
67 > * synthetic ids like `auto` that aren't real Claude endpoints.
68 > */
69 function isClaudeModel(m: CCAModel): boolean {
70 return (
76 );
77 }
79 > /**
80 > * Augments the published `@vscode/copilot-api` `CCAModelSupports` with the
81 > * per-model `adaptive_thinking` / `reasoning_effort` fields the runtime
82 > * CAPI `/models` payload already carries but the SDK type doesn't yet
83 > * declare. Tracked at microsoft/vscode-capi#85; remove this when the SDK
84 > * catches up. Mirror of the same pattern at
85 > * `extensions/copilot/src/platform/endpoint/common/endpointProvider.ts`
86 > * (its locally-declared `IChatModelCapabilities`).
87 > */
88 > interface IClaudeModelSupports {
89 > readonly adaptive_thinking?: boolean;
90 > readonly reasoning_effort?: readonly string[];
91 > }
92 >
93 > /**
94 > * Project a {@link CCAModel} into the agent host's
95 > * {@link IAgentModelInfo} surface. The returned `provider` is the
96 > * agent's id (`'claude'`) — clients filter the root state's model list
97 > * by provider, so this must match {@link ClaudeAgent.id}, NOT the
98 > * upstream `vendor: 'Anthropic'` field.
99 > */
100 function toAgentModelInfo(m: CCAModel, provider: AgentProvider): IAgentModelInfo {
101 const supports = m.capabilities?.supports;
124 };
125 }
127 > /**
128 > * Project an SDK {@link ModelInfo} into the agent host's
129 > * {@link IAgentModelInfo} surface for the native (BYO-Anthropic) transport.
130 > * Carries NO commercial metadata (no `policyState`, no pricing `_meta`) —
131 > * those are Copilot/CAPI concepts. Reuses the shared effort-schema helpers so
132 > * the thinking-level picker matches the proxied projection.
133 > */
134 > export function fromSdkModelInfo(m: ModelInfo, provider: AgentProvider): IAgentModelInfo {
135 const supportedEfforts = (m.supportedEffortLevels ?? []).filter(isClaudeEffortLevel);
136 const configSchema = createClaudeThinkingLevelSchema(supportedEfforts);
145 };
146 }
148 > // Single source of truth for narrowing an arbitrary runtime value to
149 > // the closed `ClaudePermissionMode` union now lives in
150 > // `../../common/claudeSessionConfigKeys.ts` so it can be shared by
151 > // `ClaudeAgent`, `ClaudeSessionMetadataStore`, and any other consumer
152 > // that needs the same narrowing semantics. The live per-session read
153 > // helper lives in `./claudeSessionPermissionMode.ts` so the session
154 > // and materializer can read directly without threading callbacks
155 > // through the agent.
156 >
157 > // Provisional session state is hosted directly on {@link ClaudeAgentSession}
158 > // (pre-materialize fields: project, abortController, provisionalModel,
159 > // provisionalConfig). The legacy `IClaudeProvisionalSession` map shape
160 > // was retired in Phase 10.5 Step 3a.
161 >
162 > /**
163 > * Claude active-client handle. Tools read/write through the live session's
164 > * {@link SessionClientToolsModel}; customization assignment kicks off the
165 > * agent's async sync (via the provided closure). The handle caches the last
166 > * assigned customization inputs so the getter reflects what the client most
167 > * recently published.
168 > */
169 > class ClaudeActiveClientHandle implements IActiveClient {
170 > private _customizations: readonly ClientPluginCustomization[] = [];
171 >
172 > constructor(
173 readonly clientId: string,
174 readonly displayName: string | undefined,
177 private readonly _syncCustomizations: (customizations: readonly ClientPluginCustomization[]) => void,
178 ) { }
180 > get tools(): readonly ToolDefinition[] {
181 return this._getTools();
182 }
183 > set tools(tools: readonly ToolDefinition[]) { claudeAgent.ts
184 this._setTools(tools);
185 }
187 > get customizations(): readonly ClientPluginCustomization[] {
188 return this._customizations;
189 }
190 > set customizations(customizations: readonly ClientPluginCustomization[]) { claudeAgent.ts
191 this._customizations = customizations;
192 this._syncCustomizations(customizations);
193 }
194 > } claudeAgent.ts
195 >
196 > /**
197 > * Phase 4 skeleton {@link IAgent} provider for the Claude Agent SDK.
198 > *
199 > * What is implemented:
200 > * - Provider id, descriptor, and protected resources surface so root
201 > * state advertises Claude alongside Copilot CLI.
202 > * - GitHub token capture via {@link authenticate} and lazy acquisition
203 > * of an {@link IClaudeProxyHandle} from {@link IClaudeProxyService}.
204 > * - {@link models} observable derived from {@link ICopilotApiService.models}
205 > * filtered to Claude-family entries via {@link isClaudeModel}.
206 > *
207 > * What is stubbed:
208 > * - All other {@link IAgent} methods throw `Error('TODO: Phase N')`. The
209 > * exact phase numbers reference the roadmap in
210 > * `src/vs/platform/agentHost/node/claude/roadmap.md`.
211 > *
212 > * The class is intentionally lean: each subsequent phase adds one
213 > * concern (sessions, sendMessage, permissions, etc.) so the surface area
214 > * of any single review stays small.
215 > */
216 > export class ClaudeAgent extends Disposable implements IAgent {
217 > readonly id: AgentProvider = CLAUDE_AGENT_PROVIDER_ID;
218 >
219 > private readonly _onDidSessionProgress = this._register(new Emitter<AgentSignal>());
220 > readonly onDidSessionProgress = this._onDidSessionProgress.event;
221 >
222 > private readonly _onDidCustomizationsChange = this._register(new Emitter<void>());
223 > readonly onDidCustomizationsChange = this._onDidCustomizationsChange.event;
224 >
225 > private readonly _onDidRequireAuth = this._register(new Emitter<Omit<AuthRequiredParams, 'channel'>>());
226 > readonly onDidRequireAuth = this._onDidRequireAuth.event;
227 >
228 > private readonly _models = observableValue<readonly IAgentModelInfo[]>(this, []);
229 > readonly models: IObservable<readonly IAgentModelInfo[]> = this._models;
230 > /**
231 > * In-flight {@link refreshModels} call, so overlapping triggers (an auth
232 > * token change, a transport flip, or a periodic tick from the host's
233 > * model-refresh scheduler) collapse into a single enumeration instead of
234 > * racing each other's writes to {@link _models}.
235 > */
236 > private _modelRefreshInFlight: Promise<void> | undefined;
237 >
238 > private _githubToken: string | undefined;
239 > private _proxyHandle: IClaudeProxyHandle | undefined;
240 > private _serverToolHost: IAgentServerToolHost | undefined;
241 >
242 > /**
243 > * Resolved host transport mode (Phase 19). `proxy` (default) routes through
244 > * the Copilot-CAPI proxy; `native` talks to Anthropic directly on the user's
245 > * own credentials. Resolved once from the `ClaudeUseCopilotProxy` root
246 > * config value and kept current by an `onDidRootConfigChange` subscription.
247 > * Config changes affect FUTURE sessions only — never an in-flight subprocess.
248 > */
249 > private _transportMode: 'proxy' | 'native' = 'proxy';
250 >
251 > /**
252 > * Memoized teardown promise. Set on the first call to {@link shutdown},
253 > * returned by every subsequent call. Mirrors `CopilotAgent.shutdown`
254 > * at copilotAgent.ts:1246. Phase 5 has no async work so the race
255 > * is benign, but the contract is locked now so Phase 6's real
256 > * async teardown (Query.interrupt(), in-flight metadata writes)
257 > * cannot regress.
258 > */
259 > private _shutdownPromise: Promise<void> | undefined;
260 >
261 > /**
262 > * Live in-memory session entries, keyed by raw session id (not URI).
263 > * Each {@link ClaudeSessionEntry} owns its {@link ClaudeAgentSession} plus
264 > * any per-session disposables registered against it (e.g. the forward
265 > * subscription to the session's `onDidSessionProgress` event). Disposing
266 > * the map disposes every entry, which in turn disposes everything
267 > * registered to it — no parallel maps, no implicit lockstep invariants.
268 > * {@link createSession} is the only writer; {@link disposeSession} and
269 > * {@link shutdown} remove via {@link DisposableMap.deleteAndDispose}, which
270 > * is idempotent if the key has already been removed.
271 > */
272 > private readonly _sessions = this._register(new DisposableMap<string, ClaudeSessionEntry>());
273 >
274 > /**
275 > * Live, in-memory peer-chat backings keyed by the chat's `ahp-chat` channel
276 > * URI string. Populated by {@link createChat} on creation and by
277 > * {@link materializeChat} on session restore (decoding the opaque
278 > * `providerData` the orchestrator persisted). This is the live source of the
279 > * `chatUri → sdkSessionId` mapping.
280 > */
281 > private readonly _chatBackings = new Map<string, IPersistedChat>();
282 >
283 > /**
284 > * Fires when a peer chat's opaque `providerData` blob changes after creation
285 > * (e.g. a per-chat model switch) so the orchestrator can re-persist the
286 > * refreshed token. See {@link IAgent.onDidChangeChatData}.
287 > */
288 > private readonly _onDidChangeChatData = this._register(new Emitter<IAgentChatDataChange>());
289 > readonly onDidChangeChatData: Event<IAgentChatDataChange> = this._onDidChangeChatData.event;
290 >
291 > /**
292 > * Membership channel for chats the agent spawns itself — today the
293 > * sub-agent chats delegated by a `Task`/`Agent` tool call (and, when the
294 > * harness gains them, Claude Teams teammates). Derived from the
295 > * `subagent_started` / `subagent_completed` signals that already flow on
296 > * {@link onDidSessionProgress}, so the orchestrator records the spawn edge
297 > * on the unified chat catalog. See {@link IAgent.onDidSpawnChat}.
298 > */
299 > private readonly _onDidSpawnChat = this._register(new Emitter<IAgentSpawnChatEvent>());
300 > readonly onDidSpawnChat: Event<IAgentSpawnChatEvent> = this._onDidSpawnChat.event;
301 >
302 > /** Stable active-client handles, keyed by `${sessionId}\0${clientId}`. */
303 > private readonly _activeClientHandles = new Map<string, ClaudeActiveClientHandle>();
304 >
305 > /**
306 > * Phase 6: fired once per session when {@link _materializeProvisional}
307 > * promotes a provisional record into a real {@link ClaudeAgentSession}.
308 > * The {@link IAgentService} subscribes via the platform contract
309 > * (`agentService.ts:412`) to dispatch the deferred `sessionAdded`
310 > * notification — observers don't see the session in their list until
311 > * persistence has settled.
312 > */
313 > private readonly _onDidMaterializeSession = this._register(new Emitter<IAgentMaterializeSessionEvent>());
314 > readonly onDidMaterializeSession = this._onDidMaterializeSession.event;
315 >
316 > /**
317 > * Per-session-id serializer shared by {@link disposeSession} and
318 > * {@link shutdown}. Phase 5 dispose work is synchronous, so the queued
319 > * tasks resolve immediately and the sequencer is mostly a no-op. The
320 > * routing is locked in now (per plan section 3.3.4 / section 3.3.6) so
321 > * Phase 6's real async teardown (`Query.interrupt()`, in-flight metadata
322 > * writes) inherits per-session serialization for free — a concurrent
323 > * `disposeSession(uri)` already in flight is awaited before
324 > * `shutdown()` reuses the same key.
325 > */
326 > private readonly _disposeSequencer = new SequencerByKey<string>();
327 >
328 > /**
329 > * Phase 6: per-session-id serializer for {@link sendMessage}. Held
330 > * across both {@link _materializeProvisional} AND `entry.send()` so
331 > * two concurrent first-message calls on the same session collapse
332 > * into one materialize plus two ordered sends. Separate from
333 > * {@link _disposeSequencer} so a `disposeSession` racing a first send
334 > * still serializes against in-flight teardown without deadlocking
335 > * inside the send sequencer (different key spaces, single
336 > * race-resolution lattice via the underlying `AbortController`).
337 > */
338 > private readonly _sessionSequencer = new SequencerByKey<string>();
339 >
340 > private readonly _metadataStore: ClaudeSessionMetadataStore;
341 >
342 > /**
343 > * Unified per-session lookup. Returns the session's default chat whether it
344 > * is still provisional or already materialized; callers branch on
345 > * {@link ClaudeAgentSession.isPipelineReady} when behavior differs.
346 > */
347 > private _findAnySession(sessionId: string): ClaudeAgentSession | undefined {
348 > return this._sessions.get(sessionId)?.defaultChat;
349 > }
350 >
351 > /**
352 > * Resolve the live {@link ClaudeAgentSession} for a chat — the session's
353 > * default (main) chat, or an additional peer chat addressed by its
354 > * `ahp-chat` channel URI — via a single uniform lookup in the owning
355 > * session's chat map. Returns `undefined` when the session (or the chat) is
356 > * not in memory.
357 > */
358 > private _findChat(session: URI, chat: URI | undefined): ClaudeAgentSession | undefined {
359 const entry = this._sessions.get(AgentSession.id(session));
360 if (!entry) {
363 return entry.getChat((chat ?? URI.parse(buildDefaultChatUri(session))).toString());
364 }
366 > private _getChatContext(chatOrSession: URI): { session: URI; sessionId: string; chatKey: string; target: ClaudeAgentSession | undefined; isPeerChat: boolean } {
367 // Accept either a chat channel URI or a bare session URI: per the AHP
368 // convention the default chat's URI equals the session URI, so callers
382 };
383 }
385 > /**
386 > * Resolve a live {@link ClaudeAgentSession} by its SDK chat id,
387 > * searching every session entry's default chat and its peer chats. Used by
388 > * SDK-id-addressed callbacks — proxy credit reports and the `canUseTool`
389 > * permission bridge — which carry the SDK session id, not the chat URI.
390 > */
391 > private _findSessionBySdkId(sdkSessionId: string): ClaudeAgentSession | undefined {
392 for (const entry of this._sessions.values()) {
393 for (const chat of entry.allChatSessions()) {
399 return undefined;
400 }
402 > /** Wrap a {@link ClaudeAgentSession} in a chat-leaf entry and forward its events. */
403 > private _wireEntry(session: ClaudeAgentSession): ClaudeSessionEntry {
404 const entry = new ClaudeSessionEntry(session);
405 entry.addDisposable(session.onDidSessionProgress(signal => {
410 return entry;
411 }
413 > /**
414 > * Create a session container seeding its default (main) chat as the first
415 > * entry in the uniform chat map, keyed by the session's default-chat URI.
416 > */
417 > private _seedSessionEntry(sessionId: string, session: URI, mainSession: ClaudeAgentSession): ClaudeSessionEntry {
418 const container = new ClaudeSessionEntry();
419 container.setDefaultChat(buildDefaultChatUri(session), this._wireEntry(mainSession));
421 return container;
422 }
424 > /**
425 > * Bridges the agent's `subagent_started` signal onto the
426 > * {@link onDidSpawnChat} membership channel. The signals are still forwarded
427 > * verbatim on {@link onDidSessionProgress} (the orchestrator's
428 > * `AgentSideEffects` keeps driving the sub-agent turn + parent tool-call
429 > * content); this event only mirrors the spawn into the unified chat catalog.
430 > * A completed subagent chat stays live and subscribable (it is removed only
431 > * on session teardown), so there is no corresponding end event. The catalog
432 > * add is idempotent so the overlap with the orchestrator's own membership
433 > * sequencing is safe.
434 > */
435 > private _emitSpawnedChatEvents(signal: AgentSignal): void {
436 const spawn = SubagentChatSignal.toSpawnEvent(signal);
437 if (spawn) {
439 }
440 }
442 > constructor(
443 @ILogService private readonly _logService: ILogService,
444 @ICopilotApiService private readonly _copilotApiService: ICopilotApiService,
505 }
506 }
508 > private _resolveTransportMode(): 'proxy' | 'native' {
509 // Defaults to proxied when the `claudeUseCopilotProxy` root value is unset.
510 const useProxy = this._configurationService.getRootValue(agentHostCustomizationConfigSchema, AgentHostConfigKey.ClaudeUseCopilotProxy) ?? true;
511 return useProxy ? 'proxy' : 'native';
512 }
514 > // #region Descriptor + auth
515 >
516 > getDescriptor(): IAgentDescriptor {
517 return {
518 provider: this.id,
522 };
523 }
525 > getProtectedResources(): ProtectedResourceMetadata[] {
526 // Native (BYO-Anthropic) mode needs no GitHub Copilot auth — the SDK owns
527 // the Anthropic credential — so the required Copilot resource is dropped.
535 ];
536 }
538 > /**
539 > * Resolve the active {@link ClaudeTransport}. In native mode the transport
540 > * is always ready (the SDK owns credentials); in proxied mode a started
541 > * proxy handle is required, otherwise {@link AHP_AUTH_REQUIRED} is thrown.
542 > */
543 > private _ensureAuthenticated(): ClaudeTransport {
544 if (this._transportMode !== 'proxy') {
545 return { kind: 'native' };
555 return { kind: 'proxy', handle };
556 }
558 > async authenticate(resource: string, token: string): Promise<boolean> {
559 if (resource === this._gitHubEndpointService.getRepoResource().resource) {
560 return true;
602 return true;
603 }
605 > /**
606 > * Whether the Claude provider routes through the Copilot-CAPI proxy.
607 > * Reads the resolved {@link _transportMode} (Phase 19), which the
608 > * constructor seeds from the `ClaudeUseCopilotProxy` root config value.
609 > */
610 > private _isProxyEnabled(): boolean {
611 return this._transportMode === 'proxy';
612 }
614 > /**
615 > * {@link IAgent.refreshModels}. Coalesces onto an in-flight refresh and
616 > * never rejects — {@link _refreshModels} already logs and handles failure.
617 > *
618 > * Only safe for callers with no new input to apply (the host's periodic
619 > * scheduler). Triggers that invalidate the in-flight request — a rotated
620 > * token, a transport flip — must call {@link _startModelRefresh} so they
621 > * are not answered by a refresh bound to the superseded input.
622 > */
623 > refreshModels(): Promise<void> {
624 return this._modelRefreshInFlight ?? this._startModelRefresh();
625 }
627 > /**
628 > * Unconditionally begins a refresh, superseding any in-flight one as the
629 > * coalescing target. The superseded request stays harmless: its own
630 > * stale-write guard drops the result if the token or transport moved on.
631 > */
632 > private _startModelRefresh(): Promise<void> {
633 const refresh = this._refreshModels().finally(() => {
634 if (this._modelRefreshInFlight === refresh) {
639 return refresh;
640 }
642 > private async _refreshModels(): Promise<void> {
643 const proxyAtStart = this._isProxyEnabled();
644 const tokenAtStart = this._githubToken;
667 }
668 }
670 > /**
671 > * Native (BYO-Anthropic) model source: enumerate the SDK's built-in /
672 > * subscription models by opening a throwaway {@link IClaudeAgentSdkService.query}
673 > * (workspace-free options that read the user's real `~/.claude` config) and
674 > * calling `Query.supportedModels()` on it, then `close()`. The prompt never
675 > * yields, so no turn runs and no session transcript is written (verified
676 > * Phase 19 E2E). Projected with no commercial metadata.
677 > */
678 > private async _fetchNativeModels(): Promise<readonly IAgentModelInfo[]> {
679 // A prompt iterable that never yields: enumeration only needs the
680 // control-request channel (`Query.supportedModels()`), not a real turn.
694 }
695 }
697 > /**
698 > * Proxied (Copilot-CAPI) model source: fetch via {@link ICopilotApiService},
699 > * keep the Claude family, and surface the CAPI-flagged chat-default first.
700 > * The picker treats `models[0]` as the de facto default (modelPicker.ts:144
701 > * — `_selectedModel ?? models[0]`) since `IAgentModelInfo` carries no
702 > * explicit `isDefault` bit; the stable comparator returns 0 for equal-
703 > * priority models so CAPI's ordering wins on ties.
704 > */
705 > private async _fetchProxyModels(token: string): Promise<readonly IAgentModelInfo[]> {
706 const userAgent = `${USER_AGENT_PREFIX}/${this._productService.version}`;
707 const all = await this._copilotApiService.models(token, { headers: { 'User-Agent': userAgent }, suppressIntegrationId: true });
711 .map(m => toAgentModelInfo(m, this.id));
712 }
714 > // #endregion
715 >
716 > // #region Stubs — implemented in later phases
717 >
718 > async createSession(config: IAgentCreateSessionConfig = {}): Promise<IAgentCreateSessionResult> {
719 this._ensureAuthenticated();
720 if (config.fork) {
779 };
780 }
782 > /**
783 > * Seed the eagerly-claimed active client (tools + customizations) into the
784 > * SDK at session creation, mirroring the Copilot agent. Runs for fresh AND
785 > * reconnected sessions: when the workbench session state already carries the
786 > * active client, no follow-up `session/activeClientSet` is dispatched to
787 > * trigger the customization sync, so the built-in skills bundle would never
788 > * reach Claude otherwise. Progress is suppressed (`quiet`) because the AH
789 > * service has not created the session state yet — a
790 > * `SessionCustomizationUpdated` envelope would be orphaned; the completed
791 > * snapshot is provided via `getSessionCustomizations` immediately after.
792 > */
793 > private async _seedEagerActiveClient(sessionUri: URI, activeClient: IAgentCreateSessionConfig['activeClient']): Promise<void> {
794 if (!activeClient) {
795 return;
801 }
802 }
804 > /**
805 > * In-place "Restore Checkpoint" truncation. Keeps turns
806 > * `[0..turnId]` INCLUSIVE (or removes all turns when `turnId` is
807 > * omitted) on the **same** session id / URI — unlike fork, which mints a
808 > * new id. The `turnId` path resolves the protocol turn to its SDK
809 > * assistant-envelope uuid ({@link resolveForkAnchorUuid}) and stages it
810 > * as a one-shot `resumeSessionAt` anchor that the next turn's rebuild
811 > * applies (the truncation finalizes when the next turn writes the
812 > * branch). Serialized on {@link _sessionSequencer} (same key as
813 > * `sendMessage`) so the `ChatTruncated` → `ChatTurnStarted` dispatch pair
814 > * stays ordered. Provisional sessions short-circuit.
815 > */
816 > async truncateSession(session: URI, turnId?: string): Promise<void> {
817 const sessionId = AgentSession.id(session);
818 await this._sessionSequencer.queue(sessionId, async () => {
842 });
843 }
845 > /**
846 > * Remove-all ("start over") branch of {@link truncateSession}: there is no
847 > * anchor to resume at, so tear down the live Query, delete the on-disk
848 > * transcript via the SDK, then recreate a fresh provisional under the SAME
849 > * id/URI so the next `sendMessage` materializes non-resume `{ sessionId }`
850 > * on a clean transcript (keeps the id stable). `deleteSession` is eagerly
851 > * durable (unlike the lazy `turnId` path), matching its "clear / start
852 > * over" semantic. `existing` is the live session, or `undefined` on the
853 > * cold path (unloaded session). Caller serializes on {@link _sessionSequencer}.
854 > */
855 > private async _removeAllTurns(session: URI, sessionId: string, existing: ClaudeAgentSession | undefined): Promise<void> {
856 const info = existing ? undefined : await this._sdkService.getSessionInfo(sessionId);
857 const workingDirectory = existing?.workingDirectory ?? (info?.cwd ? URI.file(info.cwd) : undefined);
891 this._logService.info(`[Claude:${sessionId}] truncateSession removed all turns (deleteSession + fresh same-id)`);
892 }
894 > // ---- Chat surface ------------------------------------------------------
895 > //
896 > // `chats` exposes the per-chat operations addressed by a single,
897 > // concrete chat channel URI (the default chat channel or a peer/subagent
898 > // URI). The default chat's SDK id is still the owning session id, derived
899 > // inside the harness from the chat URI.
900 >
901 > /**
902 > * The chat-addressed operation surface
903 > * ({@link IAgentChats}). Every method addresses a chat by a single,
904 > * already-resolved chat URI; this maps to the `(session, chat)` pair
905 > * the agent's internal SDK storage is keyed by (via
906 > * {@link _resolveChatTarget}).
907 > */
908 > readonly chats: IAgentChats = {
909 > createChat: (chat, options) => this._createChat(chat, options),
910 > fork: (chat, source: IAgentCreateChatForkSource, options?: IAgentCreateChatOptions) =>
911 this._createChat(chat, { ...options, fork: source }),
912 > disposeChat: chatUri => { claudeAgent.ts
913 const { session, chat } = this._resolveChatTarget(chatUri);
914 return this._disposeChat(session, chat);
915 },
916 > sendMessage: (chatUri, prompt, workingDirectory, attachments, turnId, senderClientId) => { claudeAgent.ts
917 return this._sendMessage(chatUri, prompt, workingDirectory, attachments, turnId, senderClientId);
918 },
919 > abort: chatUri => { claudeAgent.ts
920 return this._abortSession(chatUri);
921 },
922 > changeModel: (chatUri, model) => { claudeAgent.ts
923 return this._changeModel(chatUri, model);
924 },
925 > changeAgent: (chatUri, agent) => { claudeAgent.ts
926 return this._changeAgent(chatUri, agent);
927 },
928 > getMessages: chat => this.getSessionMessages(chat), claudeAgent.ts
929 > };
930 >
931 > /**
932 > * Map an already-resolved chat URI to the `(session, chat)` pair the agent's
933 > * internal SDK storage is keyed by. A peer (or subagent) chat is addressed by
934 > * its own `ahp-chat` channel URI, from which the owning session is recovered.
935 > * The default chat is addressed by its deterministic chat channel URI.
936 > */
937 > private _resolveChatTarget(chat: URI): { session: URI; chat: URI } {
938 const parsed = parseChatUri(chat);
939 if (!parsed) {
942 return { session: URI.parse(parsed.session), chat };
943 }
945 > /**
946 > * NOT started here (CONTEXT M9): `forkSession` writes the transcript to
947 > * disk and we return; the `Query` materializes lazily on the first
948 > * {@link sendMessage} via {@link _resumeSession}. `turnId` is translated
949 > * to the SDK envelope `uuid` by {@link resolveForkAnchorUuid};
950 > * `config.fork.turnIdMapping` is ignored (the SDK already remaps uuids).
951 > */
952 > private async _forkSession(config: IAgentCreateSessionConfig, fork: NonNullable<IAgentCreateSessionConfig['fork']>): Promise<IAgentCreateSessionResult> {
953 if (isSubagentSession(fork.session)) {
954 throw new Error('Cannot fork a subagent session');
1010 });
1011 }
1013 > /**
1014 > * Builds the SDK `canUseTool` permission bridge for a session/chat. The
1015 > * resolver searches both default chats and peer chats by SDK id so a peer
1016 > * chat's tool-permission requests reach its own pending-permission registry.
1017 > */
1018 > private _makeCanUseTool(sdkSessionId: string): NonNullable<Options['canUseTool']> {
1019 return (toolName, input, options) =>
1020 handleCanUseTool(
1023 );
1024 }
1026 > /**
1027 > * Builds the SDK `onElicitation` bridge for a session/chat. Mirrors
1028 > * {@link _makeCanUseTool}: resolves the session by SDK id (default and peer
1029 > * chats) and delegates to the elicitation bridge, which parks on the
1030 > * session's user-input channel. Phase 10.6.
1031 > */
1032 > private _makeOnElicitation(sdkSessionId: string): OnElicitation {
1033 return (request, options) =>
1034 handleElicitation(
1037 );
1038 }
1040 > /**
1041 > * Promote a provisional {@link ClaudeAgentSession} into a live one.
1042 > * Called from {@link sendMessage} inside the {@link _sessionSequencer.queue}
1043 > * block, so concurrent first sends serialize naturally — exactly
1044 > * one materialize per session.
1045 > *
1046 > * Failure modes:
1047 > * - Missing session entry → programmer error, throws.
1048 > * - Missing proxy handle → caller forgot {@link authenticate}, throws.
1049 > * - Aborted before SDK init returns → {@link ClaudeAgentSession.materialize}
1050 > * disposes the `WarmQuery` and throws {@link CancellationError}.
1051 > * - Customization-directory persistence failure → fatal: the session's
1052 > * `materialize` throws, the agent drops the entry, and the error
1053 > * propagates so the caller learns about it.
1054 > * - Aborted post-metadata-write but pre-commit → second abort gate
1055 > * inside `materialize` throws so we never expose a live pipeline
1056 > * for a session the caller has already torn down.
1057 > */
1058 > private async _materializeProvisional(sessionId: string, workingDirectory?: URI): Promise<ClaudeAgentSession> {
1059 const session = this._findAnySession(sessionId);
1060 if (!session) {
1080 return session;
1081 }
1083 > /**
1084 > * Bring up a session whose state exists only on disk — created in
1085 > * another window, or before an agent-host restart. Mirror of
1086 > * `CopilotAgent._resumeSession`. Reads `workingDirectory` from the
1087 > * SDK's session record and `model` / `permissionMode` from the
1088 > * metadata overlay, constructs a provisional {@link ClaudeAgentSession},
1089 > * and calls {@link ClaudeAgentSession.materialize} with `isResume: true`
1090 > * so the SDK reloads the existing transcript instead of minting a
1091 > * fresh one.
1092 > *
1093 > * Caller must hold the session sequencer so two concurrent
1094 > * `sendMessage` calls for a freshly-resumed session collapse into
1095 > * one resume + two ordered sends.
1096 > */
1097 > private async _resumeSession(sessionId: string, sessionUri: URI): Promise<ClaudeAgentSession> {
1098 this._logService.info(`[Claude:${sessionId}] _resumeSession — no in-memory state, rebuilding from disk`);
1099 const transport = this._ensureAuthenticated();
1155 return session;
1156 }
1158 > /**
1159 > * Pull `permissionMode` out of the post-validation `IAgentCreateSessionConfig.config`
1160 > * bag, narrowing the runtime `unknown` value to the SDK's `PermissionMode`
1161 > * union (5/6 values, excluding `dontAsk`; sdk.d.ts:1560). Falls back to
1162 > * `'default'` when the bag is absent or carries something the schema
1163 > * validator shouldn't have accepted (defense-in-depth).
1164 > */
1165 > private _resolvePermissionMode(config: Record<string, unknown> | undefined): ClaudePermissionMode {
1166 return narrowClaudePermissionMode(config?.[ClaudeSessionConfigKey.PermissionMode]) ?? 'default';
1167 }
1169 > disposeSession(session: URI): Promise<void> {
1170 // Routed through {@link _disposeSequencer} so a concurrent
1171 // {@link shutdown} already serializing teardown for this same
1180 });
1181 }
1183 > /**
1184 > * Non-destructive counterpart to {@link disposeSession}: releases the
1185 > * session's in-memory resources — its live SDK subprocess (via the disposed
1186 > * pipeline) and cached entry — but preserves the on-disk session so it can
1187 > * be transparently resumed later via {@link _resumeSession}. Used by
1188 > * idle-session eviction to bound memory in long-lived host processes.
1189 > *
1190 > * No-ops for provisional sessions (never materialized, so nothing on disk to
1191 > * resume from) and for sessions with a turn in flight — tearing the pipeline
1192 > * down mid-turn would abort live work. Shares the same in-memory teardown as
1193 > * {@link disposeSession}; the destructive difference (deleting durable data)
1194 > * lives in the orchestrator, which only invokes it on dispose.
1195 > */
1196 > releaseSession(session: URI): Promise<void> {
1197 const sessionId = AgentSession.id(session);
1198 return this._disposeSequencer.queue(sessionId, async () => {
1218 });
1219 }
1221 > /**
1222 > * Abort and dispose a session entry — its default chat and every peer chat.
1223 > * Each peer teardown serializes on the peer's own {@link _sessionSequencer}
1224 > * key so it waits for any in-flight materialize/send rather than disposing
1225 > * the chat under it.
1226 > */
1227 > private async _teardownEntry(sessionId: string): Promise<void> {
1228 const entry = this._sessions.get(sessionId);
1229 if (!entry) {
1257 }
1258 }
1260 > // #region Multi-chat — additional (non-default) peer chats
1261 >
1262 > /**
1263 > * Create an additional peer chat within an existing session. The new chat
1264 > * is backed by its own SDK chat (a fresh one, or a fork of the
1265 > * source chat at a turn) that shares the parent session's working directory
1266 > * and inherited model / agent / permission-mode parentSession. The backing is
1267 > * recorded in the live {@link _chatBackings} map and returned as an opaque
1268 > * `providerData` blob for the orchestrator to persist; the chat's metadata
1269 > * overlay is seeded so a later lazy resume inherits the parent parentSession. The
1270 > * live {@link ClaudeAgentSession} is built lazily on the chat's first send
1271 > * (mirroring how default sessions materialize lazily).
1272 > */
1273 > private async _createChat(chat: URI, options?: IAgentCreateChatOptions): Promise<IAgentCreateChatResult | void> {
1274 this._ensureAuthenticated();
1275 if (isDefaultChatUri(chat)) {
1338 return result;
1339 }
1341 > /**
1342 > * Dispose an additional peer chat, tearing down its live chat (if
1343 > * any) and dropping its live backing. The default chat cannot be disposed in
1344 > * isolation — it lives and dies with the session.
1345 > *
1346 > * Routed through {@link _sessionSequencer} (keyed on the chat URI) so it
1347 > * waits for any in-flight {@link _materializeChatLocked} or
1348 > * {@link sendMessage} to finish before tearing down — prevents
1349 > * use-after-dispose if a send is concurrently in progress. The durable
1350 > * peer-chat catalog is owned by the orchestrator now, so this only drops the
1351 > * live backing and chat.
1352 > */
1353 > private async _disposeChat(session: URI, chat: URI): Promise<void> {
1354 if (isDefaultChatUri(chat)) {
1355 return;
1374 // resumed again.
1375 }
1377 > /**
1378 > /**
1379 > * Resolve the inherited session settings (working directory, project, model, agent,
1380 > * permission mode) a new or resumed peer chat copies from its parent
1381 > * session. Prefers the live in-memory parent; falls back to the SDK's
1382 > * on-disk session record + metadata overlay for an unloaded parent.
1383 > */
1384 > private async _resolveParentSession(session: URI, parentSessionId: string): Promise<{ workingDirectory: URI; project: IAgentSessionProjectInfo | undefined; model: ModelSelection | undefined; agent: AgentSelection | undefined; permissionMode: ClaudePermissionMode }> {
1385 const parent = this._findAnySession(parentSessionId);
1386 let workingDirectory = parent?.workingDirectory;
1409 return { workingDirectory, project, model: overlay.model, agent: overlay.agent, permissionMode };
1410 }
1412 > /**
1413 > * Fork the source chat's SDK chat at the requested turn into a new
1414 > * chat and return its SDK session id. Returns `undefined` (so the
1415 > * caller creates a fresh chat instead) when the source chat or the
1416 > * fork anchor cannot be resolved.
1417 > */
1418 > private async _forkChat(session: URI, fork: IAgentCreateChatOptions['fork'] & {}): Promise<{ sessionId: string; inheritedTurnCount: number } | undefined> {
1419 const sourceSdkId = await this._resolveChatSdkId(session, fork.source);
1420 if (!sourceSdkId) {
1433 return { sessionId, inheritedTurnCount };
1434 }
1436 > /**
1437 > * Resolve the SDK chat id backing a chat URI — the session's
1438 > * default chat (the parent session's own id) or an additional peer chat
1439 > * (from the in-memory entry, else the live/legacy backing).
1440 > */
1441 > private async _resolveChatSdkId(session: URI, chatUri: URI): Promise<string | undefined> {
1442 if (isDefaultChatUri(chatUri) || chatUri.toString() === session.toString()) {
1443 return AgentSession.id(session);
1449 return this._resolveChatBacking(chatUri)?.sdkSessionId;
1450 }
1452 > private _getSourceChatState(session: URI, chatUri: URI): ChatState | undefined {
1453 if (isDefaultChatUri(chatUri) || chatUri.toString() === session.toString()) {
1454 return this._stateManager.getDefaultChatState(session.toString());
1456 return this._stateManager.getChatState(chatUri.toString());
1457 }
1459 > private _buildSideChatContext(session: URI, chatUri: URI, turnId: string): string | undefined {
1460 const state = this._getSourceChatState(session, chatUri);
1461 if (!state) {
1470 return boundedTurns ? buildSideChatSourceContext(boundedTurns, state.activeTurn?.id === turnId ? state.activeTurn : undefined) : undefined;
1471 }
1473 > /**
1474 > * Resolves the live backing for a peer chat from the in-memory
1475 > * {@link _chatBackings} map. Returns `undefined` for a chat that has not been
1476 > * materialized via {@link materializeChat}.
1477 > */
1478 > private _resolveChatBacking(chat: URI): IPersistedChat | undefined {
1479 return this._chatBackings.get(chat.toString());
1480 }
1482 > /**
1483 > * Return the in-memory entry for a session, creating a provisional (not yet
1484 > * materialized) default chat to host its peer chats if none exists — e.g. a
1485 > * peer chat is sent to after a restart before the default chat is touched.
1486 > * Serialized on the session id so concurrent peer sends share one entry.
1487 > */
1488 > private _ensureSessionEntry(session: URI): Promise<ClaudeSessionEntry> {
1489 const sessionId = AgentSession.id(session);
1490 return this._sessionSequencer.queue(sessionId, async () => {
1511 });
1512 }
1514 > /**
1515 > * Build + materialize the peer chat's live {@link ClaudeAgentSession},
1516 > * resuming its persisted SDK chat when one already exists on disk
1517 > * (forked or restored chats) or starting fresh otherwise. The caller MUST
1518 > * hold the per-chat (`chat.toString()`) {@link _sessionSequencer} lock so
1519 > * concurrent first sends collapse into one materialize and teardown can't
1520 > * race the build.
1521 > */
1522 > private async _materializeChatLocked(session: URI, chat: URI): Promise<ClaudeAgentSession> {
1523 const chatKey = chat.toString();
1524 const entry = await this._ensureSessionEntry(session);
1542 return chatSession;
1543 }
1545 > /**
1546 > * Build a provisional peer-chat {@link ClaudeAgentSession} from its live (or
1547 > * legacy) backing + overlay: its `sessionUri` is the real parent session URI
1548 > * and its `chatChannelUri` is the chat's own channel (never overloaded),
1549 > * backed by the resolved SDK chat id. Registers it on the owning
1550 > * {@link ClaudeSessionEntry}; the caller materializes it.
1551 > */
1552 > private async _buildProvisionalChat(session: URI, chat: URI, entry: ClaudeSessionEntry): Promise<ClaudeAgentSession> {
1553 const info = this._resolveChatBacking(chat);
1554 if (!info) {
1586 return chatSession;
1587 }
1589 > /**
1590 > * Update a peer chat's live backing model and push the refreshed opaque
1591 > * `providerData` blob to the orchestrator (via
1592 > * {@link onDidChangeChatData}) so the durable catalog stays in sync.
1593 > */
1594 > private async _updateChatBackingModel(chat: URI, model: ModelSelection): Promise<void> {
1595 const backing = this._resolveChatBacking(chat);
1596 if (!backing) {
1601 this._onDidChangeChatData.fire({ chat: chat, providerData: encodeProviderData(updated) });
1602 }
1604 > /**
1605 > * Re-attach the in-memory backing for a peer chat on session restore,
1606 > * decoding the opaque `providerData` the orchestrator persisted at creation
1607 > * (or the latest {@link onDidChangeChatData}). After this resolves the
1608 > * chat's backing SDK chat can be resumed lazily on its first send.
1609 > * Best-effort — a corrupt/unknown blob is logged and dropped rather than
1610 > * thrown.
1611 > */
1612 > async materializeChat(chat: URI, providerData: string | undefined): Promise<void> {
1613 if (isDefaultChatUri(chat)) {
1614 return;
1628 this._chatBackings.set(chat.toString(), backing);
1629 }
1631 > // #endregion
1632 >
1633 > /**
1634 > * Test-only accessor for the materialized {@link ClaudeAgentSession}.
1635 > * Phase 6 section 5.1 Test 10 needs to inspect `_isResumed` directly because
1636 > * Phase 6 has no teardown+recreate flow yet to observe its effect
1637 > * (the flag drives `Options.resume = sessionId` in Phase 7+). Marked
1638 > * `ForTesting` so the production surface stays unaware of its
1639 > * existence; the protocol surface (`IAgent`) does not include it.
1640 > */
1641 > getSessionForTesting(session: URI): ClaudeAgentSession | undefined {
1642 const sess = this._sessions.get(AgentSession.id(session))?.defaultChat;
1643 return sess?.isPipelineReady ? sess : undefined;
1644 }
1646 > /**
1647 > * Phase 13 — reconstruct the full turn history from the SDK's on-disk
1648 > * JSONL transcript. Out-of-process: no live `Query` required. Subagent
1649 > * URIs (`<parent>/subagent/<toolCallId>`) throw `TODO: Phase 12` until
1650 > * Phase 12 wires `getSubagentMessages`. Provisional sessions return `[]`.
1651 > * Resilient: any failure (transcript fetch, mapping, backfill) warn-logs
1652 > * and returns `[]` rather than propagating — mirrors `listSessions`.
1653 > */
1654 > async getSessionMessages(session: URI): Promise<readonly Turn[]> {
1655 // Don't trigger a cold SDK download just to reconstruct a transcript
1656 // during restore (the renderer subscribes to the last-active session
1709 return this._reconstructTurns(sessionId, parentSessionUri, sess);
1710 }
1712 > /**
1713 > * Fetch a chat's SDK transcript ({@link sdkSessionId}) and map it to
1714 > * protocol {@link Turn}s routed to {@link routingUri} (the session or chat
1715 > * channel URI). When {@link primeOn} is supplied (the materialized owning
1716 > * session), its subagent registry is primed from the agentId suffixes the
1717 > * SDK encoded in Task tool_result blocks. Resilient: any failure warn-logs
1718 > * and returns `[]` rather than propagating.
1719 > */
1720 > private async _reconstructTurns(sdkSessionId: string, routingUri: URI, primeOn: ClaudeAgentSession | undefined): Promise<readonly Turn[]> {
1721 let messages;
1722 try {
1744 return turns;
1745 }
1747 > async listSessions(): Promise<IAgentSessionMetadata[]> {
1748 // Plan section 3.3.2: SDK is the source of truth; we deliberately do
1749 // NOT filter entries that lack a per-session DB — external Claude Code
1775 return sdkEntries.map(entry => this._metadataStore.project(entry));
1776 }
1778 > /**
1779 > * Phase 6.1 / Cycle D4 — per-session lookup. Mirrors
1780 > * {@link CopilotAgent.getSessionMetadata} but accepts the
1781 > * external-CLI case: a session that exists on disk via the raw
1782 > * Anthropic CLI has no per-session DB, so we MUST NOT gate on the
1783 > * sidecar (the way Copilot's variant does). The SDK is the source
1784 > * of truth for existence.
1785 > *
1786 > * The projected metadata is derived purely from the SDK entry, so no
1787 > * per-session overlay read is needed. Failures in the SDK lookup
1788 > * propagate (the caller is doing a single targeted fetch and should
1789 > * learn that the SDK module is broken).
1790 > */
1791 > async getSessionMetadata(session: URI): Promise<IAgentSessionMetadata | undefined> {
1792 // Don't trigger a cold SDK download just to hydrate session metadata
1793 // during restore (the renderer subscribes to the last-active session
1807 return this._metadataStore.project(sdkInfo);
1808 }
1810 > resolveSessionConfig(_params: IAgentResolveSessionConfigParams): Promise<ResolveSessionConfigResult> {
1811 // Decision B5 (plan section 3.3.5): Claude collapses the platform's
1812 // `autoApprove` × `mode` two-axis approval surface onto a single
1857 });
1858 }
1860 > sessionConfigCompletions(_params: IAgentSessionConfigCompletionsParams): Promise<SessionConfigCompletionsResult> {
1861 // Plan section 3.3.5: Claude's only schema property is the
1862 // `permissionMode` static enum, so dynamic completion is
1865 return Promise.resolve({ items: [] });
1866 }
1868 > shutdown(): Promise<void> {
1869 // Phase 6: drain provisional sessions FIRST so any in-flight
1870 // `await sdk.startup()` (kicked off by a racing `sendMessage`)
1907 })();
1908 }
1910 > private async _sendMessage(chat: URI, prompt: string, workingDirectory: URI | undefined, attachments?: readonly MessageAttachment[], turnId?: string, _senderClientId?: string): Promise<void> {
1911 // `IAgent.sendMessage` declares `turnId?` but every production caller in
1912 // `AgentSideEffects` supplies one. Generate a fallback so the
1953 });
1954 }
1956 > /** Builds the SDK user message for a send, addressed to `sdkSessionId`. */
1957 > private _buildSdkPrompt(sdkSessionId: string, prompt: string, attachments: readonly MessageAttachment[] | undefined, turnId: string): SDKUserMessage {
1958 const contentBlocks = resolvePromptToContentBlocks(prompt, attachments);
1959 return {
1969 };
1970 }
1972 > respondToPermissionRequest(requestId: string, approved: boolean): void {
1973 // `requestId` is the SDK's `tool_use_id` — globally unique, so a
1974 // single matching chat is all we need. Silent on miss (workbench may
1980 }
1981 }
1983 > respondToUserInputRequest(requestId: string, response: ChatInputResponseKind, answers?: Record<string, ChatInputAnswer>): void {
1984 // `requestId` is the SDK's `tool_use_id` (interactive tools reuse it as
1985 // the {@link ChatInputRequest.id}); globally unique, so a single
1992 }
1993 }
1995 > /** Every live chat — each session's default chat and its peers. */
1996 > private _allLiveSessions(): ClaudeAgentSession[] {
1997 const all: ClaudeAgentSession[] = [];
1998 for (const entry of this._sessions.values()) {
2001 return all;
2002 }
2004 > private async _abortSession(chat: URI): Promise<void> {
2005 // Phase 9 D1: cancel via the abort controller, NOT `Query.interrupt()`.
2006 // Abort is a control-plane operation — it must NOT serialize
2021 sess.abort();
2022 }
2024 > setPendingMessages(chat: URI, steeringMessage: PendingMessage | undefined, _queuedMessages: readonly PendingMessage[]): void {
2025 // Phase 9 D5: queued messages are intentionally a no-op. CONTEXT.md
2026 // M10 + AgentSideEffects confirm queued messages are consumed
2040 }
2041 }
2043 > /**
2044 > * Forward a user/picker `permissionMode` change to the running SDK so it
2045 > * applies to the next tool this turn, not only from the next `send()`
2046 > * (issue #321691). Only fires for client-originated changes (the host routes
2047 > * internal server writes elsewhere), so this can forward without re-entering
2048 > * a `canUseTool` callback.
2049 > *
2050 > * `permissionMode` is a **session-scoped** config value today (AHP has no
2051 > * per-chat config), so — matching Copilot's session-scoped approvals — we
2052 > * apply it to EVERY materialized chat's `Query` in the session, not just the
2053 > * one the change arrived on. A `replace` that deletes the key resolves to the
2054 > * chat's `permissionModeFallback`, the same value the next `send()` would
2055 > * apply, so live state mirrors the reducer. Provisional chats are skipped —
2056 > * their first `send()` seeds the mode into `Options.permissionMode`. Fire-and-
2057 > * forget: the SDK control round-trip isn't awaited here; the pipeline caches
2058 > * the mode so a later rebind / send re-applies it.
2059 > *
2060 > * TODO: adopt per-chat config when the protocol allows for such — see
2061 > * https://github.com/microsoft/agent-host-protocol/issues/335 — so a picker
2062 > * change scopes to its own chat instead of the whole session.
2063 > */
2064 > onSessionConfigChanged(session: URI, values: Record<string, unknown>): void {
2065 const entry = this._sessions.get(this._getChatContext(session).sessionId);
2066 if (!entry) {
2078 }
2079 }
2081 > private async _changeModel(chat: URI, model: ModelSelection): Promise<void> {
2082 const context = this._getChatContext(chat);
2083 const queueKey = context.isPeerChat ? context.chatKey : context.sessionId;
2097 });
2098 }
2100 > /**
2101 > * Switch (or clear with `undefined`) the selected custom agent for an
2102 > * existing session. Mirrors {@link changeModel}: session owns its
2103 > * provisional/runtime branching and metadata write
2104 > * (see {@link ClaudeAgentSession.setAgent}). For external-only
2105 > * sessions (no in-memory record), the agent is persisted directly to
2106 > * the overlay so a later resume picks it up. When `chat` is an additional
2107 > * peer chat, the change targets that chat's chat.
2108 > */
2109 > private async _changeAgent(chat: URI, agent: AgentSelection | undefined): Promise<void> {
2110 const context = this._getChatContext(chat);
2111 const queueKey = context.isPeerChat ? context.chatKey : context.sessionId;
2120 });
2121 }
2123 > setServerToolHost(host: IAgentServerToolHost): void {
2124 this._serverToolHost = host;
2125 }
2127 > getOrCreateActiveClient(session: URI, client: { readonly clientId: string; readonly displayName?: string }): IActiveClient {
2128 const sessionId = AgentSession.id(session);
2129 const key = `${sessionId}\u0000${client.clientId}`;
2144 return handle;
2145 }
2147 > removeActiveClient(session: URI, clientId: string): void {
2148 const sessionId = AgentSession.id(session);
2149 this._activeClientHandles.delete(`${sessionId}\u0000${clientId}`);
2157 }).catch(() => { /* session torn down */ });
2158 }
2160 > /** Drop cached active-client handles belonging to a session being torn down. */
2161 > private _pruneActiveClientHandles(sessionId: string): void {
2162 const prefix = `${sessionId}\u0000`;
2163 for (const key of [...this._activeClientHandles.keys()]) {
2167 }
2168 }
2170 > onClientToolCallComplete(session: URI, _chat: URI, toolCallId: string, result: ToolCallResult): void {
2171 let target = session;
2172 let parsed;
2180 entry?.defaultChat?.completeClientToolCall(toolCallId, result);
2181 }
2183 > async syncClientCustomizations(session: URI, clientId: string, customizations: ClientPluginCustomization[], options?: { readonly quiet?: boolean }): Promise<ISyncedCustomization[]> {
2184 const sessionId = AgentSession.id(session);
2185 const sess = this._findAnySession(sessionId);
2203 });
2204 }
2206 > /**
2207 > * Project a per-item sync result onto a `SessionCustomizationUpdated`
2208 > * action and emit it on {@link onDidSessionProgress}. Lets the workbench
2209 > * flip each row to `Loaded` / `Error` as the underlying
2210 > * {@link IAgentPluginManager.syncCustomizations} resolves it.
2211 > */
2212 > private _fireCustomizationUpdated(session: URI, item: ISyncedCustomization): void {
2213 this._onDidSessionProgress.fire({
2214 kind: 'action',
2220 });
2221 }
2223 > getCustomizations(): readonly Customization[] {
2224 // Provider-level customization catalogue — feeds `AgentInfo.customizations`
2225 // on `RootAgentsChanged`. Should advertise host-configured plugin refs
2235 return [];
2236 }
2238 > async getSessionCustomizations(session: URI): Promise<readonly Customization[]> {
2239 const sess = this._findAnySession(AgentSession.id(session));
2240 return sess ? await sess.getSessionCustomizations() : [];
2241 }
2243 > async startMcpServer(session: URI, id: string): Promise<void> {
2244 const sess = this._findAnySession(AgentSession.id(session));
2245 await sess?.startMcpServer(id);
2246 }
2248 > async stopMcpServer(session: URI, id: string): Promise<void> {
2249 const sess = this._findAnySession(AgentSession.id(session));
2250 await sess?.stopMcpServer(id);
2251 }
2253 > // #endregion
2254 >
2255 > override dispose(): void {
2256 // Phase 6+ INVARIANT: SDK Query subprocesses (owned by individual
2257 // ClaudeAgentSession wrappers) MUST die BEFORE the proxy handle
2292 this._models.set([], undefined);
2293 }
2294 > } claudeAgent.ts
2295 >
2296 > /**
2297 > * Per-session container. Owns the session's default (main) chat and any
2298 > * additional peer chats — each a {@link ClaudeAgentSession} plus the
2299 > * event-forwarding subscriptions registered against it (e.g. the agent's
2300 > * forward subscription to the session's `onDidSessionProgress` event). A single
2301 > * {@link ClaudeAgent._sessions} map of these entries keeps all chats of a
2302 > * session together (no parallel maps), so dispatch resolves a chat by looking
2303 > * up its owning session and then the chat within it. Disposing the entry
2304 > * disposes the session AND every extra registered via
2305 > * {@link AgentSessionEntry.addDisposable}.
2306 > */
2307 > class ClaudeSessionEntry extends AgentSessionEntry<ClaudeAgentSession> {
2308 > /** Claude sessions always have a materialized default chat once seeded. */
2309 > override get defaultChat(): ClaudeAgentSession {
2310 return super.defaultChat!;
2311 }
2312 > } claudeAgent.ts
src/vs/platform/agentHost/node/claude/claudeAgentSession.ts 569 introduced LOC · 45 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- claudeAgentSession.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 { McpSdkServerConfigWithInstance, OnElicitation, Options, PermissionMode, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
7 > import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
8 > import { CancellationError } from '../../../../base/common/errors.js';
9 > import { Emitter, Event } from '../../../../base/common/event.js';
10 > import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
11 > import { isEqual } from '../../../../base/common/resources.js';
12 > import { URI } from '../../../../base/common/uri.js';
13 > import { INativeEnvironmentService } from '../../../environment/common/environment.js';
14 > import { IFileService } from '../../../files/common/files.js';
15 > import { IInstantiationService } from '../../../instantiation/common/instantiation.js';
16 > import { ILogService } from '../../../log/common/log.js';
17 > import { IAgentConfigurationService } from '../agentConfigurationService.js';
18 > import { ISyncedCustomization } from '../../common/agentPluginManager.js';
19 > import { ClaudePermissionMode } from '../../common/claudeSessionConfigKeys.js';
20 > import { ClaudeRuntimeEffortLevel, toRuntimeEffortLevel, resolveClaudeEffort } from '../../common/claudeModelConfig.js';
21 > import { AgentSignal, IAgentSessionProjectInfo } from '../../common/agentService.js';
22 > import type { IAgentServerToolHost } from '../../common/agentServerTools.js';
23 > import { PendingRequestRegistry } from '../../common/pendingRequestRegistry.js';
24 > import { ISessionDatabase, ISessionDataService } from '../../common/sessionDataService.js';
25 > import { ActionType } from '../../common/state/sessionActions.js';
26 > import { PendingMessage, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ToolCallContributorKind, ToolCallPendingConfirmationState, type AgentSelection, type ModelSelection, type ToolDefinition } from '../../common/state/protocol/state.js';
27 > import { isDefaultChatUri, type Customization, type ToolCallResult } from '../../common/state/sessionState.js';
28 > import { IClaudeAgentSdkService } from './claudeAgentSdkService.js';
29 > import { buildClientMcpServers, buildOptions } from './claudeSdkOptions.js';
30 > import { toSdkModelId } from './claudeModelId.js';
31 > import { buildServerToolMcpServer, CLAUDE_SERVER_TOOL_MCP_SERVER_NAME, serverToolAllowList } from './claudeServerToolMcpServer.js';
32 > import { ClaudeSessionMetadataStore } from './claudeSessionMetadataStore.js';
33 > import { convertToolCallResult } from './clientTools/claudeClientToolResult.js';
34 > import { readClaudePermissionMode } from './claudeSessionPermissionMode.js';
35 > import { SessionClientToolsDiff } from './clientTools/claudeSessionClientToolsModel.js';
36 > import { SessionClientCustomizationsDiff } from './customizations/claudeSessionClientCustomizationsModel.js';
37 > import { ClaudeCustomizationWatcher, buildDiscoveredCustomizations, resolveClaudeAgentName } from './customizations/claudeSessionCustomizationDiscovery.js';
38 > import { applyMcpServerEnablement, findMcpChildId, findMcpServerName, getEffectiveMcpServerCustomizations } from '../shared/mcpCustomizationController.js';
39 > import { scanClaudeDiskCustomizations } from './customizations/scan/claudeAgentSkillScan.js';
40 > import { scanClaudeHooks } from './customizations/scan/claudeHookScan.js';
41 > import { scanClaudeMcpServers } from './customizations/scan/claudeMcpScan.js';
42 > import { scanClaudeNativePlugins } from './customizations/scan/claudeNativePluginScan.js';
43 > import { AgentHostStateManager, IAgentHostStateManager } from '../agentHostStateManager.js';
44 > import { scanClaudeRules } from './customizations/scan/claudeRuleScan.js';
45 > import { resolvePromptToContentBlocks } from './claudePromptResolver.js';
46 > import type { ClaudeTransport } from './claudeProxyService.js';
47 > import { ClaudeSdkPipeline, IRematerializer, type ISdkResolvedCustomizations } from './claudeSdkPipeline.js';
48 > import { SubagentRegistry } from './claudeSubagentRegistry.js';
49 > import { ClaudePermissionKind } from './claudeToolDisplay.js';
50 >
51 > // Re-export for callers that import IRematerializer from the session.
52 > export type { IRematerializer } from './claudeSdkPipeline.js';
53 >
54 > /**
55 > * Inputs to {@link ClaudeAgentSession.materialize}. Carries the
56 > * agent-supplied dependencies that the session itself does not own
57 > * (proxy auth, the `canUseTool` closure that bridges back to the
58 > * agent's per-session lookup, and the resume-vs-fresh discriminator).
59 > */
60 > export interface IMaterializeContext {
61 > readonly transport: ClaudeTransport;
62 > readonly canUseTool: NonNullable<Options['canUseTool']>;
63 > readonly onElicitation: OnElicitation;
64 > readonly isResume: boolean;
65 > /**
66 > * Working directory the host resolved for this session's first send (e.g. an
67 > * isolated worktree). When present it becomes the session's
68 > * {@link ClaudeAgentSession.workingDirectory}, overriding the
69 > * {@link ClaudeAgentSession.workspace} the session was based on. Omitted when
70 > * the session works directly in its `workspace` (folder / workspace-less).
71 > */
72 > readonly workingDirectory?: URI;
73 > /**
74 > * Agent host's server-tool host. When present, the session exposes the
75 > * agent host's server tools (feedback "comments" today, more in the future)
76 > * as an in-process MCP server and advertises them as server tools. Omitted
77 > * by providers that don't support server-side tools.
78 > */
79 > readonly serverToolHost?: IAgentServerToolHost;
80 > }
81 >
82 function resolveCurrentPermissionMode(
83 configurationService: IAgentConfigurationService,
87 return readClaudePermissionMode(configurationService, sessionUri) ?? permissionModeFallback;
88 }
90 > /**
91 > * Per-session coordinator. Owns:
92 > * • Per-session identity (sessionId / sessionUri / workspace /
93 > * workingDirectory).
94 > * • The {@link ClaudeSdkPipeline} that drives the SDK Query lifecycle
95 > * and emits every {@link AgentSignal} for this session (router-
96 > * mapped per-message signals plus `ChatTurnComplete` and
97 > * `steering_consumed`).
98 > * • Pending-permission and pending-user-input registries (Phase 7),
99 > * surfaced via `requestPermission` / `requestUserInput`.
100 > */
101 > export class ClaudeAgentSession extends Disposable {
102 >
103 > private _pipeline: ClaudeSdkPipeline | undefined;
104 > private readonly _chatChannelUri: URI;
105 >
106 > /**
107 > * URI under which this chat's per-chat resources (its session database,
108 > * metadata overlay, config scope and server-tool advertisement) are keyed.
109 > * The default chat uses the real session URI; an additional peer chat uses
110 > * its own `ahp-chat` channel URI so its chat state stays isolated
111 > * from the default chat's. `sessionUri` always remains the real session URI
112 > * and `chatChannelUri` always the chat channel — they are never overloaded.
113 > */
114 > private get _storageUri(): URI {
115 > return isDefaultChatUri(this._chatChannelUri) ? this.sessionUri : this._chatChannelUri;
116 > }
117 >
118 > private get _sessionCustomizations(): readonly Customization[] {
119 return this._stateManager.getSessionState(this.sessionUri.toString())?.customizations ?? [];
120 }
122 > /** Pre-materialize model selection. Mutable; flows into `Options.model` on first installPipeline. */
123 > private _provisionalModel: ModelSelection | undefined;
124 > /**
125 > * Pre-materialize custom-agent selection. Mutable; flows into
126 > * `Options.agent` (resolved to the SDK agent name) on materialize
127 > * and on every rematerializer call. Mid-session changes via
128 > * {@link setAgent} flip {@link clientCustomizationsDiff} dirty so the
129 > * next `send()` rebinds and the new agent reaches the SDK on the
130 > * rebuilt `Query`. The SDK's `Options.agent` is captured at startup
131 > * — there is no runtime control-plane equivalent.
132 > */
133 > private _provisionalAgent: AgentSelection | undefined;
134 > /** Pre-materialize `IAgentCreateSessionConfig.config` bag. Read at materialize time. */
135 > readonly provisionalConfig: Record<string, unknown> | undefined;
136 > /** Resolved project metadata captured at create time (if any). */
137 > readonly project: IAgentSessionProjectInfo | undefined;
138 > /** Always-present abort controller; wired into `Options.abortController` at materialize time. */
139 > readonly abortController: AbortController;
140 >
141 > /**
142 > * The actual directory work is done in. Defaults to {@link workspace} until
143 > * the host hands the session a resolved working directory (e.g. an isolated
144 > * worktree) at {@link materialize} time. `undefined` only when the session is
145 > * workspace-less and has no resolved directory yet.
146 > */
147 > get workingDirectory(): URI | undefined {
148 return this._workingDirectory ?? this.workspace;
149 }
150 > private _workingDirectory: URI | undefined; claudeAgentSession.ts
151 > private readonly _customizationWatcher = this._register(new DisposableStore());
152 >
153 > /** Exposed for the materializer's MCP-server build closure. */
154 > get pendingClientToolCalls(): PendingRequestRegistry<CallToolResult> { return this._pendingClientToolCalls; }
155 > /** Snapshot of permission-mode fallback used when live read is undefined. */
156 > get permissionModeFallback(): ClaudePermissionMode { return this._permissionModeFallback; }
157 >
158 > static createProvisional(
159 sessionId: string,
160 sessionUri: URI,
187 );
188 }
190 > /**
191 > * Phase 12 — per-session registry of Task tool calls that spawn
192 > * subagents (`SubagentSpawn` records keyed by `tool_use_id`, plus a
193 > * reverse index from inner `tool_use_id` to its parent Task). Owned
194 > * here so the registry dies with the session; consumers in the live
195 > * mapper (`ClaudeSdkMessageRouter` / `claudeMapSessionEvents` /
196 > * `claudeSubagentSignals`) and the `canUseTool` bridge read from
197 > * the same instance via the session.
198 > */
199 > readonly subagents: SubagentRegistry = this._register(new SubagentRegistry());
200 >
201 > /**
202 > * Phase 7 / S3.2. Tool-permission deferreds parked inside
203 > * {@link Options.canUseTool}. Keyed by SDK `tool_use_id`.
204 > */
205 > private readonly _pendingPermissions = new PendingRequestRegistry<boolean>();
206 >
207 > /**
208 > * Phase 7 / S3.2. User-input deferreds parked for interactive tools
209 > * (`AskUserQuestion`, `ExitPlanMode`). Keyed by `ChatInputRequest.id`.
210 > */
211 > private readonly _pendingUserInputs = new PendingRequestRegistry<{ response: ChatInputResponseKind; answers?: Record<string, ChatInputAnswer> }>();
212 >
213 > /**
214 > * Phase 10 — owns the workbench-registered client-tool snapshot
215 > * (via {@link SessionClientToolsDiff.model}) plus the
216 > * "changed since last successful build" dirty bit. Read by the
217 > * agent's sendMessage diff check; used by the materialize /
218 > * rematerializer flow to pin the SDK build against a specific
219 > * snapshot. See {@link SessionClientToolsDiff} for the C6 race
220 > * semantics this collaborator enforces.
221 > */
222 > readonly toolDiff: SessionClientToolsDiff;
223 >
224 > /**
225 > * Phase 11 — per-session **client-pushed** synced customization
226 > * snapshot + enablement map. Owns the workbench-supplied
227 > * {@link ISyncedCustomization} list, the per-URI enablement bits,
228 > * and the dirty flag drained at the next {@link send} pre-flight.
229 > * Exists from `createProvisional` onward so client-side reads /
230 > * toggles work uniformly before and after materialize.
231 > *
232 > * Server-side (SDK-discovered) customizations are NOT stored here
233 > * — they're fetched on demand from the live `Query` in
234 > * {@link getSessionCustomizations}.
235 > *
236 > * See {@link SessionClientCustomizationsDiff}.
237 > */
238 > readonly clientCustomizationsDiff: SessionClientCustomizationsDiff = this._register(new SessionClientCustomizationsDiff());
239 >
240 > private readonly _onDidSessionProgress = this._register(new Emitter<AgentSignal>());
241 > readonly onDidSessionProgress: Event<AgentSignal> = this._onDidSessionProgress.event;
242 >
243 > /**
244 > * Real Copilot credits (in nano-AIU) billed by CAPI for the current
245 > * turn, summed across every `/v1/messages` request the SDK made
246 > * (including subagents). Fed by {@link recordTurnCredits} from the
247 > * proxy's `onDidReportCredits`, reset at the start of each {@link send},
248 > * and attached to the turn's `ChatUsage` signal by
249 > * {@link _enrichSignalWithCredits}. Unlike the SDK's `total_cost_usd`
250 > * (an Anthropic-list-price estimate), this is what CAPI actually bills.
251 > */
252 > private _currentTurnNanoAiu = 0;
253 >
254 > /**
255 > * Transport the session materialized under (Phase 19). Defaults to `proxy`
256 > * until {@link materialize} resolves it from {@link IMaterializeContext}.
257 > * Gates {@link _enrichSignalWithCredits} so native turns never carry a
258 > * Copilot credits overlay (the proxy is the only credit source).
259 > */
260 > private _transportKind: ClaudeTransport['kind'] = 'proxy';
261 >
262 > /**
263 > * Accumulate proxy-reported billed credits for the in-flight turn.
264 > * Called from {@link ClaudeAgent} for every proxy `onDidReportCredits`
265 > * routed to this session. Ignores non-positive / non-finite values.
266 > */
267 > recordTurnCredits(totalNanoAiu: number): void {
268 if (Number.isFinite(totalNanoAiu) && totalNanoAiu > 0) {
269 this._currentTurnNanoAiu += totalNanoAiu;
270 }
271 }
273 > /**
274 > * Inject the turn's accumulated Copilot credits into its `ChatUsage`
275 > * signal as `_meta.copilotUsage.totalNanoAiu` — the well-known key the
276 > * workbench prefers over `_meta.cost` when rendering per-turn credits.
277 > * All other signals pass through untouched.
278 > */
279 > private _enrichSignalWithCredits(signal: AgentSignal): AgentSignal {
280 if (this._transportKind !== 'proxy' || signal.kind !== 'action' || signal.action.type !== ActionType.ChatUsage || this._currentTurnNanoAiu <= 0) {
281 return signal;
296 };
297 }
299 > /**
300 > * Stamps the MCP {@link ToolCallContributor} onto a `ChatToolCallStart` for
301 > * an external `mcp__<server>__<tool>` call, resolved from this session's
302 > * cached customization snapshot. Owned here because the session owns the
303 > * customization data; the stream mapper stays free of it. (The in-process
304 > * `mcp__client__` server already carries a Client contributor from the mapper.)
305 > */
306 > private _enrichSignalWithMcpContributor(signal: AgentSignal): AgentSignal {
307 if (signal.kind !== 'action' || signal.action.type !== ActionType.ChatToolCallStart || signal.action.contributor !== undefined) {
308 return signal;
319 return { ...signal, action: { ...signal.action, contributor: { kind: ToolCallContributorKind.MCP, customizationId } } };
320 }
322 > constructor(
323 readonly sessionId: string,
324 readonly sessionUri: URI,
355 this._watchCustomizations(this.workspace);
356 }
358 > private _watchCustomizations(directory: URI | undefined): void {
359 this._customizationWatcher.clear();
360 const watcher = this._customizationWatcher.add(new ClaudeCustomizationWatcher(
366 this._customizationWatcher.add(watcher.onDidChange(() => this._onDidCustomizationsChange.fire()));
367 }
369 > /**
370 > * One-shot SDK assistant-message uuid that the next materialize / rebuild
371 > * resumes *up to and including* (the SDK's `Options.resumeSessionAt`).
372 > * Staged by {@link truncateToTurn}; read by the next build and cleared
373 > * only once that build *succeeds* (so a thrown / cancelled rebuild keeps
374 > * the anchor staged and the next send retries the truncation rather than
375 > * silently proceeding without it and undoing the checkpoint restore).
376 > */
377 > private _pendingResumeSessionAt: string | undefined;
378 >
379 > /**
380 > * In-place truncation to `turnId` ("Restore Checkpoint"): prune the
381 > * per-turn DB rows (file edits, checkpoint refs) past the boundary AND
382 > * stage the SDK resume anchor that the next rebuild applies via
383 > * `Options.resumeSessionAt`. These two halves are one invariant — pruning
384 > * without staging the anchor would drop DB rows while the SDK still
385 > * replays the truncated turns; staging without pruning would leave stale
386 > * rows — so they live behind a single call rather than two the caller
387 > * could half-invoke. The prune runs first because it is the fallible half:
388 > * a DB failure then rejects without leaving an anchor staged for the next
389 > * turn. `turnId` is the protocol turn id (DB key); `resumeAnchorUuid` is
390 > * the SDK assistant-message uuid the agent resolved for it.
391 > */
392 > async truncateToTurn(turnId: string, resumeAnchorUuid: string): Promise<void> {
393 await this._withDatabase(db => db.deleteTurnsAfter(turnId));
394 this._pendingResumeSessionAt = resumeAnchorUuid;
395 }
397 > /** Prunes all per-turn DB rows (remove-all truncation). */
398 > async pruneAllTurns(): Promise<void> {
399 await this._withDatabase(db => db.deleteAllTurns());
400 }
402 > /**
403 > * Runs `fn` against a short-lived, ref-counted session DB handle so the
404 > * write is safe regardless of the pipeline's own dbRef lifecycle (the
405 > * ref-count keeps the shared DB alive; disposing only decrements).
406 > */
407 > private async _withDatabase(fn: (db: ISessionDatabase) => Promise<void>): Promise<void> {
408 const ref = this._sessionDataService.openDatabase(this._storageUri);
409 try {
413 }
414 }
416 > /**
417 > * Bring the session up: build SDK `Options`, start the SDK, open the
418 > * session-scoped DB ref, construct the pipeline, and attach the
419 > * rematerializer used for yield-restart (e.g. after a client-tool
420 > * snapshot change). Idempotent on re-call: extra calls throw rather
421 > * than silently re-materialize.
422 > *
423 > * If the supplied {@link IMaterializeContext.proxyHandle}'s underlying
424 > * `abortController` fires while `sdk.startup()` is in flight, the SDK
425 > * unwinds via the controller; if `startup` resolves anyway, the
426 > * `WarmQuery` is asyncDisposed and a {@link CancellationError} is
427 > * thrown (Q8 belt-and-suspenders).
428 > */
429 > async materialize(ctx: IMaterializeContext): Promise<void> {
430 if (this._pipeline) {
431 throw new Error('ClaudeAgentSession is already materialized');
590 this._onDidCustomizationsChange.fire();
591 }
593 > /**
594 > * Build the SDK tool wiring shared by the initial materialize and every
595 > * yield-restart rematerialize: the in-process MCP servers plus the
596 > * auto-approve allow-list.
597 > *
598 > * The MCP servers are the workbench client tools (which round-trip to the
599 > * workbench) plus, when a server-tool host is wired, the agent host's own
600 > * server tools (executed in-process). `mcpServers` is `undefined` when
601 > * neither is present so `Options.mcpServers` is omitted entirely and the
602 > * SDK keeps its default; `allowedTools` carries the SDK-prefixed server tool
603 > * names (so they auto-approve without prompting) and is `undefined` when no
604 > * server-tool host is wired.
605 > *
606 > * Keeping both in one place ensures the two startup paths can never drift,
607 > * and that a newly registered server tool is wired everywhere at once.
608 > */
609 > private async _buildStartupToolWiring(
610 serverToolHost: IAgentServerToolHost | undefined,
611 ): Promise<{ mcpServers: Record<string, McpSdkServerConfigWithInstance> | undefined; allowedTools: readonly string[] | undefined }> {
629 return { mcpServers, allowedTools: autoApproveToolNames ? serverToolAllowList(autoApproveToolNames) : undefined };
630 }
632 > /** True once {@link materialize} has installed the SDK pipeline. */
633 > get isPipelineReady(): boolean { return this._pipeline !== undefined; }
634 >
635 > /**
636 > * Whether this chat currently has a turn in flight or queued. False when
637 > * provisional (no pipeline) or idle between turns. Used by non-destructive
638 > * idle release to avoid disconnecting mid-turn.
639 > */
640 > get hasActiveTurn(): boolean { return this._pipeline?.hasActiveTurn ?? false; }
641 >
642 > /** Pre-materialize model selection accessor (read by materializer to build Options). */
643 > get provisionalModel(): ModelSelection | undefined { return this._provisionalModel; }
644 >
645 > private _requirePipeline(): ClaudeSdkPipeline {
646 if (!this._pipeline) {
647 throw new Error('ClaudeAgentSession is not materialized');
649 return this._pipeline;
650 }
652 > get isResumed(): boolean { return this._requirePipeline().isResumed; }
653 >
654 > /**
655 > * Abort the live SDK subprocess and await its full teardown so the
656 > * session id is released. No-op when the session was never materialized
657 > * (no subprocess to stop). Used by remove-all truncation before it
658 > * recreates a fresh session under the same id — the CLI keeps the id
659 > * locked until the old subprocess exits.
660 > */
661 > async shutdownLiveQuery(): Promise<void> {
662 await this._pipeline?.shutdownAndWait();
663 }
665 > /**
666 > * Seed the pipeline's current + applied config cache from
667 > * materialize-time `Options`. The SDK already starts with these
668 > * values, so the cache prevents a redundant first `setModel` /
669 > * `applyFlagSettings` call.
670 > */
671 > seedBijectiveState(state: { model?: string; effort?: ClaudeRuntimeEffortLevel; permissionMode?: PermissionMode }): void {
672 this._requirePipeline().seedCurrentConfig(state.model, state.effort, state.permissionMode);
673 }
675 > attachRematerializer(rematerializer: IRematerializer): void {
676 this._requirePipeline().attachRematerializer(rematerializer);
677 }
679 > /**
680 > * Send a user prompt. Performs the per-turn pre-flight before
681 > * yielding to the pipeline:
682 > *
683 > * - If {@link toolDiff} or {@link clientCustomizationsDiff} reports the
684 > * live `Query` is out of sync with the workbench's view, yield-restart
685 > * so the SDK picks up the new `Options.mcpServers` / `Options.plugins`.
686 > * `Query.reloadPlugins()` cannot help here — the SDK's plugin URI set
687 > * is captured at startup, so any add / remove / nonce-bump must go
688 > * through a full rebuild. The rebind itself re-applies the live
689 > * `permissionMode` via the rematerializer.
690 > * - Otherwise forward the live `permissionMode` to the bound `Query` so
691 > * a `SessionConfigChanged` action that arrived between turns wins.
692 > * The pipeline's bijective cache dedupes a no-op `setPermissionMode`,
693 > * so this is free when nothing changed.
694 > *
695 > * Model / effort are not threaded through here — the pipeline's current
696 > * model / effort (set eagerly via {@link setModel}) is whatever
697 > * the SDK has been told.
698 > */
699 > async send(prompt: SDKUserMessage, turnId: string): Promise<void> {
700 const pipeline = this._requirePipeline();
701 // New turn: reset the per-turn credit accumulator so proxy reports
710 return pipeline.send(prompt, turnId);
711 }
713 > /**
714 > * Single yield-restart that covers both client-tool and
715 > * customization divergence in one trip. Drains the parked
716 > * client-tool MCP handlers (same as the original tool-only
717 > * rebind), then triggers the pipeline rebind — the rematerializer
718 > * reads `toolDiff` and reducer-backed client plugin paths while
719 > * building the new `Options`, so the bit on each diff clears in
720 > * lockstep with the SDK actually receiving the new values. Fires
721 > * `_onDidCustomizationsChange` afterwards so the workbench
722 > * refetches `getSessionCustomizations` and picks up any newly
723 > * resolved server-side entries from the rebuilt `Query`.
724 > */
725 > private async _rebindForSyncedState(): Promise<void> {
726 this._pendingClientToolCalls.rejectAll(new CancellationError());
727 await this._requirePipeline().rebindForRestart();
728 this._onDidCustomizationsChange.fire();
729 }
731 > /**
732 > * Cancel the in-flight SDK turn. Mirrors the production reference;
733 > * see {@link ClaudeSdkPipeline.abort}. Also denies any parked
734 > * permission / user-input requests so the SDK's `canUseTool`
735 > * callback (and any interactive tool waiting on user input) unwinds
736 > * with a deny / cancel result instead of leaving stale UI behind.
737 > */
738 > abort(): void {
739 this._pendingPermissions.denyAll(false);
740 this._pendingUserInputs.denyAll({ response: ChatInputResponseKind.Cancel });
741 this._requirePipeline().abort();
742 }
744 > /**
745 > * Eagerly apply a model change and persist the new selection. Safe to
746 > * call before or after materialize:
747 > *
748 > * - Pre-materialize: stash the model on the session so the first SDK
749 > * startup picks it up via `Options.model` / `Options.effort`.
750 > * - Post-materialize: queue the change on the pipeline; the SDK
751 > * applies it on the NEXT user request via
752 > * `Query.setModel` / `Query.applyFlagSettings`. `'max'` flows through
753 > * unchanged — see {@link toRuntimeEffortLevel}.
754 > *
755 > * In both cases the new model is persisted to the per-session
756 > * metadata overlay so a later resume sees the user's choice.
757 > */
758 > async setModel(model: ModelSelection): Promise<void> {
759 this._provisionalModel = model;
760 if (this._pipeline) {
770 await this._metadataStore.write(this._storageUri, { model });
771 }
773 > /**
774 > * Pre-materialize custom-agent selection accessor.
775 > */
776 > get provisionalAgent(): AgentSelection | undefined { return this._provisionalAgent; }
777 >
778 > /**
779 > * Change (or clear with `undefined`) the selected custom agent for this
780 > * session. The SDK captures `Options.agent` at startup with no
781 > * working runtime control (`applyFlagSettings({ agent })` exists on
782 > * the SDK surface but doesn't actually swap the live agent), so
783 > * post-materialize calls flip {@link clientCustomizationsDiff}
784 > * dirty and the next `send()` pre-flight rebinds with the new agent
785 > * baked into the rebuilt `Query`. Persisted to the per-session
786 > * metadata overlay so a resume picks up the choice.
787 > */
788 > async setAgent(agent: AgentSelection | undefined): Promise<void> {
789 if (this._provisionalAgent === agent) {
790 return;
798 await this._metadataStore.write(this._storageUri, { agent: agent ?? null });
799 }
801 > /**
802 > * Inject a steering message. Builds the `priority: 'now'`
803 > * {@link SDKUserMessage} and hands it to the pipeline; the pipeline
804 > * inherits the parent's turnId (CONTEXT.md M10) and fires
805 > * `steering_consumed` when the SDK accepts it. No-op if the pipeline
806 > * is aborted.
807 > */
808 > injectSteering(steeringMessage: PendingMessage): void {
809 const pipeline = this._requirePipeline();
810 if (pipeline.isAborted) {
829 pipeline.injectSteering(sdkMessage, steeringMessage.id);
830 }
832 > /** Live permission-mode change. Forwards to the pipeline; the pipeline remembers it for re-application after a rebind. */
833 > setPermissionMode(mode: PermissionMode): Promise<void> {
834 return this._requirePipeline().setPermissionMode(mode);
835 }
837 > // #region Phase 7 / S3.2 — pending state
838 >
839 > /**
840 > * Atomically register a pending-permission deferred and fire the
841 > * `pending_confirmation` signal. The SDK is blocked on the returned
842 > * promise inside its `canUseTool` callback until
843 > * {@link respondToPermissionRequest} resolves it. Resolves with
844 > * `false` if the pipeline is aborted.
845 > */
846 > requestPermission(args: {
847 readonly toolUseID: string;
848 readonly state: ToolCallPendingConfirmationState;
866 });
867 }
869 > respondToPermissionRequest(requestId: string, approved: boolean): boolean {
870 return this._pendingPermissions.respond(requestId, approved);
871 }
873 > /**
874 > * Fire a {@link ActionType.ChatInputRequested} action and park on
875 > * a deferred until {@link respondToUserInputRequest} resolves it.
876 > * Resolves with `{ response: Cancel }` if the pipeline is aborted.
877 > */
878 > requestUserInput(request: ChatInputRequest, parentToolCallId?: string): Promise<{ response: ChatInputResponseKind; answers?: Record<string, ChatInputAnswer> }> {
879 if (!this._pipeline || this._pipeline.isAborted || !this._pipeline.hasActiveTurn) {
880 return Promise.resolve({ response: ChatInputResponseKind.Cancel });
892 });
893 }
895 > respondToUserInputRequest(
896 requestId: string,
897 response: ChatInputResponseKind,
900 return this._pendingUserInputs.respond(requestId, { response, answers });
901 }
903 > // #endregion
904 >
905 > // #region Phase 10 — client tools
906 >
907 > /** Replace a client's registered tools (full replacement). */
908 > setClientTools(clientId: string, tools: readonly ToolDefinition[]): void {
909 this.toolDiff.model.setTools(clientId, tools);
910 }
912 > /** This client's registered tools (empty when absent). */
913 > getClientTools(clientId: string): readonly ToolDefinition[] {
914 return this.toolDiff.model.getTools(clientId);
915 }
917 > /** Remove a client's tool contribution from this session. */
918 > removeClientTools(clientId: string): void {
919 this.toolDiff.model.removeClient(clientId);
920 }
922 > /** Remove a client's customization contribution from this session. */
923 > removeClientCustomizations(clientId: string): void {
924 this.clientCustomizationsDiff.model.removeClient(clientId);
925 }
927 > /**
928 > * Resolve a parked client-tool MCP handler with the workbench-supplied
929 > * result. Returns `true` if a matching deferred was found and settled.
930 > * Unknown ids are a benign no-op — `agentSideEffects.ts` forwards every
931 > * `ChatToolCallComplete` envelope, so SDK-owned tool completions land
932 > * here too and must NOT throw.
933 > */
934 > completeClientToolCall(toolCallId: string, result: ToolCallResult): boolean {
935 const converted = convertToolCallResult(result, toolCallId);
936 return this._pendingClientToolCalls.respond(toolCallId, converted);
937 }
939 > /**
940 > * Drive a yield-restart so the SDK picks up the new client-tool set
941 > * on its next user request. Public entry point for callers that need
942 > * to force a tool-only rebind; internal pre-flight goes through
943 > * {@link _rebindForSyncedState}.
944 > */
945 > async rebindForClientTools(): Promise<void> {
946 await this._rebindForSyncedState();
947 }
949 > // #endregion
950 >
951 > // #region Phase 11 — customizations / plugins
952 >
953 > /**
954 > * Merged fire-and-forget signal that this session's customization
955 > * surface changed. Fires from three sources:
956 > *
957 > * 1. Client-side writes (`adoptClientCustomizations`) — via the
958 > * {@link SessionClientCustomizationsDiff} observable wired up in the
959 > * constructor.
960 > * 2. Materialize completes — surfaces the server-side
961 > * (SDK-discovered) tier to the workbench for the first time.
962 > * 3. The send() pre-flight rebind completes — the rebuilt SDK's
963 > * resolved set may have changed.
964 > *
965 > * Drives a workbench refetch of {@link getSessionCustomizations}.
966 > * Does NOT itself trigger any SDK action — the dirty bit on
967 > * {@link SessionClientCustomizationsDiff} drives plugin rebinds,
968 > * and only flips on client-side writes.
969 > */
970 > private readonly _onDidCustomizationsChange = this._register(new Emitter<void>());
971 > readonly onDidCustomizationsChange: Event<void> = this._onDidCustomizationsChange.event;
972 >
973 > /**
974 > * Adopt the result of a global {@link IAgentPluginManager.syncCustomizations}
975 > * pass (**client-pushed** path). The agent owns the manager (it's
976 > * a process-wide singleton with a shared on-disk cache) and pushes
977 > * the resulting snapshot down here. Flips the client-side dirty bit
978 > * so the next {@link send} pre-flight reloads SDK plugins.
979 > */
980 > adoptClientCustomizations(clientId: string, synced: readonly ISyncedCustomization[]): void {
981 this.clientCustomizationsDiff.model.setSyncedCustomizations(clientId, synced);
982 }
984 > /**
985 > * Snapshot of the **client-pushed** customizations on this session.
986 > * Does NOT include server-side (SDK-discovered) entries — use
987 > * {@link getSessionCustomizations} for the merged view.
988 > */
989 > getClientCustomizations(): readonly ISyncedCustomization[] {
990 return this.clientCustomizationsDiff.model.state.get().synced;
991 }
993 > /** Snapshot of the last {@link getSessionCustomizations} result, read by {@link _enrichSignalWithMcpContributor}. */
994 > private _lastCustomizations: readonly Customization[] = [];
995 >
996 > /**
997 > * Project the union of (a) **client-pushed** customizations and
998 > * (b) the **server-side** (SDK-discovered) view (commands / agents
999 > * / MCP servers, including those the SDK discovered on its own
1000 > * from `~/.claude/**`) onto the protocol's
1001 > * {@link Customization} surface, with reducer-backed enablement
1002 > * applied to client-pushed entries.
1003 > *
1004 > * Pre-materialize sessions return only the client-pushed projection
1005 > * — the SDK side has no Query to query yet. A failure to read the
1006 > * SDK snapshot is warn-logged and the client-pushed projection is
1007 > * still returned, so a transient SDK hiccup doesn't blank the UI.
1008 > */
1009 > async getSessionCustomizations(): Promise<readonly Customization[]> {
1010 const { synced } = this.clientCustomizationsDiff.model.state.get();
1011 const userHome = this._environmentService.userHome;
1052 return projected;
1053 }
1055 > private async _reconcileMcpServerEnablement(): Promise<void> {
1056 const pipeline = this._requirePipeline();
1057 const state = this._sessionCustomizations;
1065 }
1066 }
1068 > private _desiredClientPluginPaths(): readonly URI[] {
1069 const state = this._sessionCustomizations;
1070 const desiredById = new Map(state.map(customization => [customization.id, customization.enabled]));
1077 return paths;
1078 }
1080 > async startMcpServer(id: string): Promise<void> {
1081 const serverName = await this._resolveMcpServerName(id);
1082 if (!serverName) {
1090 this._onDidCustomizationsChange.fire();
1091 }
1093 > async stopMcpServer(id: string): Promise<void> {
1094 const serverName = await this._resolveMcpServerName(id);
1095 if (!serverName) {
1104 this._onDidCustomizationsChange.fire();
1105 }
1107 > private async _resolveMcpServerName(id: string): Promise<string | undefined> {
1108 return findMcpServerName(this._lastCustomizations, id) ?? findMcpServerName(await this.getSessionCustomizations(), id);
1109 }
1111 > // #endregion
1112 >
1113 > override dispose(): void {
1114 // Resolve parked deferreds before tearing the pipeline down so the
1115 // SDK's canUseTool callback unwinds with a deny and the loop exits.
src/vs/platform/agentHost/node/claude/claudeCanUseTool.ts 116 introduced LOC · 6 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- claudeCanUseTool.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 { PermissionResult, PermissionUpdate } from '@anthropic-ai/claude-agent-sdk';
7 > import { ClaudePermissionMode, ClaudeSessionConfigKey } from '../../common/claudeSessionConfigKeys.js';
8 > import { ChatInputResponseKind, ToolCallPendingConfirmationState, ToolCallStatus } from '../../common/state/protocol/state.js';
9 > import { IAgentConfigurationService } from '../agentConfigurationService.js';
10 > import { ClaudeAgentSession } from './claudeAgentSession.js';
11 > import { buildAskUserSessionInputQuestions, buildExitPlanModeConfirmationState, flattenAskUserAnswers, parseAskUserQuestionInput } from './claudeInteractiveTools.js';
12 > import { CLAUDE_PLAN_DECLINED_MESSAGE, CLAUDE_QUESTION_CANCELLED_MESSAGE, CLAUDE_USER_DECLINED_MESSAGE } from './claudeToolDenial.js';
13 > import { getClaudeConfirmationTitle, getClaudeInvocationMessage, getClaudePermissionKind, getClaudeToolDisplayName, getClaudeToolInputString, getClaudeToolPath, INTERACTIVE_CLAUDE_TOOLS, buildClaudeToolMeta } from './claudeToolDisplay.js';
14 >
15 > /**
16 > * Dependencies for {@link handleCanUseTool}. Kept narrow: a session
17 > * lookup callback (so the agent's `_sessions` map stays private) and
18 > * the configuration service for the one mutation point
19 > * (`ExitPlanMode` Approve persists `permissionMode = 'acceptEdits'`).
20 > * Subagent correlation reads from `session.subagents` (the per-session
21 > * {@link import('./claudeSubagentRegistry.js').SubagentRegistry}); the
22 > * bridge no longer takes a host-singleton resolver dep.
23 > */
24 > export interface IClaudeCanUseToolDeps {
25 > readonly getSession: (sessionId: string) => ClaudeAgentSession | undefined;
26 > readonly configurationService: IAgentConfigurationService;
27 > }
28 >
29 > /**
30 > * SDK `canUseTool` `options` shape. Re-stated here to keep this module
31 > * decoupled from the agent's import wall.
32 > */
33 > export interface IClaudeCanUseToolOptions {
34 > readonly suggestions?: PermissionUpdate[];
35 > readonly signal: AbortSignal;
36 > readonly blockedPath?: string;
37 > readonly toolUseID: string;
38 > /**
39 > * Phase 12 step 5 — SDK-supplied subagent id for inner-tool
40 > * confirmations. When set, the bridge resolves the parent
41 > * `tool_use_id` via the mapper state and tags the resulting
42 > * `pending_confirmation` so the host can route it to the subagent
43 > * session and feed the resolver cache.
44 > */
45 > readonly agentID?: string;
46 > }
47 >
48 > /**
49 > * SDK `canUseTool` callback. Fires `pending_confirmation` and parks
50 > * on {@link ClaudeAgentSession.requestPermission} (or
51 > * {@link ClaudeAgentSession.requestUserInput} for `AskUserQuestion`)
52 > * until the workbench dispatches a response.
53 > *
54 > * **Pure UI bridge.** No permission judgement of its own — the SDK
55 > * owns auto-approval / auto-denial via `permissionMode`
56 > * ([sdk.d.ts:1558](../../../../../../extensions/copilot/node_modules/@anthropic-ai/claude-agent-sdk/sdk.d.ts#L1558))
57 > * and only invokes `canUseTool` for tools it has decided the host
58 > * needs to surface. The interactive built-ins (`AskUserQuestion`,
59 > * `ExitPlanMode`) are exempt from auto-approval and always reach
60 > * `canUseTool` regardless of mode — their "permission" is itself the
61 > * user-facing question.
62 > *
63 > * Note: protocol-level auto-approve for write tools lives in
64 > * `agentSideEffects.ts:_handleToolReady`, which subscribes to the
65 > * `pending_confirmation` signal and calls
66 > * `respondToPermissionRequest`. The atomic register-then-fire
67 > * invariant lives inside {@link ClaudeAgentSession.requestPermission}
68 > * (via `PendingRequestRegistry.registerAndFire`).
69 > */
70 export async function handleCanUseTool(
71 deps: IClaudeCanUseToolDeps,
100 }
101 }
103 async function dispatchCanUseTool(
104 deps: IClaudeCanUseToolDeps,
151 : { behavior: 'deny', message: CLAUDE_USER_DECLINED_MESSAGE };
152 }
154 > /**
155 > * Phase 12 step 5 — shared subagent-context resolution for every
156 > * `pending_confirmation` and `ChatInputRequested` emission. When the
157 > * SDK delivers `options.agentID`, look up the parent spawn via the
158 > * session's registry and write the agentId back to it. The write is
159 > * **first-writer-wins** (a mismatched late agentID is silently dropped
160 > * — see {@link SubagentSpawn.setAgentId}); all writers converge on the
161 > * SDK's single identity for a given Task, so conflict is not expected.
162 > * Returns the parent `tool_use_id` for top-level callers to spread
163 > * onto the request payload, or `undefined` when this isn't an inner
164 > * tool call (no subagent context).
165 > */
166 function resolveSubagentParent(
167 session: ClaudeAgentSession,
178 return undefined;
179 }
181 > /**
182 > * Dispatch the two interactive built-in tools (S3.5). They share a
183 > * dispatcher only because both are exempt from SDK
184 > * `permissionMode` auto-approval; routing then splits by tool
185 > * semantics. Caller must guard with {@link INTERACTIVE_CLAUDE_TOOLS} —
186 > * the `default` branch is defensive and should never fire.
187 > */
188 function handleInteractiveTool(
189 deps: IClaudeCanUseToolDeps,
202 }
203 }
205 > /**
206 > * `ExitPlanMode` (S3.5b): render the plan body inside the standard
207 > * tool-confirmation card (`pending_confirmation` channel — same path
208 > * normal write tools take), persist `permissionMode = 'acceptEdits'`
209 > * on Approve (next `sendMessage` forwards via `Query.setPermissionMode`),
210 > * deny with production-mirrored wording on cancel.
211 > *
212 > * NOTE: we MUST NOT call `session.setPermissionMode` here. That issues
213 > * a live SDK control request on the same channel the SDK is using to
214 > * deliver the canUseTool request — interleaving a second control
215 > * request before returning the canUseTool response collides with the
216 > * SDK's loop and the turn never resumes. Production updates state
217 > * post-tool-result (`claudeMessageDispatch.ts:328` →
218 > * `setPermissionModeForSession`); we mirror by writing
219 > * `IAgentConfigurationService` and letting `sendMessage`'s
220 > * `entry.setPermissionMode(...)` (between turns) do the live forward.
221 > */
222 async function handleExitPlanMode(
223 deps: IClaudeCanUseToolDeps,
242 return { behavior: 'deny', message: CLAUDE_PLAN_DECLINED_MESSAGE };
243 }
245 > /**
246 > * `AskUserQuestion` (S3.5a): translate the SDK's question carousel
247 > * into a {@link ChatInputRequest}, await the workbench answer,
248 > * and re-key answers by question text (matching the production
249 > * extension's `Record<question, value>` contract).
250 > */
251 async function handleAskUserQuestion(
252 deps: IClaudeCanUseToolDeps,
src/vs/platform/agentHost/node/claude/claudePromptResolver.ts 31 introduced LOC · 2 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- claudePromptResolver.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 Anthropic from '@anthropic-ai/sdk';
7 > import { URI } from '../../../../base/common/uri.js';
8 > import { isAgentFeedbackAnnotationsAttachment, renderAgentFeedbackAnnotationsAttachment } from '../../common/meta/agentFeedbackAttachments.js';
9 > import { MessageAttachmentKind, type MessageAttachment } from '../../common/state/protocol/state.js';
10 >
11 > /**
12 > * Build the {@link Anthropic.ContentBlockParam}[] payload for an
13 > * {@link SDKUserMessage} from a plain text prompt and the protocol
14 > * attachments accompanying the user message.
15 > *
16 > * Phase 6 keeps the resolver pure and minimal: a single `text` block
17 > * carrying the prompt, plus additional text blocks for attachments. This
18 > * mirrors the production extension's resolver shape so a future phase that
19 > * adds image rendering or inline range substitution can extend without
20 > * restructuring.
21 > *
22 > * Selections are rendered as URI references with an optional line
23 > * suffix. The protocol's {@link TextSelection} carries range metadata
24 > * only; the selected text is not included inline.
25 > *
26 > * Resource attachments and simple attachments with model representations
27 > * are honoured today. Embedded resources are dropped because the current
28 > * Claude path does not have a place to consume them.
29 > */
30 > export function resolvePromptToContentBlocks(
31 prompt: string,
32 attachments?: readonly MessageAttachment[],
89 return blocks;
90 }
92 function uriToString(uri: URI): string {
93 return uri.scheme === 'file' ? uri.fsPath : uri.toString();
src/vs/platform/agentHost/node/claude/claudeSessionPermissionMode.ts 23 introduced LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- claudeSessionPermissionMode.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 { URI } from '../../../../base/common/uri.js';
7 > import { type ClaudePermissionMode, ClaudeSessionConfigKey, narrowClaudePermissionMode } from '../../common/claudeSessionConfigKeys.js';
8 > import type { IAgentConfigurationService } from '../agentConfigurationService.js';
9 >
10 > /**
11 > * Read the live `permissionMode` for a session from
12 > * {@link IAgentConfigurationService}, narrowed to the SDK's
13 > * `PermissionMode` union (5/6 values, excluding `dontAsk`; sdk.d.ts:1560).
14 > * Returns `undefined` when the session's schema hasn't been registered or
15 > * carries a value that slipped past schema validation — callers pick the
16 > * fallback (the createSession-time intent at materialize, `'default'` at
17 > * the canUseTool gate, etc.).
18 > *
19 > * Called on every canUseTool entry, on every rebind, and before each
20 > * `session.send` so a mid-turn `SessionConfigChanged` action wins over
21 > * the materialize-time seed (plan S3.6).
22 > */
23 > export function readClaudePermissionMode(
24 configurationService: IAgentConfigurationService,
25 sessionUri: URI,