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