claude_agent/
lib.rs

1//! # claude-agent
2//!
3//! Rust SDK for building AI agents with Anthropic's Claude.
4//!
5//! This crate provides a production-ready, memory-efficient way to build AI agents
6//! using the Anthropic Messages API directly, without CLI subprocess dependencies.
7//!
8//! ## Quick Start
9//!
10//! ```rust,no_run
11//! use claude_agent::query;
12//!
13//! #[tokio::main]
14//! async fn main() -> Result<(), claude_agent::Error> {
15//!     let response = query("What is 2 + 2?").await?;
16//!     println!("{}", response);
17//!     Ok(())
18//! }
19//! ```
20//!
21//! ## Full Agent Example
22//!
23//! ```rust,no_run
24//! use claude_agent::{Agent, AgentEvent, ToolAccess};
25//! use futures::StreamExt;
26//! use std::pin::pin;
27//!
28//! #[tokio::main]
29//! async fn main() -> Result<(), claude_agent::Error> {
30//!     let agent = Agent::builder()
31//!         .model("claude-sonnet-4-5")
32//!         .tools(ToolAccess::all())
33//!         .working_dir("./project")
34//!         .build()
35//!         .await?;
36//!
37//!     let stream = agent.execute_stream("Fix the bug").await?;
38//!     let mut stream = pin!(stream);
39//!     while let Some(event) = stream.next().await {
40//!         match event? {
41//!             AgentEvent::Text(text) => print!("{}", text),
42//!             AgentEvent::Complete(result) => {
43//!                 println!("Done: {} tokens", result.total_tokens());
44//!             }
45//!             _ => {}
46//!         }
47//!     }
48//!     Ok(())
49//! }
50//! ```
51
52#![cfg_attr(docsrs, feature(doc_cfg))]
53#![allow(missing_docs)]
54#![deny(rustdoc::broken_intra_doc_links)]
55
56pub mod agent;
57pub mod auth;
58pub mod budget;
59pub mod client;
60pub mod common;
61pub mod config;
62pub mod context;
63pub mod hooks;
64pub mod mcp;
65pub mod observability;
66pub mod output_style;
67pub mod permissions;
68pub mod prelude;
69pub mod prompts;
70pub mod security;
71pub mod session;
72pub mod skills;
73pub mod subagents;
74pub mod tools;
75pub mod types;
76
77// Re-exports for convenience
78pub use agent::{
79    Agent, AgentBuilder, AgentConfig, AgentEvent, AgentMetrics, AgentModelConfig, AgentResult,
80    AgentState, BudgetConfig, CacheConfig, DEFAULT_COMPACT_KEEP_MESSAGES, ExecutionConfig,
81    PromptConfig, SecurityConfig, SystemPromptMode, ToolStats,
82};
83#[cfg(feature = "cli-integration")]
84pub use auth::ClaudeCliProvider;
85pub use auth::{
86    ApiKeyHelper, Auth, AwsCredentialRefresh, AwsCredentials, ChainProvider, Credential,
87    CredentialManager, CredentialProvider, EnvironmentProvider, ExplicitProvider, OAuthConfig,
88    OAuthConfigBuilder,
89};
90pub use budget::{
91    BudgetStatus, BudgetTracker, ModelPricing, OnExceed, PricingTable, PricingTableBuilder,
92    TenantBudget, TenantBudgetManager,
93};
94pub use client::{
95    AnthropicAdapter, BetaConfig, BetaFeature, CircuitBreaker, CircuitConfig, CircuitState, Client,
96    ClientBuilder, ClientCertConfig, CloudProvider, CountTokensRequest, CountTokensResponse,
97    EffortLevel, ExponentialBackoff, FallbackConfig, FallbackTrigger, File, FileData, FileDownload,
98    FileListResponse, FilesClient, GatewayConfig, ModelConfig, ModelType, NetworkConfig,
99    OutputConfig, ProviderAdapter, ProviderConfig, ProxyConfig, Resilience, ResilienceConfig,
100    RetryConfig, UploadFileRequest, strict_schema, transform_for_strict,
101};
102pub use common::{Named, Provider, SourceType, ToolRestricted};
103pub use context::{
104    ContextBuilder, FileMemoryProvider, InMemoryProvider, LeveledMemoryProvider, MemoryContent,
105    MemoryLevel, MemoryLoader, MemoryProvider, PromptOrchestrator, RoutingStrategy, RuleIndex,
106    SkillIndex, StaticContext,
107};
108pub use hooks::{CommandHook, Hook, HookContext, HookEvent, HookInput, HookManager, HookOutput};
109pub use observability::{
110    AgentMetrics as ObservabilityMetrics, MetricsConfig, MetricsRegistry, ObservabilityConfig,
111    SpanContext, TracingConfig,
112};
113pub use output_style::{
114    OutputStyle, OutputStyleSourceType, builtin_styles, default_style, explanatory_style,
115    learning_style,
116};
117#[cfg(feature = "cli-integration")]
118pub use output_style::{OutputStyleLoader, SystemPromptGenerator};
119pub use permissions::{PermissionDecision, PermissionMode, PermissionPolicy, PermissionResult};
120pub use session::{
121    CompactExecutor, CompactStrategy, ExecutionGuard, QueueError, Session, SessionConfig,
122    SessionError, SessionId, SessionManager, SessionMessage, SessionResult, SessionState,
123    ToolState,
124};
125#[cfg(feature = "cli-integration")]
126pub use skills::FileSkillProvider;
127pub use skills::{
128    ChainSkillProvider, CommandLoader, InMemorySkillProvider, SkillDefinition, SkillExecutor,
129    SkillProviderTrait, SkillRegistry, SkillResult, SkillTool, SlashCommand,
130};
131pub use subagents::{
132    ChainSubagentProvider, InMemorySubagentProvider, SubagentDefinition, SubagentProviderTrait,
133    SubagentRegistry, builtin_subagents, find_builtin,
134};
135#[cfg(feature = "cli-integration")]
136pub use subagents::{FileSubagentProvider, SubagentLoader};
137pub use tools::{
138    ExecutionContext, SchemaTool, Tool, ToolAccess, ToolRegistry, ToolRegistryBuilder,
139};
140pub use types::{
141    CompactResult, ContentBlock, DocumentBlock, ImageSource, Message, Role, ToolError, ToolOutput,
142    UserLocation, WebSearchTool,
143};
144
145// MCP re-exports
146pub use mcp::{
147    McpContent, McpError, McpManager, McpResourceDefinition, McpResult, McpServerConfig,
148    McpServerInfo, McpServerState, McpToolDefinition, McpToolResult, ReconnectPolicy,
149};
150
151// Security re-exports
152pub use security::{SecurityContext, SecurityContextBuilder};
153
154#[cfg(feature = "aws")]
155pub use client::BedrockAdapter;
156#[cfg(feature = "azure")]
157pub use client::FoundryAdapter;
158#[cfg(feature = "gcp")]
159pub use client::VertexAdapter;
160
161/// Error type for claude-agent operations.
162///
163/// All errors include actionable context to help diagnose and resolve issues.
164#[derive(Debug, thiserror::Error)]
165#[non_exhaustive]
166pub enum Error {
167    /// API returned an error response.
168    #[error("API error (HTTP {status}): {message}", status = status.map(|s| s.to_string()).unwrap_or_else(|| "unknown".into()))]
169    Api {
170        message: String,
171        status: Option<u16>,
172        error_type: Option<String>,
173    },
174
175    /// Authentication failed.
176    #[error("Authentication failed: {message}")]
177    Auth { message: String },
178
179    /// Network connectivity or request failed.
180    #[error("Network request failed: {0}")]
181    Network(#[from] reqwest::Error),
182
183    /// JSON serialization or deserialization failed.
184    #[error("JSON parsing failed: {0}")]
185    Json(#[from] serde_json::Error),
186
187    /// Failed to parse response or configuration.
188    #[error("Parse error: {0}")]
189    Parse(String),
190
191    /// Tool execution failed.
192    #[error("Tool execution failed: {0}")]
193    Tool(#[from] types::ToolError),
194
195    /// Invalid or missing configuration.
196    #[error("Configuration error: {0}")]
197    Config(String),
198
199    /// File system operation failed.
200    #[error("IO error: {0}")]
201    Io(#[from] std::io::Error),
202
203    /// API rate limit exceeded.
204    #[error("Rate limit exceeded{}", match retry_after {
205        Some(d) => format!(", retry in {:.0}s", d.as_secs_f64()),
206        None => String::new(),
207    })]
208    RateLimit {
209        retry_after: Option<std::time::Duration>,
210    },
211
212    /// Context window token limit exceeded.
213    #[error("Context limit exceeded: {current}/{max} tokens ({:.0}% used)", (*current as f64 / *max as f64) * 100.0)]
214    ContextOverflow { current: usize, max: usize },
215
216    /// Operation exceeded timeout.
217    #[error("Operation timed out after {:.1}s", .0.as_secs_f64())]
218    Timeout(std::time::Duration),
219
220    /// Request parameters are invalid.
221    #[error("Invalid request: {0}")]
222    InvalidRequest(String),
223
224    /// Streaming response error.
225    #[error("Stream error: {0}")]
226    Stream(String),
227
228    /// Required environment variable missing or invalid.
229    #[error("Environment variable error: {0}")]
230    Env(#[from] std::env::VarError),
231
232    /// Operation not supported by the current provider.
233    #[error("{operation} is not supported by {provider}")]
234    NotSupported {
235        provider: &'static str,
236        operation: &'static str,
237    },
238
239    /// Operation blocked by permission policy.
240    #[error("Permission denied: {0}")]
241    Permission(String),
242
243    /// Budget limit exceeded.
244    #[error("Budget exceeded: ${used:.2} used (limit: ${limit:.2}, over by ${:.2})", used - limit)]
245    BudgetExceeded { used: f64, limit: f64 },
246
247    /// Model is temporarily overloaded.
248    #[error("Model {model} is overloaded, try again later")]
249    ModelOverloaded { model: String },
250
251    /// Session operation failed.
252    #[error("Session error: {0}")]
253    Session(String),
254
255    /// MCP server communication failed.
256    #[error("MCP error: {0}")]
257    Mcp(String),
258
259    /// System resource limit reached (memory, processes, etc.)
260    #[error("Resource exhausted: {0}")]
261    ResourceExhausted(String),
262
263    /// Hook execution failed (blockable hooks only).
264    #[error("Hook '{hook}' failed: {reason}")]
265    HookFailed { hook: String, reason: String },
266
267    /// Hook timed out (blockable hooks only).
268    #[error("Hook '{hook}' timed out after {duration_secs}s")]
269    HookTimeout { hook: String, duration_secs: u64 },
270}
271
272/// Error category for unified error handling.
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub enum ErrorCategory {
275    /// Authentication or authorization failures (401, 403)
276    Authorization,
277    /// Configuration, parsing, or setup errors
278    Configuration,
279    /// Network, rate limit, or transient errors that may succeed on retry
280    Transient,
281    /// Session, MCP, or other stateful operation errors
282    Stateful,
283    /// Internal errors (IO, JSON, unexpected states)
284    Internal,
285    /// Resource limits (budget, context, timeout)
286    ResourceLimit,
287}
288
289impl Error {
290    pub fn auth(message: impl Into<String>) -> Self {
291        Error::Auth {
292            message: message.into(),
293        }
294    }
295
296    pub fn category(&self) -> ErrorCategory {
297        match self {
298            Error::Auth { .. } => ErrorCategory::Authorization,
299            Error::Api {
300                status: Some(401 | 403),
301                ..
302            } => ErrorCategory::Authorization,
303            Error::Permission(_) | Error::HookFailed { .. } | Error::HookTimeout { .. } => {
304                ErrorCategory::Authorization
305            }
306
307            Error::Config(_) | Error::Parse(_) | Error::Env(_) | Error::InvalidRequest(_) => {
308                ErrorCategory::Configuration
309            }
310
311            Error::Network(_) | Error::RateLimit { .. } | Error::ModelOverloaded { .. } => {
312                ErrorCategory::Transient
313            }
314            Error::Api {
315                status: Some(500..=599),
316                ..
317            } => ErrorCategory::Transient,
318
319            Error::Session(_) | Error::Mcp(_) | Error::Stream(_) => ErrorCategory::Stateful,
320
321            Error::BudgetExceeded { .. }
322            | Error::ContextOverflow { .. }
323            | Error::Timeout(_)
324            | Error::ResourceExhausted(_) => ErrorCategory::ResourceLimit,
325
326            Error::Io(_)
327            | Error::Json(_)
328            | Error::Tool(_)
329            | Error::Api { .. }
330            | Error::NotSupported { .. } => ErrorCategory::Internal,
331        }
332    }
333
334    pub fn is_authorization_error(&self) -> bool {
335        self.category() == ErrorCategory::Authorization
336    }
337
338    pub fn is_configuration_error(&self) -> bool {
339        self.category() == ErrorCategory::Configuration
340    }
341
342    pub fn is_resource_limit(&self) -> bool {
343        self.category() == ErrorCategory::ResourceLimit
344    }
345
346    pub fn is_retryable(&self) -> bool {
347        self.category() == ErrorCategory::Transient
348    }
349
350    pub fn is_unauthorized(&self) -> bool {
351        matches!(
352            self,
353            Error::Api {
354                status: Some(401),
355                ..
356            } | Error::Auth { .. }
357        )
358    }
359
360    pub fn is_overloaded(&self) -> bool {
361        match self {
362            Error::Api {
363                status: Some(529 | 503),
364                ..
365            } => true,
366            Error::Api {
367                error_type: Some(t),
368                ..
369            } if t.contains("overloaded") => true,
370            Error::Api { message, .. } if message.to_lowercase().contains("overloaded") => true,
371            Error::ModelOverloaded { .. } => true,
372            _ => false,
373        }
374    }
375
376    pub fn status_code(&self) -> Option<u16> {
377        match self {
378            Error::Api { status, .. } => *status,
379            _ => None,
380        }
381    }
382
383    pub fn retry_after(&self) -> Option<std::time::Duration> {
384        match self {
385            Error::RateLimit { retry_after } => *retry_after,
386            _ => None,
387        }
388    }
389}
390
391impl From<config::ConfigError> for Error {
392    fn from(err: config::ConfigError) -> Self {
393        match err {
394            config::ConfigError::NotFound { key } => {
395                Error::Config(format!("Key not found: {}", key))
396            }
397            config::ConfigError::InvalidValue { key, message } => {
398                Error::Config(format!("Invalid value for {}: {}", key, message))
399            }
400            config::ConfigError::Serialization(e) => Error::Json(e),
401            config::ConfigError::Io(e) => Error::Io(e),
402            config::ConfigError::Env(e) => Error::Env(e),
403            config::ConfigError::Provider { message } => Error::Config(message),
404            config::ConfigError::ValidationErrors(errors) => Error::Config(errors.to_string()),
405        }
406    }
407}
408
409impl From<context::ContextError> for Error {
410    fn from(err: context::ContextError) -> Self {
411        match err {
412            context::ContextError::Source { message } => Error::Config(message),
413            context::ContextError::TokenBudgetExceeded { current, limit } => {
414                Error::ContextOverflow {
415                    current: current as usize,
416                    max: limit as usize,
417                }
418            }
419            context::ContextError::SkillNotFound { name } => {
420                Error::Config(format!("Skill not found: {}", name))
421            }
422            context::ContextError::RuleNotFound { name } => {
423                Error::Config(format!("Rule not found: {}", name))
424            }
425            context::ContextError::Parse { message } => Error::Parse(message),
426            context::ContextError::Io(e) => Error::Io(e),
427        }
428    }
429}
430
431impl From<session::SessionError> for Error {
432    fn from(err: session::SessionError) -> Self {
433        match err {
434            session::SessionError::NotFound { id } => {
435                Error::Config(format!("Session not found: {}", id))
436            }
437            session::SessionError::Expired { id } => {
438                Error::Config(format!("Session expired: {}", id))
439            }
440            session::SessionError::PermissionDenied { reason } => Error::auth(reason),
441            session::SessionError::Storage { message } => Error::Config(message),
442            session::SessionError::PersistenceError(msg) => Error::Config(msg),
443            session::SessionError::Serialization(e) => Error::Json(e),
444            session::SessionError::Compact { message } => Error::Config(message),
445            session::SessionError::Context(e) => e.into(),
446            session::SessionError::Plan { message } => Error::Config(message),
447        }
448    }
449}
450
451impl From<security::SecurityError> for Error {
452    fn from(err: security::SecurityError) -> Self {
453        match err {
454            security::SecurityError::Io(e) => Error::Io(e),
455            security::SecurityError::ResourceLimit(msg) => Error::ResourceExhausted(msg),
456            security::SecurityError::BashBlocked(msg) => Error::Permission(msg),
457            security::SecurityError::DeniedPath(path) => {
458                Error::Permission(format!("denied path: {}", path.display()))
459            }
460            security::SecurityError::PathEscape(path) => {
461                Error::Permission(format!("path escapes sandbox: {}", path.display()))
462            }
463            security::SecurityError::NotWithinSandbox(path) => {
464                Error::Permission(format!("path not within sandbox: {}", path.display()))
465            }
466            security::SecurityError::InvalidPath(msg) => Error::Config(msg),
467            security::SecurityError::AbsoluteSymlink(path) => Error::Permission(format!(
468                "absolute symlink outside sandbox: {}",
469                path.display()
470            )),
471            security::SecurityError::SymlinkDepthExceeded { path, max } => Error::Permission(
472                format!("symlink depth exceeded (max {}): {}", max, path.display()),
473            ),
474        }
475    }
476}
477
478impl From<security::sandbox::SandboxError> for Error {
479    fn from(err: security::sandbox::SandboxError) -> Self {
480        match err {
481            security::sandbox::SandboxError::Io(e) => Error::Io(e),
482            security::sandbox::SandboxError::NotSupported => {
483                Error::Config("sandbox not supported on this platform".into())
484            }
485            security::sandbox::SandboxError::NotAvailable(msg) => {
486                Error::Config(format!("sandbox not available: {}", msg))
487            }
488            _ => Error::Config(err.to_string()),
489        }
490    }
491}
492
493impl From<mcp::McpError> for Error {
494    fn from(err: mcp::McpError) -> Self {
495        match err {
496            mcp::McpError::Io(e) => Error::Io(e),
497            mcp::McpError::Json(e) => Error::Json(e),
498            _ => Error::Mcp(err.to_string()),
499        }
500    }
501}
502
503pub type Result<T> = std::result::Result<T, Error>;
504
505/// Simple query function for one-shot requests
506pub async fn query(prompt: &str) -> Result<String> {
507    let client = Client::builder().auth(Auth::FromEnv).await?.build().await?;
508    client.query(prompt).await
509}
510
511/// Query with a specific model
512pub async fn query_with_model(model: &str, prompt: &str) -> Result<String> {
513    use client::CreateMessageRequest;
514    let client = Client::builder().auth(Auth::FromEnv).await?.build().await?;
515    let request =
516        CreateMessageRequest::new(model, vec![types::Message::user(prompt)]).with_max_tokens(8192);
517    let response = client.send(request).await?;
518    Ok(response.text())
519}
520
521/// Stream a response for one-shot requests
522pub async fn stream(
523    prompt: &str,
524) -> Result<impl futures::Stream<Item = Result<String>> + Send + 'static + use<>> {
525    let client = Client::builder().auth(Auth::FromEnv).await?.build().await?;
526    client.stream(prompt).await
527}
528
529#[cfg(test)]
530mod tests {
531    use super::*;
532
533    #[test]
534    fn test_error_display() {
535        let err = Error::Api {
536            message: "Invalid API key".to_string(),
537            status: Some(401),
538            error_type: None,
539        };
540        assert!(err.to_string().contains("Invalid API key"));
541    }
542
543    #[test]
544    fn test_error_is_retryable() {
545        let rate_limit = Error::RateLimit { retry_after: None };
546        assert!(rate_limit.is_retryable());
547
548        let server_error = Error::Api {
549            message: "Internal error".to_string(),
550            status: Some(500),
551            error_type: None,
552        };
553        assert!(server_error.is_retryable());
554
555        let auth_error = Error::auth("Invalid token");
556        assert!(!auth_error.is_retryable());
557    }
558
559    #[test]
560    fn test_config_error_conversion() {
561        let config_err = config::ConfigError::NotFound {
562            key: "api_key".to_string(),
563        };
564        let err: Error = config_err.into();
565        assert!(matches!(err, Error::Config(_)));
566    }
567}