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 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
79pub 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
145pub use mcp::{
147 McpContent, McpError, McpManager, McpResourceDefinition, McpResult, McpServerConfig,
148 McpServerInfo, McpServerState, McpToolDefinition, McpToolResult, ReconnectPolicy,
149};
150
151pub use security::{SecurityContext, SecurityContextBuilder};
153
154pub use models::{
156 Capabilities, ModelFamily, ModelId, ModelRegistry, ModelRole, ModelSpec, ModelVersion, Pricing,
157 ProviderIds, registry as model_registry,
158};
159
160pub 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#[derive(Debug, thiserror::Error)]
176#[non_exhaustive]
177pub enum Error {
178 #[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 #[error("Authentication failed: {message}")]
188 Auth { message: String },
189
190 #[error("Network request failed: {0}")]
192 Network(#[from] reqwest::Error),
193
194 #[error("JSON parsing failed: {0}")]
196 Json(#[from] serde_json::Error),
197
198 #[error("Parse error: {0}")]
200 Parse(String),
201
202 #[error("Tool execution failed: {0}")]
204 Tool(#[from] types::ToolError),
205
206 #[error("Configuration error: {0}")]
208 Config(String),
209
210 #[error("IO error: {0}")]
212 Io(#[from] std::io::Error),
213
214 #[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 #[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 #[error("Context window exceeded: {estimated} tokens > {limit} limit (overage: {overage})")]
229 ContextWindowExceeded {
230 estimated: u64,
231 limit: u64,
232 overage: u64,
233 },
234
235 #[error("Operation timed out after {:.1}s", .0.as_secs_f64())]
237 Timeout(std::time::Duration),
238
239 #[error("Token validation failed: {0}")]
241 TokenValidation(#[from] client::messages::TokenValidationError),
242
243 #[error("Invalid request: {0}")]
245 InvalidRequest(String),
246
247 #[error("Stream error: {0}")]
249 Stream(String),
250
251 #[error("Environment variable error: {0}")]
253 Env(#[from] std::env::VarError),
254
255 #[error("{operation} is not supported by {provider}")]
257 NotSupported {
258 provider: &'static str,
259 operation: &'static str,
260 },
261
262 #[error("Permission denied: {0}")]
264 Permission(String),
265
266 #[error("Budget exceeded: ${used:.2} used (limit: ${limit:.2}, over by ${:.2})", used - limit)]
268 BudgetExceeded { used: f64, limit: f64 },
269
270 #[error("Model {model} is overloaded, try again later")]
272 ModelOverloaded { model: String },
273
274 #[error("Session error: {0}")]
276 Session(String),
277
278 #[error("MCP error: {0}")]
280 Mcp(String),
281
282 #[error("Resource exhausted: {0}")]
284 ResourceExhausted(String),
285
286 #[error("Hook '{hook}' failed: {reason}")]
288 HookFailed { hook: String, reason: String },
289
290 #[error("Hook '{hook}' timed out after {duration_secs}s")]
292 HookTimeout { hook: String, duration_secs: u64 },
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq)]
297pub enum ErrorCategory {
298 Authorization,
300 Configuration,
302 Transient,
304 Stateful,
306 Internal,
308 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
530pub 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
536pub 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
546pub 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}