1use thiserror::Error;
7
8#[derive(Debug, Error)]
10pub enum KernelError {
11 #[error("Agent {id} not found")]
13 AgentNotFound {
14 id: crate::types::AgentId,
16 },
17
18 #[error("Permission denied: {reason}")]
20 PermissionDenied {
21 reason: String,
23 },
24
25 #[error("Program '{name}' not found")]
27 ProgramNotFound {
28 name: String,
30 },
31
32 #[error("Program '{name}' already installed")]
34 ProgramAlreadyExists {
35 name: String,
37 },
38
39 #[error("Invalid configuration: {detail}")]
41 InvalidConfig {
42 detail: String,
44 },
45
46 #[error("Session '{id}' not found")]
48 SessionNotFound {
49 id: String,
51 },
52
53 #[error("State store error: {0}")]
55 StateStore(#[from] std::io::Error),
56
57 #[error("{0}")]
59 Internal(#[from] anyhow::Error),
60
61 #[error("Memory error: {reason}")]
63 Memory {
64 reason: String,
66 },
67
68 #[error("Operation timed out: {context}")]
70 Timeout {
71 context: String,
73 },
74
75 #[error("Rate limit exceeded: {context}")]
77 RateLimited {
78 context: String,
80 },
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum HttpStatus {
86 Ok = 200,
88 BadRequest = 400,
90 Forbidden = 403,
92 NotFound = 404,
94 Conflict = 409,
96 TooManyRequests = 429,
98 InternalServerError = 500,
100 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 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
133pub type KernelResult<T> = Result<T, KernelError>;
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum ErrorCategory {
139 NotFound,
141 Conflict,
143 Security,
145 Timeout,
147 RateLimit,
149 Config,
151 Execution,
153 Infrastructure,
155 Internal,
157}
158
159impl KernelError {
160 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 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}