1#![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
77pub 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
146pub use mcp::{
148 McpContent, McpError, McpManager, McpResourceDefinition, McpResult, McpServerConfig,
149 McpServerInfo, McpServerState, McpToolDefinition, McpToolResult, ReconnectPolicy,
150};
151
152pub 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#[derive(Debug, thiserror::Error)]
166#[non_exhaustive]
167pub enum Error {
168 #[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 #[error("Authentication failed: {message}")]
178 Auth { message: String },
179
180 #[error("Network request failed: {0}")]
182 Network(#[from] reqwest::Error),
183
184 #[error("JSON parsing failed: {0}")]
186 Json(#[from] serde_json::Error),
187
188 #[error("Parse error: {0}")]
190 Parse(String),
191
192 #[error("Tool execution failed: {0}")]
194 Tool(#[from] types::ToolError),
195
196 #[error("Configuration error: {0}")]
198 Config(String),
199
200 #[error("IO error: {0}")]
202 Io(#[from] std::io::Error),
203
204 #[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 #[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 #[error("Operation timed out after {:.1}s", .0.as_secs_f64())]
219 Timeout(std::time::Duration),
220
221 #[error("Token validation failed: {0}")]
223 TokenValidation(#[from] client::messages::TokenValidationError),
224
225 #[error("Invalid request: {0}")]
227 InvalidRequest(String),
228
229 #[error("Stream error: {0}")]
231 Stream(String),
232
233 #[error("Environment variable error: {0}")]
235 Env(#[from] std::env::VarError),
236
237 #[error("{operation} is not supported by {provider}")]
239 NotSupported {
240 provider: &'static str,
241 operation: &'static str,
242 },
243
244 #[error("Permission denied: {0}")]
246 Permission(String),
247
248 #[error("Budget exceeded: ${used:.2} used (limit: ${limit:.2}, over by ${:.2})", used - limit)]
250 BudgetExceeded { used: f64, limit: f64 },
251
252 #[error("Model {model} is overloaded, try again later")]
254 ModelOverloaded { model: String },
255
256 #[error("Session error: {0}")]
258 Session(String),
259
260 #[error("MCP error: {0}")]
262 Mcp(String),
263
264 #[error("Resource exhausted: {0}")]
266 ResourceExhausted(String),
267
268 #[error("Hook '{hook}' failed: {reason}")]
270 HookFailed { hook: String, reason: String },
271
272 #[error("Hook '{hook}' timed out after {duration_secs}s")]
274 HookTimeout { hook: String, duration_secs: u64 },
275}
276
277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
279pub enum ErrorCategory {
280 Authorization,
282 Configuration,
284 Transient,
286 Stateful,
288 Internal,
290 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
512pub 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
518pub 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
528pub 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}