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#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_error_display() {
150        let id = crate::types::AgentId::new_v4();
151        let err = KernelError::AgentNotFound { id };
152        let msg = err.to_string();
153        assert!(msg.contains("not found"));
154    }
155
156    #[test]
157    fn test_all_http_status_mappings() {
158        let id = crate::types::AgentId::new_v4();
159        assert_eq!(
160            u16::from(KernelError::AgentNotFound { id }.http_status()),
161            404
162        );
163        assert_eq!(
164            u16::from(
165                KernelError::PermissionDenied {
166                    reason: "test".into()
167                }
168                .http_status()
169            ),
170            403
171        );
172        assert_eq!(
173            u16::from(KernelError::ProgramNotFound { name: "p".into() }.http_status()),
174            404
175        );
176        assert_eq!(
177            u16::from(KernelError::ProgramAlreadyExists { name: "p".into() }.http_status()),
178            409
179        );
180        assert_eq!(
181            u16::from(
182                KernelError::InvalidConfig {
183                    detail: "bad".into()
184                }
185                .http_status()
186            ),
187            400
188        );
189        assert_eq!(
190            u16::from(KernelError::SeedNotFound { id: "s".into() }.http_status()),
191            404
192        );
193        assert_eq!(
194            u16::from(KernelError::SessionNotFound { id: "s".into() }.http_status()),
195            404
196        );
197    }
198
199    #[test]
200    fn test_internal_error_wrapping() {
201        let err = KernelError::Internal(anyhow::anyhow!("something broke"));
202        assert!(err.to_string().contains("something broke"));
203        assert_eq!(u16::from(err.http_status()), 500);
204    }
205
206    #[test]
207    fn test_io_error_conversion() {
208        let err =
209            KernelError::StateStore(std::io::Error::new(std::io::ErrorKind::NotFound, "gone"));
210        assert!(err.to_string().contains("gone"));
211        assert_eq!(u16::from(err.http_status()), 500);
212    }
213
214    #[test]
215    fn test_timeout_error_status() {
216        let err = KernelError::Timeout {
217            context: "agent execution exceeded 300s".into(),
218        };
219        assert!(err.to_string().contains("timed out"));
220        assert!(err.to_string().contains("300s"));
221        assert_eq!(u16::from(err.http_status()), 503);
222    }
223
224    #[test]
225    fn test_rate_limited_error_status() {
226        let err = KernelError::RateLimited {
227            context: "API calls exceeded 60/min".into(),
228        };
229        assert!(err.to_string().contains("Rate limit exceeded"));
230        assert!(err.to_string().contains("60/min"));
231        assert_eq!(u16::from(err.http_status()), 429);
232    }
233}