Skip to main content

oxios_kernel/
error.rs

1//! Typed error types for the Oxios kernel public API.
2//!
3//! Library consumers should match on these variants for structured error handling.
4//! Internal implementation uses `anyhow` and wraps into [`KernelError::Internal`].
5
6use thiserror::Error;
7
8/// Oxios kernel error type.
9#[derive(Debug, Error)]
10pub enum KernelError {
11    /// Requested agent was not found.
12    #[error("Agent {id} not found")]
13    AgentNotFound {
14        /// The agent identifier.
15        id: crate::types::AgentId,
16    },
17
18    /// Permission denied for the requested operation.
19    #[error("Permission denied: {reason}")]
20    PermissionDenied {
21        /// Why permission was denied.
22        reason: String,
23    },
24
25    /// Requested program was not found.
26    #[error("Program '{name}' not found")]
27    ProgramNotFound {
28        /// Program name.
29        name: String,
30    },
31
32    /// A program with this name is already installed.
33    #[error("Program '{name}' already installed")]
34    ProgramAlreadyExists {
35        /// Program name.
36        name: String,
37    },
38
39    /// Invalid configuration value.
40    #[error("Invalid configuration: {detail}")]
41    InvalidConfig {
42        /// What's invalid.
43        detail: String,
44    },
45
46    /// Requested seed was not found.
47    #[error("Seed '{id}' not found")]
48    SeedNotFound {
49        /// Seed identifier.
50        id: String,
51    },
52
53    /// Requested session was not found.
54    #[error("Session '{id}' not found")]
55    SessionNotFound {
56        /// Session identifier.
57        id: String,
58    },
59
60    /// I/O error from the state store.
61    #[error("State store error: {0}")]
62    StateStore(#[from] std::io::Error),
63
64    /// An internal error wrapped from anyhow.
65    #[error("{0}")]
66    Internal(#[from] anyhow::Error),
67
68    /// Memory subsystem error (HNSW index, embedding, etc.).
69    #[error("Memory error: {reason}")]
70    Memory {
71        /// Detailed error reason.
72        reason: String,
73    },
74
75    /// Operation timed out.
76    #[error("Operation timed out: {context}")]
77    Timeout {
78        /// Context describing what timed out.
79        context: String,
80    },
81
82    /// Rate limit exceeded.
83    #[error("Rate limit exceeded: {context}")]
84    RateLimited {
85        /// Context describing what was rate limited.
86        context: String,
87    },
88}
89
90/// HTTP status code mapping (independent of any web framework).
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum HttpStatus {
93    /// 200 OK
94    Ok = 200,
95    /// 400 Bad Request
96    BadRequest = 400,
97    /// 403 Forbidden
98    Forbidden = 403,
99    /// 404 Not Found
100    NotFound = 404,
101    /// 409 Conflict
102    Conflict = 409,
103    /// 429 Too Many Requests
104    TooManyRequests = 429,
105    /// 500 Internal Server Error
106    InternalServerError = 500,
107    /// 503 Service Unavailable
108    ServiceUnavailable = 503,
109}
110
111impl From<HttpStatus> for u16 {
112    fn from(status: HttpStatus) -> u16 {
113        status as u16
114    }
115}
116
117impl KernelError {
118    /// Map this error to an HTTP-compatible status code.
119    ///
120    /// Returns a framework-agnostic [`HttpStatus`] that consumers can convert
121    /// to their web framework's status type.
122    pub fn http_status(&self) -> HttpStatus {
123        match self {
124            Self::AgentNotFound { .. } => HttpStatus::NotFound,
125            Self::PermissionDenied { .. } => HttpStatus::Forbidden,
126
127            Self::ProgramNotFound { .. } => HttpStatus::NotFound,
128            Self::ProgramAlreadyExists { .. } => HttpStatus::Conflict,
129            Self::InvalidConfig { .. } => HttpStatus::BadRequest,
130            Self::SeedNotFound { .. } => HttpStatus::NotFound,
131            Self::SessionNotFound { .. } => HttpStatus::NotFound,
132            Self::StateStore(_) => HttpStatus::InternalServerError,
133            Self::Memory { .. } => HttpStatus::InternalServerError,
134            Self::Timeout { .. } => HttpStatus::ServiceUnavailable,
135            Self::RateLimited { .. } => HttpStatus::TooManyRequests,
136            Self::Internal(_) => HttpStatus::InternalServerError,
137        }
138    }
139}
140
141/// Convenience alias for results using [`KernelError`].
142pub type KernelResult<T> = Result<T, KernelError>;
143
144/// Error categories for recovery decisions and monitoring.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum ErrorCategory {
147    /// Entity not found.
148    NotFound,
149    /// Resource already exists (conflict).
150    Conflict,
151    /// Permission/security violation.
152    Security,
153    /// Operation timed out.
154    Timeout,
155    /// Rate limit exceeded.
156    RateLimit,
157    /// Invalid configuration.
158    Config,
159    /// Execution failed (agent crash, etc.).
160    Execution,
161    /// Infrastructure error (disk, network, etc.).
162    Infrastructure,
163    /// Internal/unknown error.
164    Internal,
165}
166
167impl KernelError {
168    /// Classify this error into a category.
169    ///
170    /// Useful for error monitoring, alerting, and retry decisions.
171    pub fn category(&self) -> ErrorCategory {
172        match self {
173            Self::AgentNotFound { .. } => ErrorCategory::NotFound,
174            Self::SeedNotFound { .. } => ErrorCategory::NotFound,
175            Self::SessionNotFound { .. } => ErrorCategory::NotFound,
176            Self::PermissionDenied { .. } => ErrorCategory::Security,
177            Self::ProgramNotFound { .. } => ErrorCategory::NotFound,
178            Self::ProgramAlreadyExists { .. } => ErrorCategory::Conflict,
179            Self::InvalidConfig { .. } => ErrorCategory::Config,
180            Self::StateStore(_) => ErrorCategory::Infrastructure,
181            Self::Memory { .. } => ErrorCategory::Infrastructure,
182            Self::Timeout { .. } => ErrorCategory::Timeout,
183            Self::RateLimited { .. } => ErrorCategory::RateLimit,
184            Self::Internal(_) => ErrorCategory::Internal,
185        }
186    }
187    /// Whether this error is retryable.
188    ///
189    /// Timeout and rate limit errors are retryable after backoff.
190    /// Infrastructure errors may be retryable.
191    pub fn is_retryable(&self) -> bool {
192        matches!(
193            self.category(),
194            ErrorCategory::Timeout | ErrorCategory::RateLimit | ErrorCategory::Infrastructure
195        )
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn test_error_display() {
205        let id = crate::types::AgentId::new_v4();
206        let err = KernelError::AgentNotFound { id };
207        let msg = err.to_string();
208        assert!(msg.contains("not found"));
209    }
210
211    #[test]
212    fn test_all_http_status_mappings() {
213        let id = crate::types::AgentId::new_v4();
214        assert_eq!(
215            u16::from(KernelError::AgentNotFound { id }.http_status()),
216            404
217        );
218        assert_eq!(
219            u16::from(
220                KernelError::PermissionDenied {
221                    reason: "test".into()
222                }
223                .http_status()
224            ),
225            403
226        );
227        assert_eq!(
228            u16::from(KernelError::ProgramNotFound { name: "p".into() }.http_status()),
229            404
230        );
231        assert_eq!(
232            u16::from(KernelError::ProgramAlreadyExists { name: "p".into() }.http_status()),
233            409
234        );
235        assert_eq!(
236            u16::from(
237                KernelError::InvalidConfig {
238                    detail: "bad".into()
239                }
240                .http_status()
241            ),
242            400
243        );
244        assert_eq!(
245            u16::from(KernelError::SeedNotFound { id: "s".into() }.http_status()),
246            404
247        );
248        assert_eq!(
249            u16::from(KernelError::SessionNotFound { id: "s".into() }.http_status()),
250            404
251        );
252    }
253
254    #[test]
255    fn test_internal_error_wrapping() {
256        let err = KernelError::Internal(anyhow::anyhow!("something broke"));
257        assert!(err.to_string().contains("something broke"));
258        assert_eq!(u16::from(err.http_status()), 500);
259    }
260
261    #[test]
262    fn test_io_error_conversion() {
263        let err =
264            KernelError::StateStore(std::io::Error::new(std::io::ErrorKind::NotFound, "gone"));
265        assert!(err.to_string().contains("gone"));
266        assert_eq!(u16::from(err.http_status()), 500);
267    }
268
269    #[test]
270    fn test_timeout_error_status() {
271        let err = KernelError::Timeout {
272            context: "agent execution exceeded 300s".into(),
273        };
274        assert!(err.to_string().contains("timed out"));
275        assert!(err.to_string().contains("300s"));
276        assert_eq!(u16::from(err.http_status()), 503);
277    }
278
279    #[test]
280    fn test_rate_limited_error_status() {
281        let err = KernelError::RateLimited {
282            context: "API calls exceeded 60/min".into(),
283        };
284        assert!(err.to_string().contains("Rate limit exceeded"));
285        assert!(err.to_string().contains("60/min"));
286        assert_eq!(u16::from(err.http_status()), 429);
287    }
288}