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, DEFAULT_COMPACT_KEEP_MESSAGES, ExecutionConfig, PromptConfig,
81    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, Session, SessionConfig, SessionId, SessionManager,
122};
123#[cfg(feature = "cli-integration")]
124pub use skills::FileSkillProvider;
125pub use skills::{
126    ChainSkillProvider, CommandLoader, InMemorySkillProvider, SkillDefinition, SkillExecutor,
127    SkillProviderTrait, SkillRegistry, SkillResult, SkillTool, SlashCommand,
128};
129pub use subagents::{
130    ChainSubagentProvider, InMemorySubagentProvider, SubagentDefinition, SubagentProviderTrait,
131    SubagentRegistry, builtin_subagents, find_builtin,
132};
133#[cfg(feature = "cli-integration")]
134pub use subagents::{FileSubagentProvider, SubagentLoader};
135pub use tools::{
136    ExecutionContext, SchemaTool, Tool, ToolAccess, ToolRegistry, ToolRegistryBuilder,
137};
138pub use types::{
139    CompactResult, ContentBlock, DocumentBlock, ImageSource, Message, Role, ToolError, ToolOutput,
140    UserLocation, WebSearchTool,
141};
142
143// MCP re-exports
144pub use mcp::{
145    McpContent, McpError, McpManager, McpResourceDefinition, McpResult, McpServerConfig,
146    McpServerInfo, McpServerState, McpToolDefinition, McpToolResult, ReconnectPolicy,
147};
148
149// Security re-exports
150pub use security::{SecurityContext, SecurityContextBuilder};
151
152#[cfg(feature = "aws")]
153pub use client::BedrockAdapter;
154#[cfg(feature = "azure")]
155pub use client::FoundryAdapter;
156#[cfg(feature = "gcp")]
157pub use client::VertexAdapter;
158
159/// Error type for claude-agent operations
160#[derive(Debug, thiserror::Error)]
161#[non_exhaustive]
162pub enum Error {
163    #[error("API error ({status:?}): {message}")]
164    Api {
165        message: String,
166        status: Option<u16>,
167        error_type: Option<String>,
168    },
169
170    #[error("Authentication error: {message}")]
171    Auth { message: String },
172
173    #[error("Network error: {0}")]
174    Network(#[from] reqwest::Error),
175
176    #[error("JSON error: {0}")]
177    Json(#[from] serde_json::Error),
178
179    #[error("Parse error: {0}")]
180    Parse(String),
181
182    #[error("Tool error: {0}")]
183    Tool(#[from] types::ToolError),
184
185    #[error("Configuration error: {0}")]
186    Config(String),
187
188    #[error("IO error: {0}")]
189    Io(#[from] std::io::Error),
190
191    #[error("Rate limit exceeded, retry after {retry_after:?}")]
192    RateLimit {
193        retry_after: Option<std::time::Duration>,
194    },
195
196    #[error("Context window exceeded: {current} / {max} tokens")]
197    ContextOverflow { current: usize, max: usize },
198
199    #[error("Execution timed out after {0:?}")]
200    Timeout(std::time::Duration),
201
202    #[error("Invalid request: {0}")]
203    InvalidRequest(String),
204
205    #[error("Stream error: {0}")]
206    Stream(String),
207
208    #[error("Environment variable error: {0}")]
209    Env(#[from] std::env::VarError),
210
211    #[error("Operation '{operation}' not supported by provider '{provider}'")]
212    NotSupported {
213        provider: &'static str,
214        operation: &'static str,
215    },
216
217    #[error("Permission denied: {0}")]
218    Permission(String),
219
220    #[error("Budget exceeded: used ${used:.2} of ${limit:.2} limit")]
221    BudgetExceeded { used: f64, limit: f64 },
222
223    #[error("Model overloaded: {model}")]
224    ModelOverloaded { model: String },
225
226    #[error("Session error: {0}")]
227    Session(String),
228
229    #[error("MCP error: {0}")]
230    Mcp(String),
231}
232
233impl Error {
234    pub fn auth(message: impl Into<String>) -> Self {
235        Error::Auth {
236            message: message.into(),
237        }
238    }
239
240    pub fn is_retryable(&self) -> bool {
241        matches!(
242            self,
243            Error::RateLimit { .. }
244                | Error::Network(_)
245                | Error::Api {
246                    status: Some(500..=599),
247                    ..
248                }
249        )
250    }
251
252    pub fn is_unauthorized(&self) -> bool {
253        matches!(
254            self,
255            Error::Api {
256                status: Some(401),
257                ..
258            } | Error::Auth { .. }
259        )
260    }
261
262    pub fn is_overloaded(&self) -> bool {
263        match self {
264            Error::Api {
265                status: Some(529 | 503),
266                ..
267            } => true,
268            Error::Api {
269                error_type: Some(t),
270                ..
271            } if t.contains("overloaded") => true,
272            Error::Api { message, .. } if message.to_lowercase().contains("overloaded") => true,
273            Error::ModelOverloaded { .. } => true,
274            _ => false,
275        }
276    }
277
278    pub fn status_code(&self) -> Option<u16> {
279        match self {
280            Error::Api { status, .. } => *status,
281            _ => None,
282        }
283    }
284
285    pub fn retry_after(&self) -> Option<std::time::Duration> {
286        match self {
287            Error::RateLimit { retry_after } => *retry_after,
288            _ => None,
289        }
290    }
291}
292
293impl From<config::ConfigError> for Error {
294    fn from(err: config::ConfigError) -> Self {
295        match err {
296            config::ConfigError::NotFound { key } => {
297                Error::Config(format!("Key not found: {}", key))
298            }
299            config::ConfigError::InvalidValue { key, message } => {
300                Error::Config(format!("Invalid value for {}: {}", key, message))
301            }
302            config::ConfigError::Serialization(e) => Error::Json(e),
303            config::ConfigError::Io(e) => Error::Io(e),
304            config::ConfigError::Env(e) => Error::Env(e),
305            config::ConfigError::Provider { message } => Error::Config(message),
306            config::ConfigError::ValidationErrors(errors) => Error::Config(errors.to_string()),
307        }
308    }
309}
310
311impl From<context::ContextError> for Error {
312    fn from(err: context::ContextError) -> Self {
313        match err {
314            context::ContextError::Source { message } => Error::Config(message),
315            context::ContextError::TokenBudgetExceeded { current, limit } => {
316                Error::ContextOverflow {
317                    current: current as usize,
318                    max: limit as usize,
319                }
320            }
321            context::ContextError::SkillNotFound { name } => {
322                Error::Config(format!("Skill not found: {}", name))
323            }
324            context::ContextError::RuleNotFound { name } => {
325                Error::Config(format!("Rule not found: {}", name))
326            }
327            context::ContextError::Parse { message } => Error::Parse(message),
328            context::ContextError::Io(e) => Error::Io(e),
329        }
330    }
331}
332
333impl From<session::SessionError> for Error {
334    fn from(err: session::SessionError) -> Self {
335        match err {
336            session::SessionError::NotFound { id } => {
337                Error::Config(format!("Session not found: {}", id))
338            }
339            session::SessionError::Expired { id } => {
340                Error::Config(format!("Session expired: {}", id))
341            }
342            session::SessionError::PermissionDenied { reason } => Error::auth(reason),
343            session::SessionError::Storage { message } => Error::Config(message),
344            session::SessionError::PersistenceError(msg) => Error::Config(msg),
345            session::SessionError::Serialization(e) => Error::Json(e),
346            session::SessionError::Compact { message } => Error::Config(message),
347            session::SessionError::Context(e) => e.into(),
348            session::SessionError::Plan { message } => Error::Config(message),
349        }
350    }
351}
352
353impl From<security::SecurityError> for Error {
354    fn from(err: security::SecurityError) -> Self {
355        match err {
356            security::SecurityError::Io(e) => Error::Io(e),
357            security::SecurityError::BashBlocked(msg) => Error::Permission(msg),
358            security::SecurityError::DeniedPath(path) => {
359                Error::Permission(format!("denied path: {}", path.display()))
360            }
361            security::SecurityError::PathEscape(path) => {
362                Error::Permission(format!("path escapes sandbox: {}", path.display()))
363            }
364            security::SecurityError::NotWithinSandbox(path) => {
365                Error::Permission(format!("path not within sandbox: {}", path.display()))
366            }
367            _ => Error::Permission(err.to_string()),
368        }
369    }
370}
371
372impl From<security::sandbox::SandboxError> for Error {
373    fn from(err: security::sandbox::SandboxError) -> Self {
374        match err {
375            security::sandbox::SandboxError::Io(e) => Error::Io(e),
376            security::sandbox::SandboxError::NotSupported => {
377                Error::Config("sandbox not supported on this platform".into())
378            }
379            security::sandbox::SandboxError::NotAvailable(msg) => {
380                Error::Config(format!("sandbox not available: {}", msg))
381            }
382            _ => Error::Config(err.to_string()),
383        }
384    }
385}
386
387impl From<mcp::McpError> for Error {
388    fn from(err: mcp::McpError) -> Self {
389        match err {
390            mcp::McpError::Io(e) => Error::Io(e),
391            mcp::McpError::Json(e) => Error::Json(e),
392            _ => Error::Mcp(err.to_string()),
393        }
394    }
395}
396
397pub type Result<T> = std::result::Result<T, Error>;
398
399/// Simple query function for one-shot requests
400pub async fn query(prompt: &str) -> Result<String> {
401    let client = Client::builder().auth(Auth::FromEnv).await?.build().await?;
402    client.query(prompt).await
403}
404
405/// Query with a specific model
406pub async fn query_with_model(model: &str, prompt: &str) -> Result<String> {
407    use client::CreateMessageRequest;
408    let client = Client::builder().auth(Auth::FromEnv).await?.build().await?;
409    let request =
410        CreateMessageRequest::new(model, vec![types::Message::user(prompt)]).with_max_tokens(8192);
411    let response = client.send(request).await?;
412    Ok(response.text())
413}
414
415/// Stream a response for one-shot requests
416pub async fn stream(
417    prompt: &str,
418) -> Result<impl futures::Stream<Item = Result<String>> + Send + 'static + use<>> {
419    let client = Client::builder().auth(Auth::FromEnv).await?.build().await?;
420    client.stream(prompt).await
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426
427    #[test]
428    fn test_error_display() {
429        let err = Error::Api {
430            message: "Invalid API key".to_string(),
431            status: Some(401),
432            error_type: None,
433        };
434        assert!(err.to_string().contains("Invalid API key"));
435    }
436
437    #[test]
438    fn test_error_is_retryable() {
439        let rate_limit = Error::RateLimit { retry_after: None };
440        assert!(rate_limit.is_retryable());
441
442        let server_error = Error::Api {
443            message: "Internal error".to_string(),
444            status: Some(500),
445            error_type: None,
446        };
447        assert!(server_error.is_retryable());
448
449        let auth_error = Error::auth("Invalid token");
450        assert!(!auth_error.is_retryable());
451    }
452
453    #[test]
454    fn test_config_error_conversion() {
455        let config_err = config::ConfigError::NotFound {
456            key: "api_key".to_string(),
457        };
458        let err: Error = config_err.into();
459        assert!(matches!(err, Error::Config(_)));
460    }
461}