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, DEFAULT_COMPACT_KEEP_MESSAGES, ExecutionConfig,
81 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 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, ExecutionGuard, QueueError, Session, SessionConfig,
122 SessionError, SessionId, SessionManager, SessionMessage, SessionResult, SessionState,
123 ToolState,
124};
125#[cfg(feature = "cli-integration")]
126pub use skills::FileSkillProvider;
127pub use skills::{
128 ChainSkillProvider, CommandLoader, InMemorySkillProvider, SkillDefinition, SkillExecutor,
129 SkillProviderTrait, SkillRegistry, SkillResult, SkillTool, SlashCommand,
130};
131pub use subagents::{
132 ChainSubagentProvider, InMemorySubagentProvider, SubagentDefinition, SubagentProviderTrait,
133 SubagentRegistry, builtin_subagents, find_builtin,
134};
135#[cfg(feature = "cli-integration")]
136pub use subagents::{FileSubagentProvider, SubagentLoader};
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
154#[cfg(feature = "aws")]
155pub use client::BedrockAdapter;
156#[cfg(feature = "azure")]
157pub use client::FoundryAdapter;
158#[cfg(feature = "gcp")]
159pub use client::VertexAdapter;
160
161#[derive(Debug, thiserror::Error)]
165#[non_exhaustive]
166pub enum Error {
167 #[error("API error (HTTP {status}): {message}", status = status.map(|s| s.to_string()).unwrap_or_else(|| "unknown".into()))]
169 Api {
170 message: String,
171 status: Option<u16>,
172 error_type: Option<String>,
173 },
174
175 #[error("Authentication failed: {message}")]
177 Auth { message: String },
178
179 #[error("Network request failed: {0}")]
181 Network(#[from] reqwest::Error),
182
183 #[error("JSON parsing failed: {0}")]
185 Json(#[from] serde_json::Error),
186
187 #[error("Parse error: {0}")]
189 Parse(String),
190
191 #[error("Tool execution failed: {0}")]
193 Tool(#[from] types::ToolError),
194
195 #[error("Configuration error: {0}")]
197 Config(String),
198
199 #[error("IO error: {0}")]
201 Io(#[from] std::io::Error),
202
203 #[error("Rate limit exceeded{}", match retry_after {
205 Some(d) => format!(", retry in {:.0}s", d.as_secs_f64()),
206 None => String::new(),
207 })]
208 RateLimit {
209 retry_after: Option<std::time::Duration>,
210 },
211
212 #[error("Context limit exceeded: {current}/{max} tokens ({:.0}% used)", (*current as f64 / *max as f64) * 100.0)]
214 ContextOverflow { current: usize, max: usize },
215
216 #[error("Operation timed out after {:.1}s", .0.as_secs_f64())]
218 Timeout(std::time::Duration),
219
220 #[error("Invalid request: {0}")]
222 InvalidRequest(String),
223
224 #[error("Stream error: {0}")]
226 Stream(String),
227
228 #[error("Environment variable error: {0}")]
230 Env(#[from] std::env::VarError),
231
232 #[error("{operation} is not supported by {provider}")]
234 NotSupported {
235 provider: &'static str,
236 operation: &'static str,
237 },
238
239 #[error("Permission denied: {0}")]
241 Permission(String),
242
243 #[error("Budget exceeded: ${used:.2} used (limit: ${limit:.2}, over by ${:.2})", used - limit)]
245 BudgetExceeded { used: f64, limit: f64 },
246
247 #[error("Model {model} is overloaded, try again later")]
249 ModelOverloaded { model: String },
250
251 #[error("Session error: {0}")]
253 Session(String),
254
255 #[error("MCP error: {0}")]
257 Mcp(String),
258
259 #[error("Resource exhausted: {0}")]
261 ResourceExhausted(String),
262
263 #[error("Hook '{hook}' failed: {reason}")]
265 HookFailed { hook: String, reason: String },
266
267 #[error("Hook '{hook}' timed out after {duration_secs}s")]
269 HookTimeout { hook: String, duration_secs: u64 },
270}
271
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub enum ErrorCategory {
275 Authorization,
277 Configuration,
279 Transient,
281 Stateful,
283 Internal,
285 ResourceLimit,
287}
288
289impl Error {
290 pub fn auth(message: impl Into<String>) -> Self {
291 Error::Auth {
292 message: message.into(),
293 }
294 }
295
296 pub fn category(&self) -> ErrorCategory {
297 match self {
298 Error::Auth { .. } => ErrorCategory::Authorization,
299 Error::Api {
300 status: Some(401 | 403),
301 ..
302 } => ErrorCategory::Authorization,
303 Error::Permission(_) | Error::HookFailed { .. } | Error::HookTimeout { .. } => {
304 ErrorCategory::Authorization
305 }
306
307 Error::Config(_) | Error::Parse(_) | Error::Env(_) | Error::InvalidRequest(_) => {
308 ErrorCategory::Configuration
309 }
310
311 Error::Network(_) | Error::RateLimit { .. } | Error::ModelOverloaded { .. } => {
312 ErrorCategory::Transient
313 }
314 Error::Api {
315 status: Some(500..=599),
316 ..
317 } => ErrorCategory::Transient,
318
319 Error::Session(_) | Error::Mcp(_) | Error::Stream(_) => ErrorCategory::Stateful,
320
321 Error::BudgetExceeded { .. }
322 | Error::ContextOverflow { .. }
323 | Error::Timeout(_)
324 | Error::ResourceExhausted(_) => ErrorCategory::ResourceLimit,
325
326 Error::Io(_)
327 | Error::Json(_)
328 | Error::Tool(_)
329 | Error::Api { .. }
330 | Error::NotSupported { .. } => ErrorCategory::Internal,
331 }
332 }
333
334 pub fn is_authorization_error(&self) -> bool {
335 self.category() == ErrorCategory::Authorization
336 }
337
338 pub fn is_configuration_error(&self) -> bool {
339 self.category() == ErrorCategory::Configuration
340 }
341
342 pub fn is_resource_limit(&self) -> bool {
343 self.category() == ErrorCategory::ResourceLimit
344 }
345
346 pub fn is_retryable(&self) -> bool {
347 self.category() == ErrorCategory::Transient
348 }
349
350 pub fn is_unauthorized(&self) -> bool {
351 matches!(
352 self,
353 Error::Api {
354 status: Some(401),
355 ..
356 } | Error::Auth { .. }
357 )
358 }
359
360 pub fn is_overloaded(&self) -> bool {
361 match self {
362 Error::Api {
363 status: Some(529 | 503),
364 ..
365 } => true,
366 Error::Api {
367 error_type: Some(t),
368 ..
369 } if t.contains("overloaded") => true,
370 Error::Api { message, .. } if message.to_lowercase().contains("overloaded") => true,
371 Error::ModelOverloaded { .. } => true,
372 _ => false,
373 }
374 }
375
376 pub fn status_code(&self) -> Option<u16> {
377 match self {
378 Error::Api { status, .. } => *status,
379 _ => None,
380 }
381 }
382
383 pub fn retry_after(&self) -> Option<std::time::Duration> {
384 match self {
385 Error::RateLimit { retry_after } => *retry_after,
386 _ => None,
387 }
388 }
389}
390
391impl From<config::ConfigError> for Error {
392 fn from(err: config::ConfigError) -> Self {
393 match err {
394 config::ConfigError::NotFound { key } => {
395 Error::Config(format!("Key not found: {}", key))
396 }
397 config::ConfigError::InvalidValue { key, message } => {
398 Error::Config(format!("Invalid value for {}: {}", key, message))
399 }
400 config::ConfigError::Serialization(e) => Error::Json(e),
401 config::ConfigError::Io(e) => Error::Io(e),
402 config::ConfigError::Env(e) => Error::Env(e),
403 config::ConfigError::Provider { message } => Error::Config(message),
404 config::ConfigError::ValidationErrors(errors) => Error::Config(errors.to_string()),
405 }
406 }
407}
408
409impl From<context::ContextError> for Error {
410 fn from(err: context::ContextError) -> Self {
411 match err {
412 context::ContextError::Source { message } => Error::Config(message),
413 context::ContextError::TokenBudgetExceeded { current, limit } => {
414 Error::ContextOverflow {
415 current: current as usize,
416 max: limit as usize,
417 }
418 }
419 context::ContextError::SkillNotFound { name } => {
420 Error::Config(format!("Skill not found: {}", name))
421 }
422 context::ContextError::RuleNotFound { name } => {
423 Error::Config(format!("Rule not found: {}", name))
424 }
425 context::ContextError::Parse { message } => Error::Parse(message),
426 context::ContextError::Io(e) => Error::Io(e),
427 }
428 }
429}
430
431impl From<session::SessionError> for Error {
432 fn from(err: session::SessionError) -> Self {
433 match err {
434 session::SessionError::NotFound { id } => {
435 Error::Config(format!("Session not found: {}", id))
436 }
437 session::SessionError::Expired { id } => {
438 Error::Config(format!("Session expired: {}", id))
439 }
440 session::SessionError::PermissionDenied { reason } => Error::auth(reason),
441 session::SessionError::Storage { message } => Error::Config(message),
442 session::SessionError::PersistenceError(msg) => Error::Config(msg),
443 session::SessionError::Serialization(e) => Error::Json(e),
444 session::SessionError::Compact { message } => Error::Config(message),
445 session::SessionError::Context(e) => e.into(),
446 session::SessionError::Plan { message } => Error::Config(message),
447 }
448 }
449}
450
451impl From<security::SecurityError> for Error {
452 fn from(err: security::SecurityError) -> Self {
453 match err {
454 security::SecurityError::Io(e) => Error::Io(e),
455 security::SecurityError::ResourceLimit(msg) => Error::ResourceExhausted(msg),
456 security::SecurityError::BashBlocked(msg) => Error::Permission(msg),
457 security::SecurityError::DeniedPath(path) => {
458 Error::Permission(format!("denied path: {}", path.display()))
459 }
460 security::SecurityError::PathEscape(path) => {
461 Error::Permission(format!("path escapes sandbox: {}", path.display()))
462 }
463 security::SecurityError::NotWithinSandbox(path) => {
464 Error::Permission(format!("path not within sandbox: {}", path.display()))
465 }
466 security::SecurityError::InvalidPath(msg) => Error::Config(msg),
467 security::SecurityError::AbsoluteSymlink(path) => Error::Permission(format!(
468 "absolute symlink outside sandbox: {}",
469 path.display()
470 )),
471 security::SecurityError::SymlinkDepthExceeded { path, max } => Error::Permission(
472 format!("symlink depth exceeded (max {}): {}", max, path.display()),
473 ),
474 }
475 }
476}
477
478impl From<security::sandbox::SandboxError> for Error {
479 fn from(err: security::sandbox::SandboxError) -> Self {
480 match err {
481 security::sandbox::SandboxError::Io(e) => Error::Io(e),
482 security::sandbox::SandboxError::NotSupported => {
483 Error::Config("sandbox not supported on this platform".into())
484 }
485 security::sandbox::SandboxError::NotAvailable(msg) => {
486 Error::Config(format!("sandbox not available: {}", msg))
487 }
488 _ => Error::Config(err.to_string()),
489 }
490 }
491}
492
493impl From<mcp::McpError> for Error {
494 fn from(err: mcp::McpError) -> Self {
495 match err {
496 mcp::McpError::Io(e) => Error::Io(e),
497 mcp::McpError::Json(e) => Error::Json(e),
498 _ => Error::Mcp(err.to_string()),
499 }
500 }
501}
502
503pub type Result<T> = std::result::Result<T, Error>;
504
505pub async fn query(prompt: &str) -> Result<String> {
507 let client = Client::builder().auth(Auth::FromEnv).await?.build().await?;
508 client.query(prompt).await
509}
510
511pub async fn query_with_model(model: &str, prompt: &str) -> Result<String> {
513 use client::CreateMessageRequest;
514 let client = Client::builder().auth(Auth::FromEnv).await?.build().await?;
515 let request =
516 CreateMessageRequest::new(model, vec![types::Message::user(prompt)]).with_max_tokens(8192);
517 let response = client.send(request).await?;
518 Ok(response.text())
519}
520
521pub async fn stream(
523 prompt: &str,
524) -> Result<impl futures::Stream<Item = Result<String>> + Send + 'static + use<>> {
525 let client = Client::builder().auth(Auth::FromEnv).await?.build().await?;
526 client.stream(prompt).await
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532
533 #[test]
534 fn test_error_display() {
535 let err = Error::Api {
536 message: "Invalid API key".to_string(),
537 status: Some(401),
538 error_type: None,
539 };
540 assert!(err.to_string().contains("Invalid API key"));
541 }
542
543 #[test]
544 fn test_error_is_retryable() {
545 let rate_limit = Error::RateLimit { retry_after: None };
546 assert!(rate_limit.is_retryable());
547
548 let server_error = Error::Api {
549 message: "Internal error".to_string(),
550 status: Some(500),
551 error_type: None,
552 };
553 assert!(server_error.is_retryable());
554
555 let auth_error = Error::auth("Invalid token");
556 assert!(!auth_error.is_retryable());
557 }
558
559 #[test]
560 fn test_config_error_conversion() {
561 let config_err = config::ConfigError::NotFound {
562 key: "api_key".to_string(),
563 };
564 let err: Error = config_err.into();
565 assert!(matches!(err, Error::Config(_)));
566 }
567}