Skip to main content

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