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::{
104 ContentSource, Index, IndexRegistry, LoadedEntry, Named, PathMatched, Provider, SourceType,
105 ToolRestricted,
106};
107pub use context::{
108 ContextBuilder, FileMemoryProvider, InMemoryProvider, LeveledMemoryProvider, MemoryContent,
109 MemoryLoader, MemoryProvider, PromptOrchestrator, RoutingStrategy, RuleIndex, StaticContext,
110};
111pub use hooks::{CommandHook, Hook, HookContext, HookEvent, HookInput, HookManager, HookOutput};
112pub use observability::{
113 AgentMetrics as ObservabilityMetrics, MetricsConfig, MetricsRegistry, ObservabilityConfig,
114 SpanContext, TracingConfig,
115};
116pub use output_style::{
117 OutputStyle, builtin_styles, default_style, explanatory_style, learning_style,
118};
119#[cfg(feature = "cli-integration")]
120pub use output_style::{OutputStyleLoader, SystemPromptGenerator};
121pub use permissions::{PermissionDecision, PermissionMode, PermissionPolicy, PermissionResult};
122pub use session::{
123 CompactExecutor, CompactStrategy, ExecutionGuard, QueueError, Session, SessionConfig,
124 SessionError, SessionId, SessionManager, SessionMessage, SessionResult, SessionState,
125 ToolState,
126};
127pub use skills::{
128 SkillExecutor, SkillFrontmatter, SkillIndex, SkillIndexLoader, SkillResult, SkillTool,
129 process_bash_backticks, process_file_references, resolve_markdown_paths, strip_frontmatter,
130 substitute_args,
131};
132#[cfg(feature = "cli-integration")]
133pub use subagents::{SubagentFrontmatter, SubagentIndexLoader};
134pub use subagents::{SubagentIndex, builtin_subagents, find_builtin};
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
143pub use mcp::{
145 McpContent, McpError, McpManager, McpResourceDefinition, McpResult, McpServerConfig,
146 McpServerInfo, McpServerState, McpToolDefinition, McpToolResult, ReconnectPolicy,
147};
148
149pub 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#[derive(Debug, thiserror::Error)]
163#[non_exhaustive]
164pub enum Error {
165 #[error("API error (HTTP {status}): {message}", status = status.map(|s| s.to_string()).unwrap_or_else(|| "unknown".into()))]
167 Api {
168 message: String,
169 status: Option<u16>,
170 error_type: Option<String>,
171 },
172
173 #[error("Authentication failed: {message}")]
175 Auth { message: String },
176
177 #[error("Network request failed: {0}")]
179 Network(#[from] reqwest::Error),
180
181 #[error("JSON parsing failed: {0}")]
183 Json(#[from] serde_json::Error),
184
185 #[error("Parse error: {0}")]
187 Parse(String),
188
189 #[error("Tool execution failed: {0}")]
191 Tool(#[from] types::ToolError),
192
193 #[error("Configuration error: {0}")]
195 Config(String),
196
197 #[error("IO error: {0}")]
199 Io(#[from] std::io::Error),
200
201 #[error("Rate limit exceeded{}", match retry_after {
203 Some(d) => format!(", retry in {:.0}s", d.as_secs_f64()),
204 None => String::new(),
205 })]
206 RateLimit {
207 retry_after: Option<std::time::Duration>,
208 },
209
210 #[error("Context limit exceeded: {current}/{max} tokens ({:.0}% used)", (*current as f64 / *max as f64) * 100.0)]
212 ContextOverflow { current: usize, max: usize },
213
214 #[error("Operation timed out after {:.1}s", .0.as_secs_f64())]
216 Timeout(std::time::Duration),
217
218 #[error("Token validation failed: {0}")]
220 TokenValidation(#[from] client::messages::TokenValidationError),
221
222 #[error("Invalid request: {0}")]
224 InvalidRequest(String),
225
226 #[error("Stream error: {0}")]
228 Stream(String),
229
230 #[error("Environment variable error: {0}")]
232 Env(#[from] std::env::VarError),
233
234 #[error("{operation} is not supported by {provider}")]
236 NotSupported {
237 provider: &'static str,
238 operation: &'static str,
239 },
240
241 #[error("Permission denied: {0}")]
243 Permission(String),
244
245 #[error("Budget exceeded: ${used:.2} used (limit: ${limit:.2}, over by ${:.2})", used - limit)]
247 BudgetExceeded { used: f64, limit: f64 },
248
249 #[error("Model {model} is overloaded, try again later")]
251 ModelOverloaded { model: String },
252
253 #[error("Session error: {0}")]
255 Session(String),
256
257 #[error("MCP error: {0}")]
259 Mcp(String),
260
261 #[error("Resource exhausted: {0}")]
263 ResourceExhausted(String),
264
265 #[error("Hook '{hook}' failed: {reason}")]
267 HookFailed { hook: String, reason: String },
268
269 #[error("Hook '{hook}' timed out after {duration_secs}s")]
271 HookTimeout { hook: String, duration_secs: u64 },
272}
273
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
276pub enum ErrorCategory {
277 Authorization,
279 Configuration,
281 Transient,
283 Stateful,
285 Internal,
287 ResourceLimit,
289}
290
291impl Error {
292 pub fn auth(message: impl Into<String>) -> Self {
293 Error::Auth {
294 message: message.into(),
295 }
296 }
297
298 pub fn category(&self) -> ErrorCategory {
299 match self {
300 Error::Auth { .. } => ErrorCategory::Authorization,
301 Error::Api {
302 status: Some(401 | 403),
303 ..
304 } => ErrorCategory::Authorization,
305 Error::Permission(_) | Error::HookFailed { .. } | Error::HookTimeout { .. } => {
306 ErrorCategory::Authorization
307 }
308
309 Error::Config(_)
310 | Error::Parse(_)
311 | Error::Env(_)
312 | Error::InvalidRequest(_)
313 | Error::TokenValidation(_) => ErrorCategory::Configuration,
314
315 Error::Network(_) | Error::RateLimit { .. } | Error::ModelOverloaded { .. } => {
316 ErrorCategory::Transient
317 }
318 Error::Api {
319 status: Some(500..=599),
320 ..
321 } => ErrorCategory::Transient,
322
323 Error::Session(_) | Error::Mcp(_) | Error::Stream(_) => ErrorCategory::Stateful,
324
325 Error::BudgetExceeded { .. }
326 | Error::ContextOverflow { .. }
327 | Error::Timeout(_)
328 | Error::ResourceExhausted(_) => ErrorCategory::ResourceLimit,
329
330 Error::Io(_)
331 | Error::Json(_)
332 | Error::Tool(_)
333 | Error::Api { .. }
334 | Error::NotSupported { .. } => ErrorCategory::Internal,
335 }
336 }
337
338 pub fn is_authorization_error(&self) -> bool {
339 self.category() == ErrorCategory::Authorization
340 }
341
342 pub fn is_configuration_error(&self) -> bool {
343 self.category() == ErrorCategory::Configuration
344 }
345
346 pub fn is_resource_limit(&self) -> bool {
347 self.category() == ErrorCategory::ResourceLimit
348 }
349
350 pub fn is_retryable(&self) -> bool {
351 self.category() == ErrorCategory::Transient
352 }
353
354 pub fn is_unauthorized(&self) -> bool {
355 matches!(
356 self,
357 Error::Api {
358 status: Some(401),
359 ..
360 } | Error::Auth { .. }
361 )
362 }
363
364 pub fn is_overloaded(&self) -> bool {
365 match self {
366 Error::Api {
367 status: Some(529 | 503),
368 ..
369 } => true,
370 Error::Api {
371 error_type: Some(t),
372 ..
373 } if t.contains("overloaded") => true,
374 Error::Api { message, .. } if message.to_lowercase().contains("overloaded") => true,
375 Error::ModelOverloaded { .. } => true,
376 _ => false,
377 }
378 }
379
380 pub fn status_code(&self) -> Option<u16> {
381 match self {
382 Error::Api { status, .. } => *status,
383 _ => None,
384 }
385 }
386
387 pub fn retry_after(&self) -> Option<std::time::Duration> {
388 match self {
389 Error::RateLimit { retry_after } => *retry_after,
390 _ => None,
391 }
392 }
393}
394
395impl From<config::ConfigError> for Error {
396 fn from(err: config::ConfigError) -> Self {
397 match err {
398 config::ConfigError::NotFound { key } => {
399 Error::Config(format!("Key not found: {}", key))
400 }
401 config::ConfigError::InvalidValue { key, message } => {
402 Error::Config(format!("Invalid value for {}: {}", key, message))
403 }
404 config::ConfigError::Serialization(e) => Error::Json(e),
405 config::ConfigError::Io(e) => Error::Io(e),
406 config::ConfigError::Env(e) => Error::Env(e),
407 config::ConfigError::Provider { message } => Error::Config(message),
408 config::ConfigError::ValidationErrors(errors) => Error::Config(errors.to_string()),
409 }
410 }
411}
412
413impl From<context::ContextError> for Error {
414 fn from(err: context::ContextError) -> Self {
415 match err {
416 context::ContextError::Source { message } => Error::Config(message),
417 context::ContextError::TokenBudgetExceeded { current, limit } => {
418 Error::ContextOverflow {
419 current: current as usize,
420 max: limit as usize,
421 }
422 }
423 context::ContextError::SkillNotFound { name } => {
424 Error::Config(format!("Skill not found: {}", name))
425 }
426 context::ContextError::RuleNotFound { name } => {
427 Error::Config(format!("Rule not found: {}", name))
428 }
429 context::ContextError::Parse { message } => Error::Parse(message),
430 context::ContextError::Io(e) => Error::Io(e),
431 }
432 }
433}
434
435impl From<session::SessionError> for Error {
436 fn from(err: session::SessionError) -> Self {
437 match err {
438 session::SessionError::NotFound { id } => {
439 Error::Config(format!("Session not found: {}", id))
440 }
441 session::SessionError::Expired { id } => {
442 Error::Config(format!("Session expired: {}", id))
443 }
444 session::SessionError::PermissionDenied { reason } => Error::auth(reason),
445 session::SessionError::Storage { message } => Error::Config(message),
446 session::SessionError::Serialization(e) => Error::Json(e),
447 session::SessionError::Compact { message } => Error::Config(message),
448 session::SessionError::Context(e) => e.into(),
449 session::SessionError::Plan { message } => Error::Config(message),
450 }
451 }
452}
453
454impl From<security::SecurityError> for Error {
455 fn from(err: security::SecurityError) -> Self {
456 match err {
457 security::SecurityError::Io(e) => Error::Io(e),
458 security::SecurityError::ResourceLimit(msg) => Error::ResourceExhausted(msg),
459 security::SecurityError::BashBlocked(msg) => Error::Permission(msg),
460 security::SecurityError::DeniedPath(path) => {
461 Error::Permission(format!("denied path: {}", path.display()))
462 }
463 security::SecurityError::PathEscape(path) => {
464 Error::Permission(format!("path escapes sandbox: {}", path.display()))
465 }
466 security::SecurityError::NotWithinSandbox(path) => {
467 Error::Permission(format!("path not within sandbox: {}", path.display()))
468 }
469 security::SecurityError::InvalidPath(msg) => Error::Config(msg),
470 security::SecurityError::AbsoluteSymlink(path) => Error::Permission(format!(
471 "absolute symlink outside sandbox: {}",
472 path.display()
473 )),
474 security::SecurityError::SymlinkDepthExceeded { path, max } => Error::Permission(
475 format!("symlink depth exceeded (max {}): {}", max, path.display()),
476 ),
477 }
478 }
479}
480
481impl From<security::sandbox::SandboxError> for Error {
482 fn from(err: security::sandbox::SandboxError) -> Self {
483 match err {
484 security::sandbox::SandboxError::Io(e) => Error::Io(e),
485 security::sandbox::SandboxError::NotSupported => {
486 Error::Config("sandbox not supported on this platform".into())
487 }
488 security::sandbox::SandboxError::NotAvailable(msg) => {
489 Error::Config(format!("sandbox not available: {}", msg))
490 }
491 _ => Error::Config(err.to_string()),
492 }
493 }
494}
495
496impl From<mcp::McpError> for Error {
497 fn from(err: mcp::McpError) -> Self {
498 match err {
499 mcp::McpError::Io(e) => Error::Io(e),
500 mcp::McpError::Json(e) => Error::Json(e),
501 _ => Error::Mcp(err.to_string()),
502 }
503 }
504}
505
506pub type Result<T> = std::result::Result<T, Error>;
507
508pub async fn query(prompt: &str) -> Result<String> {
510 let client = Client::builder().auth(Auth::FromEnv).await?.build().await?;
511 client.query(prompt).await
512}
513
514pub async fn query_with_model(model: &str, prompt: &str) -> Result<String> {
516 use client::CreateMessageRequest;
517 let client = Client::builder().auth(Auth::FromEnv).await?.build().await?;
518 let request =
519 CreateMessageRequest::new(model, vec![types::Message::user(prompt)]).with_max_tokens(8192);
520 let response = client.send(request).await?;
521 Ok(response.text())
522}
523
524pub async fn stream(
526 prompt: &str,
527) -> Result<impl futures::Stream<Item = Result<String>> + Send + 'static + use<>> {
528 let client = Client::builder().auth(Auth::FromEnv).await?.build().await?;
529 client.stream(prompt).await
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535
536 #[test]
537 fn test_error_display() {
538 let err = Error::Api {
539 message: "Invalid API key".to_string(),
540 status: Some(401),
541 error_type: None,
542 };
543 assert!(err.to_string().contains("Invalid API key"));
544 }
545
546 #[test]
547 fn test_error_is_retryable() {
548 let rate_limit = Error::RateLimit { retry_after: None };
549 assert!(rate_limit.is_retryable());
550
551 let server_error = Error::Api {
552 message: "Internal error".to_string(),
553 status: Some(500),
554 error_type: None,
555 };
556 assert!(server_error.is_retryable());
557
558 let auth_error = Error::auth("Invalid token");
559 assert!(!auth_error.is_retryable());
560 }
561
562 #[test]
563 fn test_config_error_conversion() {
564 let config_err = config::ConfigError::NotFound {
565 key: "api_key".to_string(),
566 };
567 let err: Error = config_err.into();
568 assert!(matches!(err, Error::Config(_)));
569 }
570}