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