1
>
/*---------------------------------------------------------------------------------------------
claudeAgentSdkService.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 { 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,
140
@IAgentSdkDownloader private readonly _downloader: IAgentSdkDownloader,
141
) { }
143
>
async listSessions(): Promise<readonly SDKSessionInfo[]> {
144
const sdk = await this._getSdk();
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 —