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;
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
81pub 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#[cfg(feature = "plugins")]
152pub use plugins::{PluginDescriptor, PluginDiscovery, PluginError, PluginManager, PluginManifest};
153
154pub use mcp::{
156 McpContent, McpError, McpManager, McpResourceDefinition, McpResult, McpServerConfig,
157 McpServerInfo, McpServerState, McpToolDefinition, McpToolResult, ReconnectPolicy,
158};
159
160pub use security::{SecurityContext, SecurityContextBuilder};
162
163pub use models::{
165 Capabilities, ModelFamily, ModelId, ModelRegistry, ModelRole, ModelSpec, ModelVersion, Pricing,
166 ProviderIds, registry as model_registry,
167};
168
169pub 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#[derive(Debug, thiserror::Error)]
185#[non_exhaustive]
186pub enum Error {
187 #[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 #[error("Authentication failed: {message}")]
197 Auth { message: String },
198
199 #[error("Network request failed: {0}")]
201 Network(#[from] reqwest::Error),
202
203 #[error("JSON parsing failed: {0}")]
205 Json(#[from] serde_json::Error),
206
207 #[error("Parse error: {0}")]
209 Parse(String),
210
211 #[error("Tool execution failed: {0}")]
213 Tool(#[from] types::ToolError),
214
215 #[error("Configuration error: {0}")]
217 Config(String),
218
219 #[error("IO error: {0}")]
221 Io(#[from] std::io::Error),
222
223 #[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 #[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 #[error("Context window exceeded: {estimated} tokens > {limit} limit (overage: {overage})")]
238 ContextWindowExceeded {
239 estimated: u64,
240 limit: u64,
241 overage: u64,
242 },
243
244 #[error("Operation timed out after {:.1}s", .0.as_secs_f64())]
246 Timeout(std::time::Duration),
247
248 #[error("Token validation failed: {0}")]
250 TokenValidation(#[from] client::messages::TokenValidationError),
251
252 #[error("Invalid request: {0}")]
254 InvalidRequest(String),
255
256 #[error("Stream error: {0}")]
258 Stream(String),
259
260 #[error("Environment variable error: {0}")]
262 Env(#[from] std::env::VarError),
263
264 #[error("{operation} is not supported by {provider}")]
266 NotSupported {
267 provider: &'static str,
268 operation: &'static str,
269 },
270
271 #[error("Permission denied: {0}")]
273 Permission(String),
274
275 #[error("Budget exceeded: ${used:.2} used (limit: ${limit:.2}, over by ${:.2})", used - limit)]
277 BudgetExceeded { used: f64, limit: f64 },
278
279 #[error("Model {model} is overloaded, try again later")]
281 ModelOverloaded { model: String },
282
283 #[error("Session error: {0}")]
285 Session(String),
286
287 #[error("MCP error: {0}")]
289 Mcp(mcp::McpError),
290
291 #[error("Resource exhausted: {0}")]
293 ResourceExhausted(String),
294
295 #[error("Hook '{hook}' failed: {reason}")]
297 HookFailed { hook: String, reason: String },
298
299 #[error("Hook '{hook}' timed out after {duration_secs}s")]
301 HookTimeout { hook: String, duration_secs: u64 },
302
303 #[cfg(feature = "plugins")]
305 #[error("Plugin error: {0}")]
306 Plugin(#[from] plugins::PluginError),
307}
308
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311pub enum ErrorCategory {
312 Authorization,
314 Configuration,
316 Transient,
318 Stateful,
320 Internal,
322 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
547pub 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
553pub 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
563pub 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}