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