port message, option, and error types
- 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).
This commit is contained in:
parent
6af8273f98
commit
184b0a786a
3 changed files with 1137 additions and 0 deletions
463
src/options.rs
Normal file
463
src/options.rs
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
//! 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue