219 lines
7.5 KiB
Markdown
219 lines
7.5 KiB
Markdown
# claude-agent-sdk (Rust)
|
|
|
|
Async Rust SDK for the Claude Agent CLI — a faithful port of the official
|
|
Python [`claude-agent-sdk`](https://github.com/anthropics/claude-agent-sdk-python).
|
|
|
|
This crate wraps the `claude` CLI as a subprocess and exposes its
|
|
newline-delimited JSON stream as ergonomic, strongly-typed Rust values. The
|
|
Python SDK's two entry points map directly:
|
|
|
|
- `query()` for fire-and-forget one-shot prompts.
|
|
- `ClaudeSDKClient` → `claude_agent_sdk::Client` for bidirectional,
|
|
multi-turn sessions.
|
|
|
|
It is an independent, community-maintained port and is not affiliated with or
|
|
endorsed by Anthropic.
|
|
|
|
## Status
|
|
|
|
**v0.0.1** — initial port. Core feature parity for the subprocess wire
|
|
protocol; advanced features (control protocol, hooks, in-process MCP servers,
|
|
session store, sandbox, plugins, agents) are deferred. See
|
|
[scope and known limitations](#scope-and-known-limitations) below.
|
|
|
|
## Requirements
|
|
|
|
- Rust 1.85 or newer (the crate uses the 2024 edition).
|
|
- The `claude` CLI available on `PATH` (or supplied explicitly via
|
|
`ClaudeAgentOptions::with_cli_path()`), and an authenticated Claude
|
|
session. **The SDK does not bundle the CLI binary** — it shells out to
|
|
whatever `claude` it finds.
|
|
|
|
## Install
|
|
|
|
The crate is distributed as a git dependency:
|
|
|
|
```toml
|
|
[dependencies]
|
|
claude-agent-sdk = { git = "https://git.sulkta.com/Sulkta-OSS/claude-agent-sdk-rust" }
|
|
tokio = { version = "1", features = ["full"] }
|
|
tokio-stream = "0.1"
|
|
```
|
|
|
|
Pin to a tag or commit (`rev = "..."` / `tag = "..."`) for reproducible
|
|
builds.
|
|
|
|
## Quick start
|
|
|
|
```rust
|
|
use claude_agent_sdk::{query, ClaudeAgentOptions, Message, ContentBlock};
|
|
use tokio_stream::StreamExt;
|
|
|
|
#[tokio::main]
|
|
async fn main() -> claude_agent_sdk::Result<()> {
|
|
let opts = ClaudeAgentOptions::new()
|
|
.with_system_prompt("You are a helpful assistant.")
|
|
.with_max_turns(1);
|
|
|
|
let mut stream = query("What is 2 + 2?", opts).await?;
|
|
|
|
while let Some(msg) = stream.next().await {
|
|
match msg? {
|
|
Message::Assistant(a) => {
|
|
for block in a.message.content {
|
|
if let ContentBlock::Text(t) = block {
|
|
println!("Claude: {}", t.text);
|
|
}
|
|
}
|
|
}
|
|
Message::Result(r) => {
|
|
if let Some(usd) = r.total_cost_usd {
|
|
println!("Cost: ${:.4}", usd);
|
|
}
|
|
break;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
`query()` returns a stream that ends when the CLI emits its terminal `result`
|
|
message, automatically tearing down the subprocess.
|
|
|
|
## Multi-turn sessions
|
|
|
|
```rust
|
|
use claude_agent_sdk::{Client, ClaudeAgentOptions, Message, ContentBlock};
|
|
use tokio_stream::StreamExt;
|
|
|
|
#[tokio::main]
|
|
async fn main() -> claude_agent_sdk::Result<()> {
|
|
let mut client = Client::new(ClaudeAgentOptions::new()).await?;
|
|
client.connect().await?;
|
|
let mut messages = client.messages();
|
|
|
|
client.send("What's the capital of France?").await?;
|
|
// ...consume messages until a Result frame, then ask a follow-up...
|
|
client.send("And of Germany?").await?;
|
|
|
|
// Drain until the next Result, then disconnect.
|
|
while let Some(msg) = messages.next().await {
|
|
if let Message::Result(_) = msg? {
|
|
break;
|
|
}
|
|
}
|
|
client.disconnect().await?;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
`Client` keeps a single CLI subprocess alive across turns, so follow-up
|
|
prompts don't pay the cold-start cost again.
|
|
|
|
## Configuration
|
|
|
|
Both entry points are configured with `ClaudeAgentOptions`, a builder you
|
|
chain `.with_*()` methods on:
|
|
|
|
```rust
|
|
use claude_agent_sdk::{ClaudeAgentOptions, PermissionMode};
|
|
|
|
let opts = ClaudeAgentOptions::new()
|
|
.with_model("claude-sonnet-4-5")
|
|
.with_permission_mode(PermissionMode::AcceptEdits)
|
|
.with_allowed_tool("Read")
|
|
.with_allowed_tool("Bash")
|
|
.with_cwd("/tmp/my-project")
|
|
.with_max_turns(5);
|
|
```
|
|
|
|
See the `ClaudeAgentOptions` API docs for the full list of supported fields.
|
|
|
|
## Examples
|
|
|
|
Runnable examples live in [`examples/`](examples/). With the `claude` CLI on
|
|
`PATH` and authenticated:
|
|
|
|
```sh
|
|
cargo run --example basic # minimal one-shot query()
|
|
cargo run --example with_options # query() with a configured options builder
|
|
cargo run --example interactive # multi-turn Client session
|
|
```
|
|
|
|
## Building and testing
|
|
|
|
```sh
|
|
cargo build
|
|
cargo test # unit + end-to-end tests. A fake CLI fixture stands in
|
|
# for the real `claude` binary, so the suite needs no
|
|
# network access or authenticated session to run.
|
|
cargo clippy --all-targets
|
|
cargo fmt --check
|
|
```
|
|
|
|
## Mapping the Python SDK
|
|
|
|
| Python | Rust |
|
|
| ---------------------------- | ------------------------------- |
|
|
| `query(prompt=...)` | `query(prompt, opts).await?` |
|
|
| `ClaudeSDKClient` | `Client` |
|
|
| `ClaudeAgentOptions` | `ClaudeAgentOptions` |
|
|
| `AssistantMessage` | `Message::Assistant(_)` |
|
|
| `UserMessage` | `Message::User(_)` |
|
|
| `SystemMessage` | `Message::System(_)` |
|
|
| `ResultMessage` | `Message::Result(_)` |
|
|
| `TextBlock` / `ToolUseBlock` | `ContentBlock::Text/ToolUse(_)` |
|
|
| `CLINotFoundError` | `Error::CliNotFound` |
|
|
| `CLIConnectionError` | `Error::CliConnection` |
|
|
| `ProcessError` | `Error::Process { .. }` |
|
|
| `CLIJSONDecodeError` | `Error::JsonDecode { .. }` |
|
|
| `MessageParseError` | `Error::MessageParse { .. }` |
|
|
|
|
## Scope and known limitations
|
|
|
|
This release covers the core path of the Python SDK:
|
|
|
|
- Subprocess transport with newline-delimited JSON framing.
|
|
- All message and content-block types parsed faithfully.
|
|
- `query()` + `Client` for one-shot and bidirectional use.
|
|
- The `ClaudeAgentOptions` flag surface relevant to subprocess arguments.
|
|
|
|
The following are deferred — see the upstream Python README for the current
|
|
shape of these features:
|
|
|
|
- **Control protocol over JSON-RPC** — `interrupt()`, `set_permission_mode()`,
|
|
`set_model()`, `get_mcp_status()`, etc. The Rust `Client` currently speaks
|
|
only the bare user / assistant / result frames. Adding the control protocol
|
|
unlocks the rest of the API.
|
|
- **`can_use_tool` permission callback** — requires the control protocol.
|
|
- **In-process MCP servers (`@tool` / `create_sdk_mcp_server`)** — needs a
|
|
Rust trait/derive-macro shape that isn't drafted yet.
|
|
- **`HookMatcher`** — the wire format is supported on the CLI side, but the
|
|
Rust callback surface is not designed.
|
|
- **`SessionStore`** mirroring adapter.
|
|
- **`Sandbox` settings**, **plugins**, **agents** dataclass.
|
|
|
|
Each of these degrades gracefully — a caller using a newer CLI sees fewer
|
|
features, not breakage.
|
|
|
|
## Contributing
|
|
|
|
Contributions are welcome. A good change:
|
|
|
|
- keeps the wire protocol faithful to the upstream Python SDK (cite the
|
|
behavior you're matching where it isn't obvious);
|
|
- comes with a test — the end-to-end suite drives a fake CLI over the real
|
|
framing, so most behavior can be covered without a live `claude` binary;
|
|
- passes `cargo fmt --check`, `cargo clippy --all-targets`, and `cargo test`.
|
|
|
|
Open an issue to discuss larger features (especially anything from the
|
|
deferred list above) before sending a big patch.
|
|
|
|
## License
|
|
|
|
MIT. See [`LICENSE`](LICENSE).
|
|
|
|
This is an independent port; the upstream Python SDK is © Anthropic, PBC, and
|
|
the bundled `LICENSE` preserves that notice alongside this port's copyright.
|