Skip to main content

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