@agentproto/adapter-claude-sdk 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 agentproto contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # @agentproto/adapter-claude-sdk
2
+
3
+ First-party agentproto adapter that runs **Claude Code's agent harness as a
4
+ library** — the Claude Agent SDK's headless `query()` async generator — behind
5
+ an AIP-44 ACP server. The daemon spawns it like any other agent-CLI arm; a user
6
+ can launch it standalone via `agentproto-claude-sdk acp`.
7
+
8
+ I/O stays **100% Anthropic-native**: the SDK's message stream is relayed to ACP
9
+ `session/update`s with no translation of the model I/O.
10
+
11
+ ## Why wrap the SDK directly?
12
+
13
+ The `@agentproto/adapter-claude-code` adapter wraps the third-party
14
+ `@agentclientprotocol/claude-agent-acp` bridge. Driving the SDK directly instead
15
+ buys us:
16
+
17
+ - **Clean model pinning** — `options.model`, no spawn-arg rejection (issue #186).
18
+ - **Native usage telemetry** — a `usage_update` per turn (tokens + cost).
19
+ - **Custom base URL** — `base_url` → `ANTHROPIC_BASE_URL`, so the same
20
+ Anthropic-native harness can front real Anthropic, Bedrock/Vertex/Azure, or an
21
+ Anthropic-compatible gateway (LiteLLM / claude-code-router). Gateway-side
22
+ translation is out of scope.
23
+
24
+ ## Usage
25
+
26
+ ```bash
27
+ # Spawned by the daemon as the `claude-sdk` arm, or standalone:
28
+ agentproto-claude-sdk acp [--model claude-opus-4-8] \
29
+ [--base-url https://gateway.example/v1] [--auth-token <token>] [--thinking]
30
+ ```
31
+
32
+ Auth is read from the spawn env: `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN`,
33
+ or `CLAUDE_CODE_USE_BEDROCK` / `CLAUDE_CODE_USE_VERTEX` / `CLAUDE_CODE_USE_FOUNDRY`.
34
+
35
+ ## Options (AIP-45 manifest)
36
+
37
+ | id | type | applied as |
38
+ | ------------ | ------- | ------------------------------------------------------- |
39
+ | `model` | string | `--model <id>` → SDK `options.model` |
40
+ | `base_url` | string | `ANTHROPIC_BASE_URL` in the child env (see below) |
41
+ | `auth_token` | string | `ANTHROPIC_AUTH_TOKEN` in the child env (`Bearer` auth) |
42
+ | `thinking` | boolean | `--thinking` → SDK `options.thinking = { type: enabled }` |
43
+
44
+ Injected MCP servers (`session/new.mcpServers`) forward to SDK
45
+ `options.mcpServers`, so the daemon can mount a scoped toolset like any other
46
+ arm.
47
+
48
+ ### Anthropic-compatible gateways
49
+
50
+ `base_url` + `auth_token` point one spawn at an Anthropic-compatible gateway
51
+ with a per-spawn Bearer key (the ambient `ANTHROPIC_API_KEY` is for real
52
+ Anthropic). `auth_token` becomes `ANTHROPIC_AUTH_TOKEN`, which the SDK sends as
53
+ `Authorization: Bearer <token>` — accepted by Moonshot, OpenRouter, and DeepSeek.
54
+ The token value is never logged.
55
+
56
+ When `base_url` is set the adapter enters **gateway mode**: it pins every model
57
+ tier the harness might internally request — `ANTHROPIC_MODEL`,
58
+ `ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`,
59
+ `ANTHROPIC_DEFAULT_HAIKU_MODEL`, `ANTHROPIC_SMALL_FAST_MODEL` — to the resolved
60
+ `model`. Without this, a single-model gateway (e.g. Moonshot serving only
61
+ `kimi-k2.7-code`) rejects the harness's background `claude-haiku-*` requests.
62
+ Native Anthropic (no `base_url`) leaves tier routing untouched.
63
+
64
+ Verified live: Moonshot `https://api.moonshot.ai/anthropic` +
65
+ `model=kimi-k2.7-code`, and OpenRouter `https://openrouter.ai/api/v1`, both
66
+ return valid Anthropic Messages format under `Authorization: Bearer`.
67
+
68
+ ### Gateway presets (modes)
69
+
70
+ So you don't hand-type the base URL each spawn, three modes pre-wire the gateway
71
+ endpoint (`base_url`/`auth_token`/`thinking` still work manually for anything
72
+ else):
73
+
74
+ | mode | pre-wires | you supply |
75
+ | ----------- | --------------------------------------------------------------- | ----------------------------------- |
76
+ | `default` | nothing — native Anthropic | `ANTHROPIC_API_KEY` |
77
+ | `moonshot` | `ANTHROPIC_BASE_URL` + `model=kimi-k2.7-code` + `--thinking` | `auth_token` (Moonshot key) |
78
+ | `openrouter`| `ANTHROPIC_BASE_URL` | `model` (slug) + `auth_token` (key) |
79
+ | `deepseek` | `ANTHROPIC_BASE_URL` + `model=deepseek-v4-pro` | `auth_token` (DeepSeek key) |
80
+
81
+ `mode: moonshot` is a one-pick Kimi run; override `model` for another Moonshot
82
+ model. `mode: openrouter` still needs a `model` (e.g. `z-ai/glm-5.2`,
83
+ `deepseek/deepseek-v4-pro`, `moonshotai/kimi-k2`). `mode: deepseek` defaults to
84
+ `deepseek-v4-pro`; override `model` for `deepseek-v4-flash`. The `auth_token`
85
+ is the gateway key — the ambient `ANTHROPIC_API_KEY` stays for real Anthropic.
86
+ DeepSeek isn't thinking-gated, so unlike `moonshot` no `--thinking` is forced.
87
+
88
+ ### Extended thinking
89
+
90
+ Some gateway models are thinking-gated — `kimi-k2.7-code` rejects any request
91
+ that omits `thinking` (`invalid thinking: only type=enabled is allowed for this
92
+ model`). The SDK's `query()` exposes `Options.thinking?: ThinkingConfig`
93
+ (`{ type: 'adaptive' } | { type: 'enabled', budgetTokens? } | { type:
94
+ 'disabled' }`); the `thinking` boolean option pass-through sets
95
+ `options.thinking = { type: 'enabled' }`. Off by default so native Claude models
96
+ keep their own (adaptive) thinking behaviour.
97
+
98
+ See [`claude-sdk.ACP.md`](./claude-sdk.ACP.md) for the full wire profile.
@@ -0,0 +1,87 @@
1
+ # claude-sdk — ACP wire profile
2
+
3
+ AIP-44 ACP profile for the first-party Claude Agent SDK adapter. The agent side
4
+ is implemented in `src/acp-host.ts` against `@agentclientprotocol/sdk`'s
5
+ `AgentSideConnection`, spawned over stdio (`agentproto-claude-sdk acp`). It
6
+ drives the Claude Agent SDK's headless `query()` and relays the SDK's native
7
+ message stream as ACP `session/update`s. I/O stays 100% Anthropic-native.
8
+
9
+ ## Lifecycle
10
+
11
+ | Method | Behaviour |
12
+ | ----------------- | --------------------------------------------------------------- |
13
+ | `initialize` | Returns `PROTOCOL_VERSION` + `agentCapabilities.loadSession=false`. |
14
+ | `authenticate` | No-op (`{}`). Auth is read from the spawn env by the SDK. |
15
+ | `session/new` | Allocates a UUID session id and records the injected `mcpServers`. The id is PINNED via the SDK's `options.sessionId` on the first turn, so the ACP and SDK session ids are the same value. |
16
+ | `session/prompt` | Extracts the user's text content blocks, drives `query({ prompt, options })`, and relays the SDK stream: `assistant` text → `agent_message_chunk`, `tool_use` → `tool_call`, `tool_result` → `tool_call_update`, `result` → `usage_update`. Resolves with `stopReason: end_turn` (or `cancelled` / `refusal`). |
17
+ | `session/cancel` | Aborts the in-flight turn (via the SDK `options.abortController`). |
18
+
19
+ ## Message mapping
20
+
21
+ | SDK message (`SDKMessage`) | ACP `session/update` |
22
+ | --------------------------------- | ------------------------ |
23
+ | `assistant` → `text` block | `agent_message_chunk` |
24
+ | `assistant` → `tool_use` block | `tool_call` (in_progress)|
25
+ | `user` → `tool_result` block | `tool_call_update` |
26
+ | `result` | `usage_update` (tokens + cost) |
27
+ | `system` / `init` | (captures `session_id`; not surfaced) |
28
+
29
+ ## Usage telemetry
30
+
31
+ The `result` message's native Anthropic usage is mapped to the runtime's
32
+ `usage_update` shape: `size` (context window from `modelUsage`), `used` (tokens
33
+ in context), `cost` (from `total_cost_usd`, in USD), and the `tokensIn` /
34
+ `tokensOut` extension.
35
+
36
+ ## Resume
37
+
38
+ Turns resume via the SDK session store: the first turn pins the session UUID
39
+ (`options.sessionId`), later turns continue it (`options.resume`). There is no
40
+ ACP `session/load` replay surface, so `loadSession` is advertised as `false`.
41
+
42
+ ## Models
43
+
44
+ `model` is a Claude model id (e.g. `claude-opus-4-8`, `claude-sonnet-5`,
45
+ `claude-haiku-4-5-20251001`), applied per spawn via the `--model` arg (manifest
46
+ `model` option's `bin_args_template`) → SDK `options.model`. Default:
47
+ `claude-haiku-4-5-20251001`.
48
+
49
+ ## Base URL / providers
50
+
51
+ `base_url` sets `ANTHROPIC_BASE_URL` in the child env (manifest `base_url`
52
+ option's `env` template), keeping the harness Anthropic-native while pointing at
53
+ real Anthropic, Bedrock/Vertex/Azure, or an Anthropic-compatible gateway.
54
+ `CLAUDE_CODE_USE_BEDROCK` / `CLAUDE_CODE_USE_VERTEX` / `CLAUDE_CODE_USE_FOUNDRY`
55
+ pass through from the spawn env.
56
+
57
+ When `base_url` is set the adapter enters **gateway mode** and pins every
58
+ internal model tier the harness may request to the resolved `model`:
59
+ `ANTHROPIC_MODEL`, `ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`,
60
+ `ANTHROPIC_DEFAULT_HAIKU_MODEL`, `ANTHROPIC_SMALL_FAST_MODEL`. This stops a
61
+ single-model gateway (e.g. Moonshot serving only `kimi-k2.7-code`) from
62
+ receiving a background `claude-haiku-*` request it can't serve. Native Anthropic
63
+ (no `base_url`) leaves tier routing untouched.
64
+
65
+ ## Auth / secrets
66
+
67
+ `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` on the spawn env (direct or via an
68
+ Anthropic-compatible gateway), or the cloud-provider toggles above. No ACP-level
69
+ auth handshake. The `auth_token` option sets `ANTHROPIC_AUTH_TOKEN` in the child
70
+ env (manifest `auth_token` option's `env` template) — the SDK sends it as
71
+ `Authorization: Bearer <token>`, letting one spawn target a gateway (Moonshot,
72
+ OpenRouter, DeepSeek) with a per-spawn Bearer key. The token value is never logged.
73
+
74
+ ## Thinking
75
+
76
+ `thinking` (boolean) appends `--thinking`, which sets SDK
77
+ `options.thinking = { type: "enabled" }` (`Options.thinking?: ThinkingConfig`).
78
+ Required by thinking-gated gateway models such as `kimi-k2.7-code`, which reject
79
+ a request that omits `thinking`. Off by default so native Claude models keep
80
+ their own adaptive thinking.
81
+
82
+ ## Permissions
83
+
84
+ Tool-permission handling defaults to `bypassPermissions` (with the required
85
+ danger flag) so the arm can act unattended inside the daemon's sandbox. Override
86
+ via `CLAUDE_SDK_PERMISSION_MODE` (`default` | `acceptEdits` | `bypassPermissions`
87
+ | `plan` | `dontAsk` | `auto`).
@@ -0,0 +1,430 @@
1
+ import { PROTOCOL_VERSION, ndJsonStream, AgentSideConnection } from '@agentclientprotocol/sdk';
2
+ import { query } from '@anthropic-ai/claude-agent-sdk';
3
+ import { Writable, Readable } from 'stream';
4
+
5
+ /**
6
+ * @agentproto/adapter-claude-sdk v0.1.0-alpha
7
+ * First-party agentproto adapter: Claude Agent SDK headless query() -> ACP server.
8
+ */
9
+
10
+ // src/options.ts
11
+ var DEFAULT_MODEL = "claude-haiku-4-5-20251001";
12
+ var CLOUD_PROVIDER_REDIRECT_TOGGLES = [
13
+ "CLAUDE_CODE_USE_BEDROCK",
14
+ "CLAUDE_CODE_USE_VERTEX",
15
+ "CLAUDE_CODE_USE_FOUNDRY",
16
+ "CLAUDE_CODE_USE_ANTHROPIC_AWS",
17
+ "CLAUDE_CODE_USE_MANTLE",
18
+ "CLAUDE_CODE_USE_GATEWAY"
19
+ ];
20
+ var DEFAULT_IDLE_TIMEOUT_MS = 9e4;
21
+ function mapAcpMcpServers(servers) {
22
+ const out = {};
23
+ for (const server of servers ?? []) {
24
+ if ("type" in server && server.type === "http") {
25
+ out[server.name] = {
26
+ type: "http",
27
+ url: server.url,
28
+ headers: flattenPairs(server.headers)
29
+ };
30
+ } else if ("type" in server && server.type === "sse") {
31
+ out[server.name] = {
32
+ type: "sse",
33
+ url: server.url,
34
+ headers: flattenPairs(server.headers)
35
+ };
36
+ } else {
37
+ out[server.name] = {
38
+ type: "stdio",
39
+ command: server.command,
40
+ args: server.args,
41
+ env: flattenPairs(server.env)
42
+ };
43
+ }
44
+ }
45
+ return out;
46
+ }
47
+ function flattenPairs(pairs) {
48
+ const out = {};
49
+ for (const pair of pairs ?? []) out[pair.name] = pair.value;
50
+ return out;
51
+ }
52
+ function buildQueryOptions(args) {
53
+ const { config, abortController, sessionId, resume, mcpServers } = args;
54
+ const model = config.model ?? DEFAULT_MODEL;
55
+ const baseEnv = args.env ?? process.env;
56
+ const env = { ...baseEnv };
57
+ if (config.baseUrl) {
58
+ env.ANTHROPIC_BASE_URL = config.baseUrl;
59
+ const gatewayKeyEnv = baseEnv.CLAUDE_SDK_GATEWAY_KEY_ENV;
60
+ const bearer = config.authToken ?? baseEnv.ANTHROPIC_AUTH_TOKEN ?? (gatewayKeyEnv ? baseEnv[gatewayKeyEnv] : void 0);
61
+ delete env.ANTHROPIC_API_KEY;
62
+ if (bearer) env.ANTHROPIC_AUTH_TOKEN = bearer;
63
+ else delete env.ANTHROPIC_AUTH_TOKEN;
64
+ for (const key of CLOUD_PROVIDER_REDIRECT_TOGGLES) delete env[key];
65
+ env.ANTHROPIC_MODEL = model;
66
+ env.ANTHROPIC_DEFAULT_OPUS_MODEL = model;
67
+ env.ANTHROPIC_DEFAULT_SONNET_MODEL = model;
68
+ env.ANTHROPIC_DEFAULT_HAIKU_MODEL = model;
69
+ env.ANTHROPIC_SMALL_FAST_MODEL = model;
70
+ } else if (config.authToken) {
71
+ env.ANTHROPIC_AUTH_TOKEN = config.authToken;
72
+ }
73
+ const options = {
74
+ model,
75
+ abortController,
76
+ permissionMode: config.permissionMode ?? "bypassPermissions",
77
+ // Required companion to bypassPermissions — this arm runs unattended.
78
+ allowDangerouslySkipPermissions: true,
79
+ // Hermetic embed: don't silently pull in filesystem settings / CLAUDE.md;
80
+ // the daemon owns the configuration surface.
81
+ settingSources: [],
82
+ // Stream partial (`stream_event`) messages so a long thinking / generation
83
+ // stretch is observable live instead of surfacing only at message
84
+ // boundaries. Two things depend on it: (1) the daemon's output ring — the
85
+ // mapped text/thinking deltas keep `lastOutputAt` advancing during a long
86
+ // busy turn instead of freezing until the next tool boundary; and (2) the
87
+ // turn watchdog (acp-host `#nextMessage`), which resets on every SDK
88
+ // message — without partials, a >90s thinking block (kimi-k2.7-code with
89
+ // `--thinking`) yields NO message and the idle watchdog wrongly aborts a
90
+ // healthy turn. See message-map's `streamEventUpdates`.
91
+ includePartialMessages: true,
92
+ env,
93
+ ...config.cwd ? { cwd: config.cwd } : {},
94
+ ...sessionId ? { sessionId } : {},
95
+ ...resume ? { resume } : {}
96
+ };
97
+ if (config.thinking) options.thinking = { type: "enabled" };
98
+ const mapped = mapAcpMcpServers(mcpServers);
99
+ if (Object.keys(mapped).length > 0) options.mcpServers = mapped;
100
+ return options;
101
+ }
102
+
103
+ // src/message-map.ts
104
+ function toolKindForClaudeTool(name) {
105
+ switch (name) {
106
+ case "Read":
107
+ case "NotebookRead":
108
+ return "read";
109
+ case "Write":
110
+ case "Edit":
111
+ case "MultiEdit":
112
+ case "NotebookEdit":
113
+ return "edit";
114
+ case "Bash":
115
+ case "BashOutput":
116
+ case "KillBash":
117
+ return "execute";
118
+ case "Grep":
119
+ case "Glob":
120
+ return "search";
121
+ case "WebFetch":
122
+ case "WebSearch":
123
+ return "fetch";
124
+ case "Task":
125
+ return "think";
126
+ default:
127
+ return "other";
128
+ }
129
+ }
130
+ function stringField(input, key) {
131
+ if (input && typeof input === "object" && key in input) {
132
+ const value = Reflect.get(input, key);
133
+ if (typeof value === "string") return value;
134
+ }
135
+ return void 0;
136
+ }
137
+ function toolCallTitle(name, input) {
138
+ const hint = stringField(input, "command") ?? stringField(input, "file_path") ?? stringField(input, "path") ?? stringField(input, "pattern") ?? stringField(input, "url");
139
+ return hint ? `${name}: ${hint}` : name;
140
+ }
141
+ function assistantMessageUpdates(msg, suppressText = false) {
142
+ const updates = [];
143
+ for (const block of msg.message.content) {
144
+ if (block.type === "text") {
145
+ if (suppressText || !block.text) continue;
146
+ updates.push({
147
+ sessionUpdate: "agent_message_chunk",
148
+ content: { type: "text", text: block.text }
149
+ });
150
+ } else if (block.type === "tool_use") {
151
+ updates.push({
152
+ sessionUpdate: "tool_call",
153
+ toolCallId: block.id,
154
+ title: toolCallTitle(block.name, block.input),
155
+ kind: toolKindForClaudeTool(block.name),
156
+ status: "in_progress",
157
+ rawInput: block.input
158
+ });
159
+ }
160
+ }
161
+ return updates;
162
+ }
163
+ function streamEventUpdates(msg) {
164
+ const event = msg.event;
165
+ if (stringField(event, "type") !== "content_block_delta") return [];
166
+ const delta = event && typeof event === "object" ? Reflect.get(event, "delta") : void 0;
167
+ switch (stringField(delta, "type")) {
168
+ case "text_delta": {
169
+ const text = stringField(delta, "text");
170
+ return text ? [
171
+ {
172
+ sessionUpdate: "agent_message_chunk",
173
+ content: { type: "text", text }
174
+ }
175
+ ] : [];
176
+ }
177
+ case "thinking_delta": {
178
+ const thinking = stringField(delta, "thinking");
179
+ return thinking ? [
180
+ {
181
+ sessionUpdate: "agent_thought_chunk",
182
+ content: { type: "text", text: thinking }
183
+ }
184
+ ] : [];
185
+ }
186
+ default:
187
+ return [];
188
+ }
189
+ }
190
+ function userMessageUpdates(msg) {
191
+ const content = msg.message.content;
192
+ if (typeof content === "string") return [];
193
+ const updates = [];
194
+ for (const block of content) {
195
+ if (block.type !== "tool_result") continue;
196
+ updates.push({
197
+ sessionUpdate: "tool_call_update",
198
+ toolCallId: block.tool_use_id,
199
+ status: block.is_error ? "failed" : "completed",
200
+ rawOutput: block.content
201
+ });
202
+ }
203
+ return updates;
204
+ }
205
+ function contextWindowOf(msg) {
206
+ let size = 0;
207
+ for (const usage of Object.values(msg.modelUsage)) {
208
+ if (usage.contextWindow > size) size = usage.contextWindow;
209
+ }
210
+ return size;
211
+ }
212
+ function resultUsageUpdate(msg) {
213
+ const u = msg.usage;
214
+ const cacheRead = u.cache_read_input_tokens ?? 0;
215
+ const cacheCreate = u.cache_creation_input_tokens ?? 0;
216
+ const tokensIn = u.input_tokens + cacheRead + cacheCreate;
217
+ const tokensOut = u.output_tokens;
218
+ return {
219
+ sessionUpdate: "usage_update",
220
+ size: contextWindowOf(msg),
221
+ used: tokensIn + tokensOut,
222
+ cost: { amount: msg.total_cost_usd, currency: "USD" },
223
+ tokensIn,
224
+ tokensOut
225
+ };
226
+ }
227
+ function sdkMessageToUpdates(msg, opts = {}) {
228
+ switch (msg.type) {
229
+ case "stream_event":
230
+ return streamEventUpdates(msg);
231
+ case "assistant":
232
+ return assistantMessageUpdates(msg, opts.suppressAssistantText ?? false);
233
+ case "user":
234
+ return userMessageUpdates(msg);
235
+ case "result":
236
+ return [resultUsageUpdate(msg)];
237
+ default:
238
+ return [];
239
+ }
240
+ }
241
+ function systemInitSessionId(msg) {
242
+ if (msg.type === "system" && isInit(msg)) return msg.session_id;
243
+ return null;
244
+ }
245
+ function isInit(msg) {
246
+ return msg.subtype === "init";
247
+ }
248
+ function errorMessage(err) {
249
+ return err instanceof Error ? err.message : String(err);
250
+ }
251
+ var TurnStalledError = class extends Error {
252
+ constructor(idleMs) {
253
+ super(
254
+ `claude-sdk: query() produced no message for ${idleMs}ms \u2014 the model stream stalled and never delivered a terminal result. Aborting the turn to avoid a silent, zero-output hang. If this was a slow but healthy turn, raise CLAUDE_SDK_IDLE_TIMEOUT_MS.`
255
+ );
256
+ this.idleMs = idleMs;
257
+ this.name = "TurnStalledError";
258
+ }
259
+ idleMs;
260
+ };
261
+ function promptText(params) {
262
+ return params.prompt.filter((b) => b.type === "text").map((b) => b.text).join("").trim();
263
+ }
264
+ var ClaudeSdkAcpAgent = class {
265
+ #conn;
266
+ #config;
267
+ #query;
268
+ #sessions = /* @__PURE__ */ new Map();
269
+ constructor(conn, config = {}, query$1 = query) {
270
+ this.#conn = conn;
271
+ this.#config = config;
272
+ this.#query = query$1;
273
+ }
274
+ async initialize(_params) {
275
+ return {
276
+ protocolVersion: PROTOCOL_VERSION,
277
+ agentCapabilities: {
278
+ // Turns resume via the SDK's session store (options.resume), keyed by
279
+ // the pinned session id — but there is no ACP `session/load` replay
280
+ // surface, so we advertise loadSession: false.
281
+ loadSession: false
282
+ }
283
+ };
284
+ }
285
+ async authenticate(_params) {
286
+ return {};
287
+ }
288
+ async newSession(params) {
289
+ const id = crypto.randomUUID();
290
+ this.#sessions.set(id, {
291
+ id,
292
+ mcpServers: params.mcpServers ?? [],
293
+ started: false,
294
+ sdkSessionId: null,
295
+ prompt: null
296
+ });
297
+ return { sessionId: id };
298
+ }
299
+ async prompt(params) {
300
+ const session = this.#sessions.get(params.sessionId);
301
+ if (!session) throw new Error(`unknown session ${params.sessionId}`);
302
+ session.prompt?.abort();
303
+ const ac = new AbortController();
304
+ session.prompt = ac;
305
+ const text = promptText(params);
306
+ try {
307
+ const options = buildQueryOptions({
308
+ config: this.#config,
309
+ abortController: ac,
310
+ // First turn pins the id; later turns resume the SDK session.
311
+ ...session.started ? { resume: session.sdkSessionId ?? session.id } : { sessionId: session.id },
312
+ mcpServers: session.mcpServers
313
+ });
314
+ session.started = true;
315
+ await this.#drive(session, text, options, ac);
316
+ } catch (err) {
317
+ if (ac.signal.aborted && !(err instanceof TurnStalledError)) {
318
+ session.prompt = null;
319
+ return { stopReason: "cancelled" };
320
+ }
321
+ await this.#conn.sessionUpdate({
322
+ sessionId: session.id,
323
+ update: {
324
+ sessionUpdate: "agent_message_chunk",
325
+ content: {
326
+ type: "text",
327
+ text: `
328
+ [claude-sdk error] ${errorMessage(err)}
329
+ `
330
+ }
331
+ }
332
+ });
333
+ session.prompt = null;
334
+ return { stopReason: "refusal" };
335
+ }
336
+ const cancelled = ac.signal.aborted;
337
+ session.prompt = null;
338
+ return { stopReason: cancelled ? "cancelled" : "end_turn" };
339
+ }
340
+ async cancel(params) {
341
+ this.#sessions.get(params.sessionId)?.prompt?.abort();
342
+ }
343
+ /**
344
+ * The daemon applies operator options (model, base_url) at spawn — via the
345
+ * manifest's `bin_args_template` / `env` — then also calls this ACP config
346
+ * hook (its default "config apply" path). Everything is already in effect,
347
+ * so this reports our (empty) runtime-configurable set. Without it the spawn
348
+ * fails with "Method not found: session/set_config_option".
349
+ */
350
+ async setSessionConfigOption(_params) {
351
+ return { configOptions: [] };
352
+ }
353
+ /** No agent-specific modes; accept and ignore so a host that sets one
354
+ * doesn't error. */
355
+ async setSessionMode(_params) {
356
+ return {};
357
+ }
358
+ /** Drive one `query()` turn: adopt the SDK session id from `init`, then map
359
+ * each SDK message to ACP updates and relay them.
360
+ *
361
+ * Iteration is driven manually (rather than `for await`) so an idle watchdog
362
+ * can bound the wait for each next message. If the SDK child wedges mid-turn
363
+ * — stops yielding messages without delivering the terminal `result` and
364
+ * without closing (e.g. a conflicting host env leaked into the subprocess) —
365
+ * the watchdog aborts the turn with a {@link TurnStalledError} instead of
366
+ * awaiting forever with zero output. A defensive safety net; a well-behaved
367
+ * turn (Moonshot included, under a clean env) reaches `result` and this
368
+ * never fires. */
369
+ async #drive(session, text, options, ac) {
370
+ const idleMs = this.#config.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
371
+ const iterator = this.#query({ prompt: text, options })[Symbol.asyncIterator]();
372
+ let sawPartial = false;
373
+ try {
374
+ while (!ac.signal.aborted) {
375
+ const next = await this.#nextMessage(iterator, idleMs, ac);
376
+ if (next.done) break;
377
+ const msg = next.value;
378
+ const sdkId = systemInitSessionId(msg);
379
+ if (sdkId) session.sdkSessionId = sdkId;
380
+ if (msg.type === "stream_event") sawPartial = true;
381
+ for (const update of sdkMessageToUpdates(msg, {
382
+ suppressAssistantText: sawPartial
383
+ })) {
384
+ await this.#conn.sessionUpdate({ sessionId: session.id, update });
385
+ }
386
+ }
387
+ } finally {
388
+ void Promise.resolve(iterator.return?.(void 0)).catch(() => void 0);
389
+ }
390
+ }
391
+ /**
392
+ * Await the next SDK message, but no longer than `idleMs`. On timeout the
393
+ * turn is treated as stalled: abort the shared controller (tearing down the
394
+ * SDK subprocess) and reject with {@link TurnStalledError}. `idleMs <= 0`
395
+ * disables the watchdog and awaits indefinitely (legacy behaviour).
396
+ */
397
+ async #nextMessage(iterator, idleMs, ac) {
398
+ const next = iterator.next();
399
+ if (idleMs <= 0) return next;
400
+ void next.catch(() => void 0);
401
+ let timer;
402
+ const watchdog = new Promise((_resolve, reject) => {
403
+ timer = setTimeout(() => {
404
+ ac.abort();
405
+ reject(new TurnStalledError(idleMs));
406
+ }, idleMs);
407
+ timer.unref();
408
+ });
409
+ try {
410
+ return await Promise.race([next, watchdog]);
411
+ } finally {
412
+ if (timer) clearTimeout(timer);
413
+ }
414
+ }
415
+ };
416
+ function runAcpOverStdio(config = {}) {
417
+ const toClient = Writable.toWeb(process.stdout);
418
+ const fromClient = Readable.toWeb(
419
+ process.stdin
420
+ );
421
+ const stream = ndJsonStream(toClient, fromClient);
422
+ return new AgentSideConnection(
423
+ (conn) => new ClaudeSdkAcpAgent(conn, config),
424
+ stream
425
+ );
426
+ }
427
+
428
+ export { ClaudeSdkAcpAgent, DEFAULT_MODEL, assistantMessageUpdates, buildQueryOptions, mapAcpMcpServers, promptText, resultUsageUpdate, runAcpOverStdio, sdkMessageToUpdates, systemInitSessionId, toolCallTitle, toolKindForClaudeTool, userMessageUpdates };
429
+ //# sourceMappingURL=chunk-DW2G2NIH.mjs.map
430
+ //# sourceMappingURL=chunk-DW2G2NIH.mjs.map