@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 +21 -0
- package/README.md +98 -0
- package/claude-sdk.ACP.md +87 -0
- package/dist/chunk-DW2G2NIH.mjs +430 -0
- package/dist/chunk-DW2G2NIH.mjs.map +1 -0
- package/dist/cli.mjs +64 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.ts +263 -0
- package/dist/index.mjs +190 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +70 -0
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
|