src/vs/platform/agentHost/node/claude/claudeAgentSdkService.ts

295 LOC · 219 covered · 76 uncovered · 23 ranges · 380 concepts · 4 introducers · 217 tests

File neighbourhood

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

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

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

Graph controls are ready.

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

1 > /*--------------------------------------------------------------------------------------------- claudeAgentSdkService.ts ×16
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 { AnyZodRawShape, ForkSessionOptions, ForkSessionResult, GetSessionMessagesOptions, GetSubagentMessagesOptions, InferShape, ListSessionsOptions, ListSubagentsOptions, McpSdkServerConfigWithInstance, Options, Query, SDKSessionInfo, SDKUserMessage, SdkMcpToolDefinition, SessionMessage, SessionMutationOptions, WarmQuery } from '@anthropic-ai/claude-agent-sdk';
7 > import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
8 > import { pathToFileURL } from 'url';
9 > import { CancellationToken } from '../../../../base/common/cancellation.js';
10 > import { join } from '../../../../base/common/path.js';
11 > import { createDecorator } from '../../../instantiation/common/instantiation.js';
12 > import { ILogService } from '../../../log/common/log.js';
13 > import { IAgentSdkDownloader, IAgentSdkPackage } from '../agentSdkDownloader.js';
14 > import { AgentHostClaudeSdkRootEnvVar } from '../../common/agentService.js';
15 >
16 > /**
17 > * `@anthropic-ai/claude-agent-sdk` distribution descriptor. Lives in this
18 > * file because it encodes Claude-specific knowledge — the env-var name
19 > * and the fact that Claude ships separate `linux-{x64,arm64}-musl` SKUs
20 > * alongside the default glibc ones. The downloader consumes this through
21 > * `IAgentSdkPackage` and never names Claude directly.
22 > */
23 > export const ClaudeSdkPackage: IAgentSdkPackage = {
24 > id: 'claude',
25 > displayName: 'Claude',
26 > devOverrideEnvVar: AgentHostClaudeSdkRootEnvVar,
27 > hasSeparateMuslLinuxPackage: true,
28 > };
29 >
30 > export const IClaudeAgentSdkService = createDecorator<IClaudeAgentSdkService>('claudeAgentSdkService');
31 >
32 > /**
33 > * Pure per-method passthrough shim over `@anthropic-ai/claude-agent-sdk`.
34 > *
35 > * Every method on this interface corresponds 1:1 to a single SDK export.
36 > * The shim owns lazy module loading and the first-failure log-once
37 > * convention; it does NOT compose, wrap, or add behavior on top of the
38 > * SDK's surface. Higher-level orchestration (e.g. building the in-process
39 > * client-tool MCP server) lives in dedicated modules that depend on this
40 > * interface for the raw bindings.
41 > */
42 > export interface IClaudeAgentSdkService {
43 > readonly _serviceBrand: undefined;
44 >
45 > listSessions(): Promise<readonly SDKSessionInfo[]>;
46 > getSessionInfo(sessionId: string): Promise<SDKSessionInfo | undefined>;
47 > startup(params: { options: Options; initializeTimeoutMs?: number }): Promise<WarmQuery>;
48 > /**
49 > * 1:1 with the SDK's top-level `query` export. Returns a `Query` whose
50 > * subprocess starts lazily; callers drive control requests on it (e.g.
51 > * `supportedModels()` for model enumeration) and `close()` it when done.
52 > * Async only because the SDK module itself is loaded lazily.
53 > */
54 > query(params: { prompt: string | AsyncIterable<SDKUserMessage>; options?: Options }): Promise<Query>;
55 > getSessionMessages(sessionId: string, options?: GetSessionMessagesOptions): Promise<readonly SessionMessage[]>;
56 > listSubagents(sessionId: string, options?: ListSubagentsOptions): Promise<readonly string[]>;
57 > getSubagentMessages(sessionId: string, agentId: string, options?: GetSubagentMessagesOptions): Promise<readonly SessionMessage[]>;
58 >
59 > /**
60 > * True iff the SDK can be loaded WITHOUT a network download — a dev
61 > * override or dev bare-import is available, or a previously-downloaded SDK
62 > * is cached on disk. Eager / background callers (e.g. `listSessions` at
63 > * startup) gate on this so listing sessions never kicks off a multi-second
64 > * cold download before the user has started a session.
65 > */
66 > canLoadWithoutDownload(): Promise<boolean>;
67 >
68 > forkSession(sessionId: string, options?: ForkSessionOptions): Promise<ForkSessionResult>;
69 > deleteSession(sessionId: string, options?: SessionMutationOptions): Promise<void>;
70 > createSdkMcpServer(options: {
71 > name: string;
72 > version?: string;
73 > // SDK signature: `tools?: Array<SdkMcpToolDefinition<any>>`. The `any`
74 > // here is required to match the SDK's own erased generic and to allow
75 > // callers to pass an array of tools whose schemas differ from each other.
76 > // eslint-disable-next-line @typescript-eslint/no-explicit-any
77 > tools?: Array<SdkMcpToolDefinition<any>>;
78 > }): Promise<McpSdkServerConfigWithInstance>;
79 >
80 > tool<Schema extends AnyZodRawShape>(
81 > name: string,
82 > description: string,
83 > inputSchema: Schema,
84 > handler: (args: InferShape<Schema>, extra: unknown) => Promise<CallToolResult>
85 > ): Promise<SdkMcpToolDefinition<Schema>>;
86 > }
87 >
88 > /**
89 > * Narrowed structural slice of `@anthropic-ai/claude-agent-sdk` covering
90 > * exactly the bindings the agent host pulls from the SDK. Production
91 > * `import()` returns the full module which is structurally assignable to
92 > * this interface. Tests usually stub {@link IClaudeAgentSdkService} via
93 > * the DI container, but a few existing suites subclass
94 > * {@link ClaudeAgentSdkService} and override {@link ClaudeAgentSdkService._loadSdk}
95 > * to fault or stub these bindings without having to name every export of
96 > * the SDK module — `_loadSdk` is `protected` for that reason.
97 > */
98 > export interface IClaudeSdkBindings {
99 > listSessions(options?: ListSessionsOptions): Promise<SDKSessionInfo[]>;
100 > getSessionInfo(sessionId: string): Promise<SDKSessionInfo | undefined>;
101 > startup(params: { options: Options; initializeTimeoutMs?: number }): Promise<WarmQuery>;
102 > query(params: { prompt: string | AsyncIterable<SDKUserMessage>; options?: Options }): Query;
103 > getSessionMessages(sessionId: string, options?: GetSessionMessagesOptions): Promise<SessionMessage[]>;
104 > listSubagents(sessionId: string, options?: ListSubagentsOptions): Promise<string[]>;
105 > getSubagentMessages(sessionId: string, agentId: string, options?: GetSubagentMessagesOptions): Promise<SessionMessage[]>;
106 > forkSession(sessionId: string, options?: ForkSessionOptions): Promise<ForkSessionResult>;
107 > deleteSession(sessionId: string, options?: SessionMutationOptions): Promise<void>;
108 > createSdkMcpServer(options: {
109 > name: string;
110 > version?: string;
111 > // eslint-disable-next-line @typescript-eslint/no-explicit-any
112 > tools?: Array<SdkMcpToolDefinition<any>>;
113 > }): McpSdkServerConfigWithInstance;
114 > tool<Schema extends AnyZodRawShape>(
115 > name: string,
116 > description: string,
117 > inputSchema: Schema,
118 > handler: (args: InferShape<Schema>, extra: unknown) => Promise<CallToolResult>
119 > ): SdkMcpToolDefinition<Schema>;
120 > }
121 >
122 > export class ClaudeAgentSdkService implements IClaudeAgentSdkService {
123 > declare readonly _serviceBrand: undefined;
124 >
125 > /**
126 > * Cached resolved bindings. We deliberately cache the *resolved* value,
127 > * not the in-flight promise — if a transient `import()` failure recovers
128 > * (e.g. user fixes a broken `node_modules`), the next call retries.
129 > */
130 > private _sdkModule: IClaudeSdkBindings | undefined;
131 >
132 > /**
133 > * Latched once we've logged a load failure, so a corrupt postinstall
134 > * doesn't flood `error` events on every `listSessions()` call.
135 > */
136 > private _firstLoadFailureLogged = false;
137 >
138 > constructor(
139 > @ILogService private readonly _logService: ILogService, claudeAgentSdkService.ts ×3
140 > @IAgentSdkDownloader private readonly _downloader: IAgentSdkDownloader,
141 > ) { }
143 > async listSessions(): Promise<readonly SDKSessionInfo[]> {
144 > const sdk = await this._getSdk(); claudeAgentSdkService.ts ×2
145 > return sdk.listSessions(undefined);
146 > }
148 > async canLoadWithoutDownload(): Promise<boolean> {
149 // A dev override (explicit SDK root) is always local. So is the dev
150 // bare-import path, which is taken when there is no product config —
151 // `isAvailable` is false exactly in that case. Otherwise the SDK comes
152 // from the downloader, which is only local once it has been cached.
153 if (process.env[AgentHostClaudeSdkRootEnvVar] || !this._downloader.isAvailable(ClaudeSdkPackage)) {
154 return true;
155 }
156 return this._downloader.isSdkResolvableWithoutDownload(ClaudeSdkPackage);
157 }
159 > async getSessionInfo(sessionId: string): Promise<SDKSessionInfo | undefined> {
160 const sdk = await this._getSdk();
161 return sdk.getSessionInfo(sessionId);
162 }
164 > async startup(params: { options: Options; initializeTimeoutMs?: number }): Promise<WarmQuery> {
165 const sdk = await this._getSdk();
166 return sdk.startup(params);
167 }
169 > async query(params: { prompt: string | AsyncIterable<SDKUserMessage>; options?: Options }): Promise<Query> {
170 const sdk = await this._getSdk();
171 return sdk.query(params);
172 }
174 > async getSessionMessages(sessionId: string, options?: GetSessionMessagesOptions): Promise<readonly SessionMessage[]> {
175 const sdk = await this._getSdk();
176 return sdk.getSessionMessages(sessionId, options);
177 }
179 > async listSubagents(sessionId: string, options?: ListSubagentsOptions): Promise<readonly string[]> {
180 > const sdk = await this._getSdk(); claudeAgentSdkService.ts ×2
181 > return sdk.listSubagents(sessionId, options);
182 > }
184 > async getSubagentMessages(sessionId: string, agentId: string, options?: GetSubagentMessagesOptions): Promise<readonly SessionMessage[]> {
185 > const sdk = await this._getSdk(); claudeAgentSdkService.ts ×2
186 > return sdk.getSubagentMessages(sessionId, agentId, options);
187 > }
189 > async forkSession(sessionId: string, options?: ForkSessionOptions): Promise<ForkSessionResult> {
190 const sdk = await this._getSdk();
191 return sdk.forkSession(sessionId, options);
192 }
194 > async deleteSession(sessionId: string, options?: SessionMutationOptions): Promise<void> {
195 const sdk = await this._getSdk();
196 return sdk.deleteSession(sessionId, options);
197 }
199 > async createSdkMcpServer(options: {
200 name: string;
201 version?: string;
202 // eslint-disable-next-line @typescript-eslint/no-explicit-any
203 tools?: Array<SdkMcpToolDefinition<any>>;
204 }): Promise<McpSdkServerConfigWithInstance> {
205 const sdk = await this._getSdk();
206 return sdk.createSdkMcpServer(options);
207 }
209 > async tool<Schema extends AnyZodRawShape>(
210 name: string,
211 description: string,
212 inputSchema: Schema,
213 handler: (args: InferShape<Schema>, extra: unknown) => Promise<CallToolResult>
214 ): Promise<SdkMcpToolDefinition<Schema>> {
215 const sdk = await this._getSdk();
216 return sdk.tool(name, description, inputSchema, handler);
217 }
219 > private async _getSdk(): Promise<IClaudeSdkBindings> {
220 > if (this._sdkModule) { claudeAgentSdkService.ts ×3
221 > return this._sdkModule;
222 > }
223 > try {
224 > this._sdkModule = await this._loadSdk();
225 > return this._sdkModule;
226 > } catch (err) {
227 > if (!this._firstLoadFailureLogged) { claudeAgentSdkService.ts ×2
228 > this._firstLoadFailureLogged = true;
229 > this._logService.error('[Claude] Failed to load @anthropic-ai/claude-agent-sdk', err);
230 > }
231 > throw err;
232 > }
235 > protected async _loadSdk(): Promise<IClaudeSdkBindings> {
236 // 1. Env-var override wins — both for the air-gapped server case
237 // (`--claude-sdk-root` flag) and for developers who want to point
238 // at an out-of-tree SDK build without touching `node_modules`.
239 const override = process.env[AgentHostClaudeSdkRootEnvVar];
240 if (override) {
241 const entry = join(override, 'node_modules', '@anthropic-ai', 'claude-agent-sdk', 'sdk.mjs');
242 return import(pathToFileURL(entry).href);
243 }
244
245 // 2. Built products: load via the downloader (cache → fetch the
246 // per-host tarball described by `product.agentSdks.claude`). Errors
247 // from this path propagate as-is so users see actionable diagnostics
248 // on a CDN outage / corrupt cache / etc., not a misleading
249 // "cannot find module" from a fallback that would never succeed in
250 // a shipped build anyway.
251 //
252 // We use `isAvailable` (env var || product config) — already false
253 // in dev — to discriminate without injecting `INativeEnvironmentService`
254 // here. The env-var branch above already returned, so reaching this
255 // point with `isAvailable === true` means product config is present
256 // and the downloader is the correct path.
257 if (this._downloader.isAvailable(ClaudeSdkPackage)) {
258 const root = await this._downloader.loadSdkRoot(ClaudeSdkPackage, CancellationToken.None);
259 const entry = join(root, 'node_modules', '@anthropic-ai', 'claude-agent-sdk', 'sdk.mjs');
260 return import(pathToFileURL(entry).href);
261 }
262
263 // 3. Dev: bare import resolves via this repo's `node_modules` where
264 // `@anthropic-ai/claude-agent-sdk` is a devDependency. Only reached
265 // when neither the env var nor product config supplied a path —
266 // i.e. exclusively in dev launches.
267 return import('@anthropic-ai/claude-agent-sdk');
268 }
270 >
271 > // #region Compile-time SDK drift detection
272 > //
273 > // Enforce that every method on IClaudeSdkBindings names a real export of
274 > // `@anthropic-ai/claude-agent-sdk` and is assignable from that export's
275 > // type. If the SDK renames or changes the signature of any of these
276 > // exports, the `_assertBindingsMatchSdk` assignment below stops type-
277 > // checking and the build fails — flagging that the shim needs updating.
278 >
279 > type SdkModule = typeof import('@anthropic-ai/claude-agent-sdk');
280 >
281 > type AssertBindingsMatchSdk = {
282 > [K in keyof IClaudeSdkBindings]: K extends keyof SdkModule
283 > ? SdkModule[K] extends IClaudeSdkBindings[K]
284 > ? true
285 > : ['SDK export signature drifted from IClaudeSdkBindings', K, SdkModule[K], IClaudeSdkBindings[K]]
286 > : ['Not an export of @anthropic-ai/claude-agent-sdk', K];
287 > };
288 >
289 > // Forces the mapped type above to be eagerly checked: if any entry is not
290 > // `true`, this assignment fails to compile. Module-local so the runtime
291 > // surface stays clean — the only purpose is the type-level assertion.
292 > const _assertBindingsMatchSdk: { [K in keyof IClaudeSdkBindings]: true } = null as unknown as AssertBindingsMatchSdk;
293 > void _assertBindingsMatchSdk;
294 >
295 > // #endregion