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
110
src/errors.rs
Normal file
110
src/errors.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
//! Error types for the Claude Agent SDK.
|
||||
//!
|
||||
//! Mirrors the exception hierarchy in the upstream Python SDK
|
||||
//! (`claude_agent_sdk._errors`):
|
||||
//!
|
||||
//! - [`Error::CliNotFound`] — `CLINotFoundError`
|
||||
//! - [`Error::CliConnection`] — `CLIConnectionError`
|
||||
//! - [`Error::Process`] — `ProcessError`
|
||||
//! - [`Error::JsonDecode`] — `CLIJSONDecodeError`
|
||||
//! - [`Error::MessageParse`] — `MessageParseError`
|
||||
//!
|
||||
//! All variants inherit from `ClaudeSDKError` in Python; in Rust we expose a
|
||||
//! single `Error` enum and rely on pattern matching (or [`Error::kind`]) to
|
||||
//! distinguish them.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// All errors surfaced by the SDK.
|
||||
///
|
||||
/// The Python SDK uses an exception hierarchy rooted at `ClaudeSDKError`. We
|
||||
/// flatten that into a single enum because Rust pattern matching is more
|
||||
/// ergonomic than `try`/`except` chains, and because most callers only care
|
||||
/// about a small subset (e.g. "did the CLI not exist?" vs everything else).
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// The `claude` CLI binary could not be located on `PATH` or in any of the
|
||||
/// fallback search locations, and no explicit `cli_path` was supplied via
|
||||
/// [`crate::ClaudeAgentOptions::cli_path`].
|
||||
///
|
||||
/// Mirrors Python's `CLINotFoundError`.
|
||||
#[error("{0}")]
|
||||
CliNotFound(String),
|
||||
|
||||
/// Generic connection / transport problem with the CLI subprocess —
|
||||
/// failed to spawn, stdin write failed, transport not ready, etc.
|
||||
///
|
||||
/// Mirrors Python's `CLIConnectionError`.
|
||||
#[error("{0}")]
|
||||
CliConnection(String),
|
||||
|
||||
/// The CLI subprocess exited with a non-zero status before yielding a
|
||||
/// `ResultMessage`.
|
||||
///
|
||||
/// Mirrors Python's `ProcessError`.
|
||||
#[error("{message}{exit_code}{stderr}",
|
||||
exit_code = match exit_code { Some(c) => format!(" (exit code: {c})"), None => String::new() },
|
||||
stderr = match stderr { Some(s) if !s.is_empty() => format!("\nError output: {s}"), _ => String::new() },
|
||||
)]
|
||||
Process {
|
||||
/// Human-readable description (e.g. `"Command failed"`).
|
||||
message: String,
|
||||
/// Subprocess exit code, when known.
|
||||
exit_code: Option<i32>,
|
||||
/// Captured stderr output, when stderr was piped via
|
||||
/// [`crate::ClaudeAgentOptions::capture_stderr`].
|
||||
stderr: Option<String>,
|
||||
},
|
||||
|
||||
/// A line on the CLI's stdout could not be decoded as JSON even after
|
||||
/// buffering up to `max_buffer_size` bytes.
|
||||
///
|
||||
/// Mirrors Python's `CLIJSONDecodeError`.
|
||||
#[error("Failed to decode JSON: {line_preview}...")]
|
||||
JsonDecode {
|
||||
/// First ~100 chars of the offending line, included in the message.
|
||||
line_preview: String,
|
||||
/// The underlying serde error.
|
||||
#[source]
|
||||
source: serde_json::Error,
|
||||
},
|
||||
|
||||
/// A JSON message from the CLI was decoded but did not match any known
|
||||
/// shape, or was missing a required field.
|
||||
///
|
||||
/// Mirrors Python's `MessageParseError`.
|
||||
#[error("Message parse error: {message}")]
|
||||
MessageParse {
|
||||
/// Human-readable description.
|
||||
message: String,
|
||||
/// The raw JSON value the parser was looking at, if available.
|
||||
data: Option<serde_json::Value>,
|
||||
},
|
||||
|
||||
/// Underlying I/O failure (rare — most I/O is surfaced as
|
||||
/// [`Error::CliConnection`]).
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
/// Invalid configuration (e.g. mutually exclusive options).
|
||||
#[error("invalid configuration: {0}")]
|
||||
Config(String),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Construct a [`Error::CliConnection`] from any displayable value.
|
||||
pub(crate) fn conn(msg: impl Into<String>) -> Self {
|
||||
Self::CliConnection(msg.into())
|
||||
}
|
||||
|
||||
/// Construct a [`Error::MessageParse`] with raw data attached.
|
||||
pub(crate) fn parse_with_data(msg: impl Into<String>, data: serde_json::Value) -> Self {
|
||||
Self::MessageParse {
|
||||
message: msg.into(),
|
||||
data: Some(data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience alias for `Result<T, claude_agent_sdk::Error>`.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
564
src/messages.rs
Normal file
564
src/messages.rs
Normal file
|
|
@ -0,0 +1,564 @@
|
|||
//! Message and content-block types emitted by the Claude CLI.
|
||||
//!
|
||||
//! These are the deserialized shapes of the newline-delimited JSON objects the
|
||||
//! CLI writes to stdout when invoked with `--output-format stream-json`. The
|
||||
//! field naming follows the wire format (snake_case), so callers can match the
|
||||
//! Python SDK's field names directly.
|
||||
//!
|
||||
//! Forward-compatibility: unknown variants are surfaced as
|
||||
//! [`Message::Unknown`] / [`ContentBlock::Unknown`] with the raw JSON
|
||||
//! preserved, rather than failing the stream. This mirrors the Python SDK's
|
||||
//! "skip unknown message types" behaviour while still letting Rust callers
|
||||
//! introspect what they got.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
/// A content block inside a [`UserMessage`] or [`AssistantMessage`].
|
||||
///
|
||||
/// The CLI tags blocks with a `type` field; we deserialize via that tag.
|
||||
/// Unknown block types are preserved as [`ContentBlock::Unknown`] so the
|
||||
/// stream is forward-compatible with newer CLI releases.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ContentBlock {
|
||||
/// Plain text emitted by the assistant or echoed from the user.
|
||||
Text(TextBlock),
|
||||
/// Extended-thinking block (only present when thinking is enabled).
|
||||
Thinking(ThinkingBlock),
|
||||
/// Model decided to invoke a client-side tool.
|
||||
ToolUse(ToolUseBlock),
|
||||
/// Result of a previously-issued tool call (returned by the user).
|
||||
ToolResult(ToolResultBlock),
|
||||
/// Server-side tool use (web_search, code_execution, advisor, etc.).
|
||||
ServerToolUse(ServerToolUseBlock),
|
||||
/// Result block returned for a server-side tool call.
|
||||
#[serde(rename = "advisor_tool_result")]
|
||||
ServerToolResult(ServerToolResultBlock),
|
||||
/// Forward-compatibility escape hatch — block type the SDK does not
|
||||
/// recognise. The raw `serde_json::Value` is kept so callers can inspect
|
||||
/// the payload without modifying the SDK.
|
||||
#[serde(other, skip_serializing)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Plain text block.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct TextBlock {
|
||||
/// Text content.
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
/// Extended-thinking block.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ThinkingBlock {
|
||||
/// The thinking text.
|
||||
pub thinking: String,
|
||||
/// Signature used to verify the thinking trace.
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
/// Tool-use block: the model is requesting that a tool be invoked.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ToolUseBlock {
|
||||
/// Unique ID for this tool invocation; pair with the matching
|
||||
/// [`ToolResultBlock::tool_use_id`].
|
||||
pub id: String,
|
||||
/// Tool name (e.g. `"Bash"`, `"Read"`).
|
||||
pub name: String,
|
||||
/// Tool input parameters, opaque to the SDK.
|
||||
pub input: Value,
|
||||
}
|
||||
|
||||
/// Tool-result block: response back to the model for a previous tool call.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ToolResultBlock {
|
||||
/// The `id` of the [`ToolUseBlock`] this result corresponds to.
|
||||
pub tool_use_id: String,
|
||||
/// The tool output. May be a plain string or a structured payload; left
|
||||
/// as opaque JSON because the CLI emits both shapes.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<Value>,
|
||||
/// Whether the tool reported failure.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub is_error: Option<bool>,
|
||||
}
|
||||
|
||||
/// Server-side tool use (executed by the API, not the SDK caller).
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ServerToolUseBlock {
|
||||
/// Unique ID for this server-side invocation.
|
||||
pub id: String,
|
||||
/// Server tool name (e.g. `"web_search"`, `"code_execution"`,
|
||||
/// `"advisor"`).
|
||||
pub name: String,
|
||||
/// Tool input, opaque.
|
||||
pub input: Value,
|
||||
}
|
||||
|
||||
/// Result block returned for a server-side tool call.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ServerToolResultBlock {
|
||||
/// The matching server tool-use ID.
|
||||
pub tool_use_id: String,
|
||||
/// Raw result payload from the API.
|
||||
pub content: Value,
|
||||
}
|
||||
|
||||
// ---- top-level messages ----------------------------------------------------
|
||||
|
||||
/// A message in the conversation stream. Top-level discriminant is the wire
|
||||
/// `type` field.
|
||||
///
|
||||
/// Forward-compatibility: unknown message types arrive as [`Message::Unknown`]
|
||||
/// rather than failing the deserialiser. The Python SDK silently skips them;
|
||||
/// in Rust we keep the raw value so callers can opt into inspection.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Message {
|
||||
/// `{"type": "user", ...}` — user turn (input echoed back, or tool results).
|
||||
User(UserMessage),
|
||||
/// `{"type": "assistant", ...}` — assistant turn with content blocks.
|
||||
Assistant(AssistantMessage),
|
||||
/// `{"type": "result", ...}` — terminal message with usage / cost.
|
||||
Result(ResultMessage),
|
||||
/// `{"type": "system", ...}` — system events (init notifications, etc.).
|
||||
System(SystemMessage),
|
||||
/// `{"type": "stream_event", ...}` — partial streaming events (enabled
|
||||
/// via [`crate::ClaudeAgentOptions::include_partial_messages`]).
|
||||
StreamEvent(StreamEventMessage),
|
||||
/// Anything else — raw JSON preserved verbatim. Includes messages with a
|
||||
/// known top-level shape but a `type` field this SDK version doesn't
|
||||
/// recognise.
|
||||
Unknown(Value),
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Message {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
// Deserialize into a Value first so we can dispatch on `type` without
|
||||
// having serde retry variants. This keeps forward-compat clean: any
|
||||
// unknown `type` (or an entirely shapeless payload) lands in
|
||||
// Message::Unknown verbatim instead of failing the whole stream.
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
let tag = value.get("type").and_then(|v| v.as_str()).map(str::to_owned);
|
||||
match tag.as_deref() {
|
||||
Some("user") => serde_json::from_value(value)
|
||||
.map(Message::User)
|
||||
.map_err(serde::de::Error::custom),
|
||||
Some("assistant") => serde_json::from_value(value)
|
||||
.map(Message::Assistant)
|
||||
.map_err(serde::de::Error::custom),
|
||||
Some("result") => serde_json::from_value(value)
|
||||
.map(Message::Result)
|
||||
.map_err(serde::de::Error::custom),
|
||||
Some("system") => serde_json::from_value(value)
|
||||
.map(Message::System)
|
||||
.map_err(serde::de::Error::custom),
|
||||
Some("stream_event") => serde_json::from_value(value)
|
||||
.map(Message::StreamEvent)
|
||||
.map_err(serde::de::Error::custom),
|
||||
_ => Ok(Message::Unknown(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Message {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
Message::User(u) => u.serialize(serializer),
|
||||
Message::Assistant(a) => a.serialize(serializer),
|
||||
Message::Result(r) => r.serialize(serializer),
|
||||
Message::System(s) => s.serialize(serializer),
|
||||
Message::StreamEvent(e) => e.serialize(serializer),
|
||||
Message::Unknown(v) => v.serialize(serializer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// User message: either the prompt echo or tool results returned to the model.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct UserMessage {
|
||||
/// Wire tag — always `"user"`.
|
||||
#[serde(rename = "type", default = "user_type")]
|
||||
pub message_type: String,
|
||||
/// Inner Anthropic-API-shape message envelope.
|
||||
pub message: UserMessageInner,
|
||||
/// Set when the user message is delivering a tool result inside a sub-agent.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub parent_tool_use_id: Option<String>,
|
||||
/// Opaque tool-result blob, set when `content` is the structured form.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tool_use_result: Option<Value>,
|
||||
/// Session identifier the message belongs to.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub session_id: Option<String>,
|
||||
/// Stable UUID of this message; available for checkpointing.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub uuid: Option<String>,
|
||||
}
|
||||
|
||||
fn user_type() -> String {
|
||||
"user".into()
|
||||
}
|
||||
|
||||
/// Inner envelope of a [`UserMessage`].
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct UserMessageInner {
|
||||
/// Always `"user"`.
|
||||
pub role: String,
|
||||
/// Either a plain string (simple prompt) or a list of content blocks.
|
||||
pub content: UserContent,
|
||||
}
|
||||
|
||||
/// `user.message.content` is polymorphic — either a string (plain prompt) or
|
||||
/// a list of typed content blocks (tool results).
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum UserContent {
|
||||
/// Plain-string content.
|
||||
Text(String),
|
||||
/// Structured content blocks.
|
||||
Blocks(Vec<ContentBlock>),
|
||||
}
|
||||
|
||||
/// Assistant message with the model's content blocks for one turn.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct AssistantMessage {
|
||||
/// Wire tag — always `"assistant"`.
|
||||
#[serde(rename = "type", default = "assistant_type")]
|
||||
pub message_type: String,
|
||||
/// Inner Anthropic-API-shape message envelope (carries `content`,
|
||||
/// `model`, `usage`, etc.).
|
||||
pub message: AssistantMessageInner,
|
||||
/// Set when the assistant turn is from a sub-agent.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub parent_tool_use_id: Option<String>,
|
||||
/// Top-level error tag set when the API returned an error for this turn.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
/// Session identifier.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub session_id: Option<String>,
|
||||
/// Stable UUID for this message.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub uuid: Option<String>,
|
||||
}
|
||||
|
||||
fn assistant_type() -> String {
|
||||
"assistant".into()
|
||||
}
|
||||
|
||||
/// Inner envelope of an [`AssistantMessage`] — mirrors the Anthropic Messages
|
||||
/// API shape.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct AssistantMessageInner {
|
||||
/// Always `"assistant"`.
|
||||
pub role: String,
|
||||
/// Content blocks for this turn.
|
||||
pub content: Vec<ContentBlock>,
|
||||
/// Model identifier (e.g. `"claude-sonnet-4-5"`).
|
||||
pub model: String,
|
||||
/// Anthropic message ID.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<String>,
|
||||
/// Reason the model stopped (e.g. `"end_turn"`, `"tool_use"`,
|
||||
/// `"max_tokens"`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub stop_reason: Option<String>,
|
||||
/// Raw usage dict from the API. Left opaque because the schema changes
|
||||
/// across API versions.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub usage: Option<Value>,
|
||||
}
|
||||
|
||||
/// Terminal `result` message — the iterator stops yielding new messages after
|
||||
/// this arrives. Carries usage, cost, and any error details for the turn.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ResultMessage {
|
||||
/// Wire tag — always `"result"`.
|
||||
#[serde(rename = "type", default = "result_type")]
|
||||
pub message_type: String,
|
||||
/// `subtype` — e.g. `"success"`, `"error_max_turns"`, `"error_during_execution"`.
|
||||
pub subtype: String,
|
||||
/// Wall-clock duration of the turn in milliseconds.
|
||||
pub duration_ms: i64,
|
||||
/// API call duration in milliseconds (subset of `duration_ms`).
|
||||
pub duration_api_ms: i64,
|
||||
/// Whether the turn ended in error.
|
||||
pub is_error: bool,
|
||||
/// Number of turns consumed.
|
||||
pub num_turns: i64,
|
||||
/// Session identifier.
|
||||
pub session_id: String,
|
||||
/// Reason the run stopped, when applicable.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub stop_reason: Option<String>,
|
||||
/// Total cost in USD, when reported by the CLI.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub total_cost_usd: Option<f64>,
|
||||
/// Aggregated usage stats; opaque shape.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub usage: Option<Value>,
|
||||
/// Final string result, when the run produced one (e.g. last assistant
|
||||
/// text content).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub result: Option<Value>,
|
||||
/// Structured output, when configured via `output_format`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub structured_output: Option<Value>,
|
||||
/// Per-model usage breakdown (legacy `modelUsage` field).
|
||||
#[serde(default, rename = "modelUsage", skip_serializing_if = "Option::is_none")]
|
||||
pub model_usage: Option<Value>,
|
||||
/// HTTP status of the failing API call when `is_error` is `true`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub api_error_status: Option<i32>,
|
||||
/// Errors list when the run had structured error info.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub errors: Option<Vec<Value>>,
|
||||
/// Stable UUID for this message.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub uuid: Option<String>,
|
||||
}
|
||||
|
||||
fn result_type() -> String {
|
||||
"result".into()
|
||||
}
|
||||
|
||||
/// System message — metadata events emitted by the CLI (init, task lifecycle,
|
||||
/// hook events, etc.).
|
||||
///
|
||||
/// The Python SDK splits these into more specific subclasses (e.g.
|
||||
/// `TaskStartedMessage`); we keep one variant and let callers branch on
|
||||
/// `subtype` + `data`. Drop them onto the floor if you don't care.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct SystemMessage {
|
||||
/// Wire tag — always `"system"`.
|
||||
#[serde(rename = "type", default = "system_type")]
|
||||
pub message_type: String,
|
||||
/// Discriminator (e.g. `"init"`, `"task_started"`, `"hook_started"`).
|
||||
pub subtype: String,
|
||||
/// Full raw payload, including any subtype-specific fields. The Python
|
||||
/// SDK exposes the full dict on `data`; we do the same.
|
||||
#[serde(flatten)]
|
||||
pub data: Value,
|
||||
}
|
||||
|
||||
fn system_type() -> String {
|
||||
"system".into()
|
||||
}
|
||||
|
||||
/// `stream_event` — partial Anthropic API stream event, only emitted when
|
||||
/// `include_partial_messages` is enabled.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct StreamEventMessage {
|
||||
/// Wire tag — always `"stream_event"`.
|
||||
#[serde(rename = "type", default = "stream_event_type")]
|
||||
pub message_type: String,
|
||||
/// Stable UUID.
|
||||
pub uuid: String,
|
||||
/// Session identifier.
|
||||
pub session_id: String,
|
||||
/// Raw Anthropic API stream event payload, opaque.
|
||||
pub event: Value,
|
||||
/// Sub-agent parent tool-use ID when running inside an agent.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub parent_tool_use_id: Option<String>,
|
||||
}
|
||||
|
||||
fn stream_event_type() -> String {
|
||||
"stream_event".into()
|
||||
}
|
||||
|
||||
impl Message {
|
||||
/// True if this message is a [`Message::Result`] — i.e. the terminal
|
||||
/// message for a turn. Useful for `take_while` / early termination.
|
||||
pub fn is_result(&self) -> bool {
|
||||
matches!(self, Message::Result(_))
|
||||
}
|
||||
|
||||
/// If this message is an [`Message::Assistant`], return its content
|
||||
/// blocks. Returns `None` otherwise.
|
||||
pub fn as_assistant(&self) -> Option<&AssistantMessage> {
|
||||
match self {
|
||||
Message::Assistant(a) => Some(a),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// If this message is a [`Message::Result`], return it. Returns `None`
|
||||
/// otherwise.
|
||||
pub fn as_result(&self) -> Option<&ResultMessage> {
|
||||
match self {
|
||||
Message::Result(r) => Some(r),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_assistant_with_text_block() {
|
||||
let raw = serde_json::json!({
|
||||
"type": "assistant",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": [{"type": "text", "text": "hi"}],
|
||||
"model": "claude-opus-4-5"
|
||||
},
|
||||
"session_id": "s1",
|
||||
"uuid": "u1"
|
||||
});
|
||||
let msg: Message = serde_json::from_value(raw).unwrap();
|
||||
match msg {
|
||||
Message::Assistant(a) => {
|
||||
assert_eq!(a.message.model, "claude-opus-4-5");
|
||||
assert_eq!(a.message.content.len(), 1);
|
||||
match &a.message.content[0] {
|
||||
ContentBlock::Text(t) => assert_eq!(t.text, "hi"),
|
||||
other => panic!("expected TextBlock, got {other:?}"),
|
||||
}
|
||||
}
|
||||
other => panic!("expected Assistant, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_user_with_plain_string_content() {
|
||||
let raw = serde_json::json!({
|
||||
"type": "user",
|
||||
"message": {"role": "user", "content": "hello"},
|
||||
"session_id": "s1"
|
||||
});
|
||||
let msg: Message = serde_json::from_value(raw).unwrap();
|
||||
match msg {
|
||||
Message::User(u) => match u.message.content {
|
||||
UserContent::Text(s) => assert_eq!(s, "hello"),
|
||||
other => panic!("expected Text, got {other:?}"),
|
||||
},
|
||||
other => panic!("expected User, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_user_with_tool_result_blocks() {
|
||||
let raw = serde_json::json!({
|
||||
"type": "user",
|
||||
"message": {
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "tool_result", "tool_use_id": "t1", "content": "ok", "is_error": false}
|
||||
]
|
||||
}
|
||||
});
|
||||
let msg: Message = serde_json::from_value(raw).unwrap();
|
||||
match msg {
|
||||
Message::User(u) => match u.message.content {
|
||||
UserContent::Blocks(blocks) => {
|
||||
assert_eq!(blocks.len(), 1);
|
||||
match &blocks[0] {
|
||||
ContentBlock::ToolResult(t) => {
|
||||
assert_eq!(t.tool_use_id, "t1");
|
||||
assert_eq!(t.is_error, Some(false));
|
||||
}
|
||||
other => panic!("expected ToolResult, got {other:?}"),
|
||||
}
|
||||
}
|
||||
other => panic!("expected Blocks, got {other:?}"),
|
||||
},
|
||||
other => panic!("expected User, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_result_message() {
|
||||
let raw = serde_json::json!({
|
||||
"type": "result",
|
||||
"subtype": "success",
|
||||
"duration_ms": 1234,
|
||||
"duration_api_ms": 1000,
|
||||
"is_error": false,
|
||||
"num_turns": 1,
|
||||
"session_id": "s1",
|
||||
"total_cost_usd": 0.0042
|
||||
});
|
||||
let msg: Message = serde_json::from_value(raw).unwrap();
|
||||
match msg {
|
||||
Message::Result(r) => {
|
||||
assert_eq!(r.subtype, "success");
|
||||
assert_eq!(r.num_turns, 1);
|
||||
assert_eq!(r.total_cost_usd, Some(0.0042));
|
||||
assert!(!r.is_error);
|
||||
}
|
||||
other => panic!("expected Result, got {other:?}"),
|
||||
}
|
||||
assert!(serde_json::from_value::<Message>(serde_json::json!({
|
||||
"type": "result", "subtype": "success",
|
||||
"duration_ms": 1, "duration_api_ms": 1,
|
||||
"is_error": false, "num_turns": 1, "session_id": "s"
|
||||
})).unwrap().is_result());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_system_init_with_passthrough_data() {
|
||||
let raw = serde_json::json!({
|
||||
"type": "system",
|
||||
"subtype": "init",
|
||||
"session_id": "s1",
|
||||
"tools": ["Read", "Write"]
|
||||
});
|
||||
let msg: Message = serde_json::from_value(raw).unwrap();
|
||||
match msg {
|
||||
Message::System(s) => {
|
||||
assert_eq!(s.subtype, "init");
|
||||
assert_eq!(s.data["session_id"], "s1");
|
||||
assert_eq!(s.data["tools"][0], "Read");
|
||||
}
|
||||
other => panic!("expected System, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_top_level_falls_through() {
|
||||
// A message with no recognised shape — must surface as Unknown rather
|
||||
// than failing deser.
|
||||
let raw = serde_json::json!({"type": "rate_limit_event", "rate_limit_info": {"status": "allowed"}, "uuid": "u", "session_id": "s"});
|
||||
let msg: Message = serde_json::from_value(raw.clone()).unwrap();
|
||||
match msg {
|
||||
Message::Unknown(v) => assert_eq!(v, raw),
|
||||
other => panic!("expected Unknown, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thinking_and_tool_use_blocks_roundtrip() {
|
||||
let raw = serde_json::json!({
|
||||
"type": "assistant",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "hmm", "signature": "sig"},
|
||||
{"type": "tool_use", "id": "t1", "name": "Bash", "input": {"cmd": "ls"}}
|
||||
],
|
||||
"model": "m"
|
||||
}
|
||||
});
|
||||
let msg: Message = serde_json::from_value(raw).unwrap();
|
||||
let a = msg.as_assistant().expect("assistant");
|
||||
assert!(matches!(a.message.content[0], ContentBlock::Thinking(_)));
|
||||
match &a.message.content[1] {
|
||||
ContentBlock::ToolUse(t) => {
|
||||
assert_eq!(t.name, "Bash");
|
||||
assert_eq!(t.input["cmd"], "ls");
|
||||
}
|
||||
other => panic!("expected ToolUse, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
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