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("Seed '{id}' not found")]
48 SeedNotFound {
49 id: String,
51 },
52
53 #[error("Session '{id}' not found")]
55 SessionNotFound {
56 id: String,
58 },
59
60 #[error("State store error: {0}")]
62 StateStore(#[from] std::io::Error),
63
64 #[error("{0}")]
66 Internal(#[from] anyhow::Error),
67
68 #[error("Memory error: {reason}")]
70 Memory {
71 reason: String,
73 },
74
75 #[error("Operation timed out: {context}")]
77 Timeout {
78 context: String,
80 },
81
82 #[error("Rate limit exceeded: {context}")]
84 RateLimited {
85 context: String,
87 },
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum HttpStatus {
93 Ok = 200,
95 BadRequest = 400,
97 Forbidden = 403,
99 NotFound = 404,
101 Conflict = 409,
103 TooManyRequests = 429,
105 InternalServerError = 500,
107 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 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
141pub type KernelResult<T> = Result<T, KernelError>;
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum ErrorCategory {
147 NotFound,
149 Conflict,
151 Security,
153 Timeout,
155 RateLimit,
157 Config,
159 Execution,
161 Infrastructure,
163 Internal,
165}
166
167impl KernelError {
168 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 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}