mixtape_core/
error.rs

1//! Top-level error types for mixtape
2//!
3//! This module provides a simplified, user-facing error type that flattens
4//! the internal error hierarchy into actionable categories.
5
6use thiserror::Error;
7
8use crate::agent::AgentError;
9use crate::provider::ProviderError;
10use crate::tool::ToolError;
11
12#[cfg(feature = "session")]
13use crate::session::SessionError;
14
15/// Top-level error type for mixtape operations
16///
17/// This enum provides a flattened view of errors, categorized by how users
18/// typically need to handle them:
19///
20/// - [`Error::Auth`] - Fix credentials and retry
21/// - [`Error::RateLimited`] - Back off and retry
22/// - [`Error::Network`] - Check connectivity, retry
23/// - [`Error::Unavailable`] - Service is down, wait and retry
24/// - [`Error::Model`] - Model-side issues (content filtered, context too long)
25/// - [`Error::Tool`] - Tool execution failed
26/// - [`Error::Config`] - Fix configuration (bad model ID, missing parameters)
27#[derive(Debug, Error)]
28pub enum Error {
29    /// Authentication failed (invalid or expired credentials)
30    #[error("authentication failed: {0}")]
31    Auth(String),
32
33    /// Rate limited - slow down requests
34    #[error("rate limited: {0}")]
35    RateLimited(String),
36
37    /// Network connectivity issue
38    #[error("network error: {0}")]
39    Network(String),
40
41    /// Service temporarily unavailable
42    #[error("service unavailable: {0}")]
43    Unavailable(String),
44
45    /// Model error (content filtered, context too long, empty response, etc.)
46    #[error("model error: {0}")]
47    Model(String),
48
49    /// Tool execution failed
50    #[error("tool error: {0}")]
51    Tool(String),
52
53    /// Configuration error (bad model ID, missing parameters)
54    #[error("configuration error: {0}")]
55    Config(String),
56
57    /// Session storage error
58    #[cfg(feature = "session")]
59    #[error("session error: {0}")]
60    Session(String),
61
62    /// MCP server error
63    #[cfg(feature = "mcp")]
64    #[error("MCP error: {0}")]
65    Mcp(String),
66
67    /// Other error
68    #[error("{0}")]
69    Other(String),
70}
71
72impl Error {
73    /// Returns true if this is an authentication error
74    pub fn is_auth(&self) -> bool {
75        matches!(self, Self::Auth(_))
76    }
77
78    /// Returns true if this is a rate limiting error
79    pub fn is_rate_limited(&self) -> bool {
80        matches!(self, Self::RateLimited(_))
81    }
82
83    /// Returns true if this is a network error
84    pub fn is_network(&self) -> bool {
85        matches!(self, Self::Network(_))
86    }
87
88    /// Returns true if the service is unavailable
89    pub fn is_unavailable(&self) -> bool {
90        matches!(self, Self::Unavailable(_))
91    }
92
93    /// Returns true if this is a model error
94    pub fn is_model(&self) -> bool {
95        matches!(self, Self::Model(_))
96    }
97
98    /// Returns true if this is a tool error
99    pub fn is_tool(&self) -> bool {
100        matches!(self, Self::Tool(_))
101    }
102
103    /// Returns true if this is a configuration error
104    pub fn is_config(&self) -> bool {
105        matches!(self, Self::Config(_))
106    }
107
108    /// Returns true if this error is potentially retryable
109    ///
110    /// Retryable errors include rate limiting, network issues, and service
111    /// unavailability. Authentication and configuration errors are not
112    /// retryable without user intervention.
113    pub fn is_retryable(&self) -> bool {
114        matches!(
115            self,
116            Self::RateLimited(_) | Self::Network(_) | Self::Unavailable(_)
117        )
118    }
119}
120
121impl From<ProviderError> for Error {
122    fn from(err: ProviderError) -> Self {
123        match err {
124            ProviderError::Authentication(msg) => Self::Auth(msg),
125            ProviderError::RateLimited(msg) => Self::RateLimited(msg),
126            ProviderError::Network(msg) => Self::Network(msg),
127            ProviderError::ServiceUnavailable(msg) => Self::Unavailable(msg),
128            ProviderError::Model(msg) => Self::Model(msg),
129            ProviderError::Configuration(msg) => Self::Config(msg),
130            ProviderError::Communication(err) => Self::Network(err.to_string()),
131            ProviderError::Other(msg) => Self::Other(msg),
132        }
133    }
134}
135
136impl From<ToolError> for Error {
137    fn from(err: ToolError) -> Self {
138        Self::Tool(err.to_string())
139    }
140}
141
142#[cfg(feature = "session")]
143impl From<SessionError> for Error {
144    fn from(err: SessionError) -> Self {
145        Self::Session(err.to_string())
146    }
147}
148
149impl From<AgentError> for Error {
150    fn from(err: AgentError) -> Self {
151        match err {
152            AgentError::Provider(e) => e.into(),
153            AgentError::Tool(e) => e.into(),
154            #[cfg(feature = "session")]
155            AgentError::Session(e) => e.into(),
156            AgentError::NoResponse => Self::Model("model returned no response".to_string()),
157            AgentError::EmptyResponse => Self::Model("model returned empty response".to_string()),
158            AgentError::MaxTokensExceeded => Self::Model(
159                "response exceeded maximum token limit - try asking the model to be more concise"
160                    .to_string(),
161            ),
162            AgentError::ContentFiltered => {
163                Self::Model("response was filtered by content moderation".to_string())
164            }
165            AgentError::ToolDenied(msg) => Self::Tool(format!("denied: {}", msg)),
166            AgentError::ToolNotFound(name) => Self::Tool(format!("not found: {}", name)),
167            AgentError::InvalidToolInput(msg) => Self::Tool(format!("invalid input: {}", msg)),
168            AgentError::PermissionFailed(msg) => Self::Tool(format!("permission failed: {}", msg)),
169            AgentError::UnexpectedStopReason(reason) => {
170                Self::Model(format!("unexpected stop reason: {}", reason))
171            }
172            AgentError::Context(e) => Self::Model(format!("context error: {}", e)),
173        }
174    }
175}
176
177/// Result type for mixtape operations
178pub type Result<T> = std::result::Result<T, Error>;
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_is_retryable() {
186        assert!(Error::RateLimited("slow down".into()).is_retryable());
187        assert!(Error::Network("connection refused".into()).is_retryable());
188        assert!(Error::Unavailable("503".into()).is_retryable());
189
190        assert!(!Error::Auth("invalid token".into()).is_retryable());
191        assert!(!Error::Config("bad model id".into()).is_retryable());
192        assert!(!Error::Model("content filtered".into()).is_retryable());
193    }
194
195    #[test]
196    fn test_from_provider_error() {
197        let err: Error = ProviderError::Authentication("expired".into()).into();
198        assert!(err.is_auth());
199
200        let err: Error = ProviderError::RateLimited("throttled".into()).into();
201        assert!(err.is_rate_limited());
202
203        let err: Error = ProviderError::Network("timeout".into()).into();
204        assert!(err.is_network());
205    }
206
207    #[test]
208    fn test_from_agent_error() {
209        let err: Error = AgentError::MaxTokensExceeded.into();
210        assert!(err.is_model());
211
212        let err: Error = AgentError::ToolNotFound("calculator".into()).into();
213        assert!(err.is_tool());
214
215        let err: Error = AgentError::Provider(ProviderError::RateLimited("slow".into())).into();
216        assert!(err.is_rate_limited());
217    }
218
219    #[test]
220    fn test_convenience_methods() {
221        assert!(Error::Auth("x".into()).is_auth());
222        assert!(Error::RateLimited("x".into()).is_rate_limited());
223        assert!(Error::Network("x".into()).is_network());
224        assert!(Error::Unavailable("x".into()).is_unavailable());
225        assert!(Error::Model("x".into()).is_model());
226        assert!(Error::Tool("x".into()).is_tool());
227        assert!(Error::Config("x".into()).is_config());
228    }
229}