- Errors: single enum mirroring the Python SDK's exception hierarchy (CliNotFound, CliConnection, Process, JsonDecode, MessageParse). Each Python class becomes a variant; the Process variant carries exit code + optional captured stderr. - Options: ClaudeAgentOptions as a plain struct with a builder. Covers the full subprocess-arg surface (system_prompt with String / PresetAppend / File forms, tools / allowed / disallowed, permission_mode, model and fallback, cwd / cli_path / settings, env, extra_args, effort, output_format, mcp_servers, fork_session, include_partial_messages, etc.). Deferred fields documented inline. - Messages: ContentBlock enum (Text / Thinking / ToolUse / ToolResult / ServerToolUse / ServerToolResult / Unknown) with serde tag dispatch and an Unknown fallthrough for forward compatibility. Top-level Message enum uses a hand-written Deserialize that dispatches on the "type" tag so unrecognised top-level frames land in Message::Unknown with the raw JSON preserved (matches the Python SDK's "skip unknown" behaviour, but lets Rust callers introspect). Unit tests cover roundtrip for the common shapes (assistant text + thinking + tool_use, user with both string and structured content, result, system, unknown top-level).
463 lines
15 KiB
Rust
463 lines
15 KiB
Rust
//! Configuration for [`crate::query`] and [`crate::Client`].
|
|
//!
|
|
//! [`ClaudeAgentOptions`] mirrors the Python SDK's dataclass of the same name,
|
|
//! with one significant difference: in Rust the struct uses the builder
|
|
//! pattern. The default-constructed value is always valid; chain
|
|
//! `.with_*` / `.set_*` methods to configure.
|
|
//!
|
|
//! ```
|
|
//! use claude_agent_sdk::ClaudeAgentOptions;
|
|
//!
|
|
//! let opts = ClaudeAgentOptions::new()
|
|
//! .with_system_prompt("You are a helpful assistant.")
|
|
//! .with_max_turns(1)
|
|
//! .with_allowed_tool("Read")
|
|
//! .with_allowed_tool("Write");
|
|
//! ```
|
|
//!
|
|
//! v0.1 ships the subset of fields needed to drive the CLI's `--output-format
|
|
//! stream-json` mode end-to-end. The advanced fields (hooks, `can_use_tool`,
|
|
//! session_store, plugins, sandbox, agents) are deferred — see the
|
|
//! crate-level README for the v0.1 vs v0.2 split.
|
|
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Permission mode for tool execution.
|
|
///
|
|
/// Maps to the CLI's `--permission-mode` flag; values match the Python SDK's
|
|
/// `PermissionMode` literal type.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum PermissionMode {
|
|
/// CLI prompts for dangerous tools (default behaviour).
|
|
Default,
|
|
/// Auto-accept file edits.
|
|
AcceptEdits,
|
|
/// Plan-only mode (no tool execution).
|
|
Plan,
|
|
/// Allow all tools — use with caution.
|
|
BypassPermissions,
|
|
/// Deny anything not pre-approved by allow rules.
|
|
DontAsk,
|
|
/// Model classifier approves or denies each tool call.
|
|
Auto,
|
|
}
|
|
|
|
impl PermissionMode {
|
|
/// CLI-flag form of this mode.
|
|
pub fn as_cli_str(self) -> &'static str {
|
|
match self {
|
|
Self::Default => "default",
|
|
Self::AcceptEdits => "acceptEdits",
|
|
Self::Plan => "plan",
|
|
Self::BypassPermissions => "bypassPermissions",
|
|
Self::DontAsk => "dontAsk",
|
|
Self::Auto => "auto",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Adaptive-thinking effort level.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum Effort {
|
|
/// Minimal thinking, fastest responses.
|
|
Low,
|
|
/// Moderate thinking.
|
|
Medium,
|
|
/// Deep reasoning (default for adaptive-capable models).
|
|
High,
|
|
/// Extended reasoning depth (Opus 4.7 only; falls back to `High` on
|
|
/// other models).
|
|
Xhigh,
|
|
/// Maximum effort.
|
|
Max,
|
|
}
|
|
|
|
impl Effort {
|
|
/// CLI-flag form.
|
|
pub fn as_cli_str(self) -> &'static str {
|
|
match self {
|
|
Self::Low => "low",
|
|
Self::Medium => "medium",
|
|
Self::High => "high",
|
|
Self::Xhigh => "xhigh",
|
|
Self::Max => "max",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// System-prompt configuration.
|
|
///
|
|
/// Mirrors the Python SDK union of `str | SystemPromptPreset | SystemPromptFile`.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum SystemPrompt {
|
|
/// Raw string — passed verbatim via `--system-prompt`.
|
|
String(String),
|
|
/// Append-only on top of the Claude Code default preset, passed via
|
|
/// `--append-system-prompt`.
|
|
PresetAppend(String),
|
|
/// Load the system prompt from a file via `--system-prompt-file`.
|
|
File(PathBuf),
|
|
}
|
|
|
|
/// Options for [`crate::query`] and [`crate::Client`].
|
|
///
|
|
/// Construct with [`ClaudeAgentOptions::new`] (or `default()`) and configure
|
|
/// via the builder methods. The struct is plain data — there is no validation
|
|
/// at construction; mutually exclusive option combinations are rejected at
|
|
/// `connect()` time with a [`crate::Error::Config`].
|
|
///
|
|
/// ## Field coverage
|
|
///
|
|
/// v0.1 supports the fields used by the CLI's stream-json subprocess transport.
|
|
/// Mirrors most of `ClaudeAgentOptions` from the Python SDK. Hook callbacks,
|
|
/// `can_use_tool`, `session_store`, in-process MCP servers, sandbox settings,
|
|
/// and plugins are deferred to v0.2 — see the crate README.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct ClaudeAgentOptions {
|
|
/// Custom system prompt. When `None`, the CLI's empty-prompt default
|
|
/// is used.
|
|
pub system_prompt: Option<SystemPrompt>,
|
|
|
|
/// Restrict the base set of tools available to the model. When `None`,
|
|
/// the CLI default toolset is used. An empty `Vec` disables all built-in
|
|
/// tools. Mirrors the Python SDK's `tools` field.
|
|
pub tools: Option<Vec<String>>,
|
|
|
|
/// Tool names that are auto-allowed without prompting. Maps to the
|
|
/// CLI's `--allowedTools` flag.
|
|
pub allowed_tools: Vec<String>,
|
|
|
|
/// Tool names explicitly disallowed. Maps to `--disallowedTools`.
|
|
pub disallowed_tools: Vec<String>,
|
|
|
|
/// Permission mode for the session.
|
|
pub permission_mode: Option<PermissionMode>,
|
|
|
|
/// Maximum number of conversation turns.
|
|
pub max_turns: Option<i32>,
|
|
|
|
/// Maximum budget in USD; the run stops when exceeded.
|
|
pub max_budget_usd: Option<f64>,
|
|
|
|
/// Continue the most recent conversation in `cwd`.
|
|
pub continue_conversation: bool,
|
|
|
|
/// Session ID to resume.
|
|
pub resume: Option<String>,
|
|
|
|
/// Specific session ID for the conversation (UUID).
|
|
pub session_id: Option<String>,
|
|
|
|
/// Model identifier (e.g. `"claude-sonnet-4-5"`).
|
|
pub model: Option<String>,
|
|
|
|
/// Fallback model when the primary fails.
|
|
pub fallback_model: Option<String>,
|
|
|
|
/// Working directory for the subprocess.
|
|
pub cwd: Option<PathBuf>,
|
|
|
|
/// Explicit path to the `claude` CLI binary. When unset, the SDK
|
|
/// searches `PATH` and a handful of standard install locations.
|
|
pub cli_path: Option<PathBuf>,
|
|
|
|
/// Path or JSON string for an additional settings file (`--settings`).
|
|
pub settings: Option<String>,
|
|
|
|
/// Additional directories Claude may access (`--add-dir`).
|
|
pub add_dirs: Vec<PathBuf>,
|
|
|
|
/// Extra environment variables for the subprocess.
|
|
pub env: HashMap<String, String>,
|
|
|
|
/// Pass-through CLI flags (`--flag value`). Value `None` becomes a
|
|
/// boolean flag.
|
|
pub extra_args: HashMap<String, Option<String>>,
|
|
|
|
/// Maximum stdout buffer size in bytes for the CLI subprocess.
|
|
/// Defaults to 1 MiB if unset.
|
|
pub max_buffer_size: Option<usize>,
|
|
|
|
/// Capture stderr from the subprocess. When `false` (default), stderr
|
|
/// is inherited from the parent process. When `true`, stderr is piped
|
|
/// and surfaced on the [`crate::Error::Process`] error variant.
|
|
pub capture_stderr: bool,
|
|
|
|
/// Include partial assistant streaming events in the message stream.
|
|
pub include_partial_messages: bool,
|
|
|
|
/// Include hook lifecycle events in the message stream.
|
|
pub include_hook_events: bool,
|
|
|
|
/// Resumed sessions fork into a new session ID.
|
|
pub fork_session: bool,
|
|
|
|
/// Adaptive-thinking effort level.
|
|
pub effort: Option<Effort>,
|
|
|
|
/// Output format — passed through as `--json-schema` when shaped as
|
|
/// `{"type": "json_schema", "schema": {...}}`.
|
|
pub output_format: Option<serde_json::Value>,
|
|
|
|
/// MCP server configurations. Either a `serde_json::Value` (passed
|
|
/// verbatim as `--mcp-config` JSON) or a path string. v0.1 does not
|
|
/// support in-process SDK MCP servers — use external stdio/sse/http
|
|
/// servers configured via JSON.
|
|
pub mcp_servers: Option<McpServersConfig>,
|
|
|
|
/// Skip the CLI version check at connect time.
|
|
pub skip_version_check: bool,
|
|
}
|
|
|
|
/// MCP server configuration, passed to the CLI as `--mcp-config`.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum McpServersConfig {
|
|
/// Inline JSON value with shape `{"mcpServers": {...}}` (or a single
|
|
/// server dict). Serialized and passed via `--mcp-config <JSON>`.
|
|
Inline(serde_json::Value),
|
|
/// Path to an MCP config JSON file.
|
|
Path(PathBuf),
|
|
}
|
|
|
|
impl ClaudeAgentOptions {
|
|
/// Construct an empty options struct — equivalent to `Default::default()`.
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Set a plain-string system prompt.
|
|
pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
|
|
self.system_prompt = Some(SystemPrompt::String(prompt.into()));
|
|
self
|
|
}
|
|
|
|
/// Append a custom string to the Claude Code default system prompt.
|
|
pub fn with_append_system_prompt(mut self, append: impl Into<String>) -> Self {
|
|
self.system_prompt = Some(SystemPrompt::PresetAppend(append.into()));
|
|
self
|
|
}
|
|
|
|
/// Read the system prompt from a file at runtime.
|
|
pub fn with_system_prompt_file(mut self, path: impl Into<PathBuf>) -> Self {
|
|
self.system_prompt = Some(SystemPrompt::File(path.into()));
|
|
self
|
|
}
|
|
|
|
/// Restrict the base toolset. Pass `None` for an empty list, or a `Vec`
|
|
/// of tool names.
|
|
pub fn with_tools(mut self, tools: Vec<String>) -> Self {
|
|
self.tools = Some(tools);
|
|
self
|
|
}
|
|
|
|
/// Add one auto-allowed tool name.
|
|
pub fn with_allowed_tool(mut self, tool: impl Into<String>) -> Self {
|
|
self.allowed_tools.push(tool.into());
|
|
self
|
|
}
|
|
|
|
/// Replace the entire allowed-tools list.
|
|
pub fn with_allowed_tools(mut self, tools: Vec<String>) -> Self {
|
|
self.allowed_tools = tools;
|
|
self
|
|
}
|
|
|
|
/// Add one disallowed tool name.
|
|
pub fn with_disallowed_tool(mut self, tool: impl Into<String>) -> Self {
|
|
self.disallowed_tools.push(tool.into());
|
|
self
|
|
}
|
|
|
|
/// Set the permission mode.
|
|
pub fn with_permission_mode(mut self, mode: PermissionMode) -> Self {
|
|
self.permission_mode = Some(mode);
|
|
self
|
|
}
|
|
|
|
/// Cap the number of conversation turns.
|
|
pub fn with_max_turns(mut self, turns: i32) -> Self {
|
|
self.max_turns = Some(turns);
|
|
self
|
|
}
|
|
|
|
/// Cap the budget in USD.
|
|
pub fn with_max_budget_usd(mut self, usd: f64) -> Self {
|
|
self.max_budget_usd = Some(usd);
|
|
self
|
|
}
|
|
|
|
/// Continue the most recent conversation in `cwd`.
|
|
pub fn with_continue_conversation(mut self, on: bool) -> Self {
|
|
self.continue_conversation = on;
|
|
self
|
|
}
|
|
|
|
/// Resume a specific session by ID.
|
|
pub fn with_resume(mut self, session_id: impl Into<String>) -> Self {
|
|
self.resume = Some(session_id.into());
|
|
self
|
|
}
|
|
|
|
/// Pin a session ID (must be a valid UUID).
|
|
pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
|
|
self.session_id = Some(session_id.into());
|
|
self
|
|
}
|
|
|
|
/// Pick a specific model.
|
|
pub fn with_model(mut self, model: impl Into<String>) -> Self {
|
|
self.model = Some(model.into());
|
|
self
|
|
}
|
|
|
|
/// Pick a fallback model.
|
|
pub fn with_fallback_model(mut self, model: impl Into<String>) -> Self {
|
|
self.fallback_model = Some(model.into());
|
|
self
|
|
}
|
|
|
|
/// Set the working directory for the subprocess.
|
|
pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
|
|
self.cwd = Some(cwd.into());
|
|
self
|
|
}
|
|
|
|
/// Override the path to the `claude` CLI binary.
|
|
pub fn with_cli_path(mut self, path: impl Into<PathBuf>) -> Self {
|
|
self.cli_path = Some(path.into());
|
|
self
|
|
}
|
|
|
|
/// Set the `--settings` argument (JSON string or file path).
|
|
pub fn with_settings(mut self, settings: impl Into<String>) -> Self {
|
|
self.settings = Some(settings.into());
|
|
self
|
|
}
|
|
|
|
/// Add an extra directory accessible to Claude.
|
|
pub fn with_add_dir(mut self, dir: impl Into<PathBuf>) -> Self {
|
|
self.add_dirs.push(dir.into());
|
|
self
|
|
}
|
|
|
|
/// Add an environment variable for the subprocess.
|
|
pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
|
|
self.env.insert(key.into(), value.into());
|
|
self
|
|
}
|
|
|
|
/// Add a pass-through CLI flag with a value.
|
|
pub fn with_extra_arg(
|
|
mut self,
|
|
flag: impl Into<String>,
|
|
value: Option<impl Into<String>>,
|
|
) -> Self {
|
|
self.extra_args.insert(flag.into(), value.map(Into::into));
|
|
self
|
|
}
|
|
|
|
/// Cap stdout buffer size.
|
|
pub fn with_max_buffer_size(mut self, size: usize) -> Self {
|
|
self.max_buffer_size = Some(size);
|
|
self
|
|
}
|
|
|
|
/// Capture stderr in errors (otherwise inherited from parent).
|
|
pub fn with_capture_stderr(mut self, on: bool) -> Self {
|
|
self.capture_stderr = on;
|
|
self
|
|
}
|
|
|
|
/// Include partial streaming events in the message stream.
|
|
pub fn with_include_partial_messages(mut self, on: bool) -> Self {
|
|
self.include_partial_messages = on;
|
|
self
|
|
}
|
|
|
|
/// Include hook lifecycle events in the message stream.
|
|
pub fn with_include_hook_events(mut self, on: bool) -> Self {
|
|
self.include_hook_events = on;
|
|
self
|
|
}
|
|
|
|
/// Set adaptive-thinking effort level.
|
|
pub fn with_effort(mut self, effort: Effort) -> Self {
|
|
self.effort = Some(effort);
|
|
self
|
|
}
|
|
|
|
/// Set MCP-server configuration (inline JSON or file path).
|
|
pub fn with_mcp_servers(mut self, cfg: McpServersConfig) -> Self {
|
|
self.mcp_servers = Some(cfg);
|
|
self
|
|
}
|
|
|
|
/// Skip the `claude --version` check at connect time. Mirrors the
|
|
/// `CLAUDE_AGENT_SDK_SKIP_VERSION_CHECK` env var behaviour.
|
|
pub fn with_skip_version_check(mut self, on: bool) -> Self {
|
|
self.skip_version_check = on;
|
|
self
|
|
}
|
|
|
|
/// Set structured-output JSON schema. Pass the full `{"type":
|
|
/// "json_schema", "schema": {...}}` shape.
|
|
pub fn with_output_format(mut self, format: serde_json::Value) -> Self {
|
|
self.output_format = Some(format);
|
|
self
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn builder_chains() {
|
|
let opts = ClaudeAgentOptions::new()
|
|
.with_system_prompt("hi")
|
|
.with_max_turns(3)
|
|
.with_allowed_tool("Read")
|
|
.with_allowed_tool("Bash")
|
|
.with_permission_mode(PermissionMode::AcceptEdits)
|
|
.with_model("claude-sonnet-4-5")
|
|
.with_env("FOO", "bar");
|
|
|
|
assert!(matches!(opts.system_prompt, Some(SystemPrompt::String(ref s)) if s == "hi"));
|
|
assert_eq!(opts.max_turns, Some(3));
|
|
assert_eq!(opts.allowed_tools, vec!["Read", "Bash"]);
|
|
assert_eq!(opts.permission_mode, Some(PermissionMode::AcceptEdits));
|
|
assert_eq!(opts.model.as_deref(), Some("claude-sonnet-4-5"));
|
|
assert_eq!(opts.env.get("FOO").map(String::as_str), Some("bar"));
|
|
}
|
|
|
|
#[test]
|
|
fn permission_mode_cli_str_roundtrip() {
|
|
assert_eq!(PermissionMode::Default.as_cli_str(), "default");
|
|
assert_eq!(PermissionMode::AcceptEdits.as_cli_str(), "acceptEdits");
|
|
assert_eq!(PermissionMode::BypassPermissions.as_cli_str(), "bypassPermissions");
|
|
assert_eq!(PermissionMode::DontAsk.as_cli_str(), "dontAsk");
|
|
assert_eq!(PermissionMode::Auto.as_cli_str(), "auto");
|
|
assert_eq!(PermissionMode::Plan.as_cli_str(), "plan");
|
|
}
|
|
|
|
#[test]
|
|
fn effort_cli_str() {
|
|
assert_eq!(Effort::Low.as_cli_str(), "low");
|
|
assert_eq!(Effort::Xhigh.as_cli_str(), "xhigh");
|
|
assert_eq!(Effort::Max.as_cli_str(), "max");
|
|
}
|
|
|
|
#[test]
|
|
fn default_is_empty() {
|
|
let opts = ClaudeAgentOptions::default();
|
|
assert!(opts.system_prompt.is_none());
|
|
assert!(opts.allowed_tools.is_empty());
|
|
assert_eq!(opts.continue_conversation, false);
|
|
assert_eq!(opts.capture_stderr, false);
|
|
}
|
|
}
|