1use thiserror::Error;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum SessionBuildResource {
18 MemoryStore,
19 SessionStore,
20 Queue,
21 Mcp,
22 RlTrajectory,
23}
24
25impl std::fmt::Display for SessionBuildResource {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 f.write_str(match self {
28 Self::MemoryStore => "memory store",
29 Self::SessionStore => "session store",
30 Self::Queue => "session queue",
31 Self::Mcp => "MCP",
32 Self::RlTrajectory => "RL trajectory recorder",
33 })
34 }
35}
36
37pub type Result<T> = std::result::Result<T, CodeError>;
39
40#[derive(Debug, Error)]
45pub enum CodeError {
46 #[error("Config error: {0}")]
48 Config(String),
49
50 #[error("LLM error: {0}")]
52 Llm(String),
53
54 #[error("Tool error: {tool}: {message}")]
56 Tool { tool: String, message: String },
57
58 #[error("Session error: {0}")]
60 Session(String),
61
62 #[error("Invalid session configuration for '{field}': {message}")]
64 SessionConfiguration {
65 field: &'static str,
66 message: String,
67 },
68
69 #[error("Failed to initialize {resource}: {message}")]
71 SessionInitialization {
72 resource: SessionBuildResource,
73 message: String,
74 },
75
76 #[error(
79 "{resource} requires asynchronous session construction; use Agent::session_builder(...).build().await"
80 )]
81 AsyncSessionBuildRequired { resource: SessionBuildResource },
82
83 #[error("Session '{session_id}' is closed")]
89 SessionClosed { session_id: String },
90
91 #[error("Session '{session_id}' already has an active operation")]
97 SessionBusy { session_id: String },
98
99 #[error("Task admission for session '{session_id}' was cancelled")]
101 TaskAdmissionCancelled { session_id: String },
102
103 #[error("Task scheduler is closed")]
105 TaskSchedulerClosed,
106
107 #[error("Run '{run_id}' is already bound to different immutable input")]
110 RunIdentityConflict { run_id: String },
111
112 #[error("Budget exhausted on '{resource}': {reason}")]
116 BudgetExhausted { resource: String, reason: String },
117
118 #[error("Security error: {0}")]
120 Security(String),
121
122 #[error("Context error: {0}")]
124 Context(String),
125
126 #[error("MCP error: {0}")]
128 Mcp(String),
129
130 #[error("Queue error: {0}")]
132 Queue(String),
133
134 #[error("IO error: {0}")]
136 Io(#[from] std::io::Error),
137
138 #[error("Serialization error: {0}")]
140 Serialization(#[from] serde_json::Error),
141
142 #[error("{0:#}")]
148 Internal(#[from] anyhow::Error),
149}
150
151impl CodeError {
152 pub const fn code(&self) -> &'static str {
154 match self {
155 Self::Config(_) => "CONFIG_ERROR",
156 Self::Llm(_) => "LLM_ERROR",
157 Self::Tool { .. } => "TOOL_ERROR",
158 Self::Session(_) => "SESSION_ERROR",
159 Self::SessionConfiguration { .. } => "SESSION_CONFIGURATION_ERROR",
160 Self::SessionInitialization { .. } => "SESSION_INITIALIZATION_ERROR",
161 Self::AsyncSessionBuildRequired { .. } => "ASYNC_SESSION_BUILD_REQUIRED",
162 Self::SessionClosed { .. } => "SESSION_CLOSED",
163 Self::SessionBusy { .. } => "SESSION_BUSY",
164 Self::TaskAdmissionCancelled { .. } => "TASK_ADMISSION_CANCELLED",
165 Self::TaskSchedulerClosed => "TASK_SCHEDULER_CLOSED",
166 Self::RunIdentityConflict { .. } => "RUN_IDENTITY_CONFLICT",
167 Self::BudgetExhausted { .. } => "BUDGET_EXHAUSTED",
168 Self::Security(_) => "SECURITY_ERROR",
169 Self::Context(_) => "CONTEXT_ERROR",
170 Self::Mcp(_) => "MCP_ERROR",
171 Self::Queue(_) => "QUEUE_ERROR",
172 Self::Io(_) => "IO_ERROR",
173 Self::Serialization(_) => "SERIALIZATION_ERROR",
174 Self::Internal(_) => "INTERNAL_ERROR",
175 }
176 }
177}
178
179pub(crate) fn read_or_recover<T>(lock: &std::sync::RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
189 lock.read().unwrap_or_else(|p| p.into_inner())
190}
191
192pub(crate) fn write_or_recover<T>(
196 lock: &std::sync::RwLock<T>,
197) -> std::sync::RwLockWriteGuard<'_, T> {
198 lock.write().unwrap_or_else(|p| p.into_inner())
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn test_code_error_config() {
207 let err = CodeError::Config("missing API key".to_string());
208 assert!(err.to_string().contains("Config error"));
209 assert!(err.to_string().contains("missing API key"));
210 }
211
212 #[test]
213 fn test_code_error_llm() {
214 let err = CodeError::Llm("rate limited".to_string());
215 assert!(err.to_string().contains("LLM error"));
216 }
217
218 #[test]
219 fn test_code_error_tool() {
220 let err = CodeError::Tool {
221 tool: "bash".to_string(),
222 message: "command not found".to_string(),
223 };
224 let msg = err.to_string();
225 assert!(msg.contains("bash"));
226 assert!(msg.contains("command not found"));
227 }
228
229 #[test]
230 fn test_code_error_session() {
231 let err = CodeError::Session("not found".to_string());
232 assert!(err.to_string().contains("Session error"));
233 }
234
235 #[test]
236 fn test_code_error_session_configuration_keeps_field_identity() {
237 let err = CodeError::SessionConfiguration {
238 field: "session_id",
239 message: "must not be empty".to_string(),
240 };
241 assert!(err.to_string().contains("session_id"));
242 assert!(err.to_string().contains("must not be empty"));
243 }
244
245 #[test]
246 fn test_code_error_session_busy() {
247 let err = CodeError::SessionBusy {
248 session_id: "session-1".to_string(),
249 };
250 assert!(err.to_string().contains("session-1"));
251 assert!(err.to_string().contains("active operation"));
252 }
253
254 #[test]
255 fn test_code_error_security() {
256 let err = CodeError::Security("taint detected".to_string());
257 assert!(err.to_string().contains("Security error"));
258 }
259
260 #[test]
261 fn test_code_error_context() {
262 let err = CodeError::Context("provider failed".to_string());
263 assert!(err.to_string().contains("Context error"));
264 }
265
266 #[test]
267 fn test_code_error_mcp() {
268 let err = CodeError::Mcp("connection refused".to_string());
269 assert!(err.to_string().contains("MCP error"));
270 }
271
272 #[test]
273 fn test_code_error_queue() {
274 let err = CodeError::Queue("lane full".to_string());
275 assert!(err.to_string().contains("Queue error"));
276 }
277
278 #[test]
279 fn test_code_error_from_io() {
280 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
281 let err: CodeError = io_err.into();
282 assert!(matches!(err, CodeError::Io(_)));
283 assert!(err.to_string().contains("file missing"));
284 }
285
286 #[test]
287 fn test_code_error_from_serde_json() {
288 let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
289 let err: CodeError = json_err.into();
290 assert!(matches!(err, CodeError::Serialization(_)));
291 }
292
293 #[test]
294 fn test_code_error_from_anyhow() {
295 let anyhow_err = anyhow::anyhow!("something went wrong");
296 let err: CodeError = anyhow_err.into();
297 assert!(matches!(err, CodeError::Internal(_)));
298 assert!(err.to_string().contains("something went wrong"));
299 }
300
301 #[test]
302 fn stable_error_codes_cover_control_flow_variants() {
303 assert_eq!(
304 CodeError::SessionBusy {
305 session_id: "session-1".to_string(),
306 }
307 .code(),
308 "SESSION_BUSY"
309 );
310 assert_eq!(
311 CodeError::SessionClosed {
312 session_id: "session-1".to_string(),
313 }
314 .code(),
315 "SESSION_CLOSED"
316 );
317 assert_eq!(
318 CodeError::RunIdentityConflict {
319 run_id: "run-1".to_string(),
320 }
321 .code(),
322 "RUN_IDENTITY_CONFLICT"
323 );
324 assert_eq!(
325 CodeError::BudgetExhausted {
326 resource: "tokens".to_string(),
327 reason: "limit".to_string(),
328 }
329 .code(),
330 "BUDGET_EXHAUSTED"
331 );
332 assert_eq!(
333 CodeError::TaskAdmissionCancelled {
334 session_id: "session-1".to_string(),
335 }
336 .code(),
337 "TASK_ADMISSION_CANCELLED"
338 );
339 assert_eq!(
340 CodeError::TaskSchedulerClosed.code(),
341 "TASK_SCHEDULER_CLOSED"
342 );
343 }
344
345 #[test]
346 fn test_code_error_question_mark_from_anyhow() {
347 fn inner() -> anyhow::Result<()> {
348 anyhow::bail!("inner error")
349 }
350
351 fn outer() -> Result<()> {
352 inner()?; Ok(())
354 }
355
356 let result = outer();
357 assert!(result.is_err());
358 let err = result.unwrap_err();
359 assert!(matches!(err, CodeError::Internal(_)));
360 }
361
362 #[test]
363 fn test_read_or_recover_normal() {
364 let lock = std::sync::RwLock::new(42);
365 let guard = read_or_recover(&lock);
366 assert_eq!(*guard, 42);
367 }
368
369 #[test]
370 fn test_write_or_recover_normal() {
371 let lock = std::sync::RwLock::new(42);
372 let mut guard = write_or_recover(&lock);
373 *guard = 99;
374 drop(guard);
375 assert_eq!(*read_or_recover(&lock), 99);
376 }
377
378 #[test]
379 fn test_read_or_recover_poisoned() {
380 let lock = std::sync::RwLock::new(42);
381 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
383 let _guard = lock.write().unwrap();
384 panic!("intentional poison");
385 }));
386 let guard = read_or_recover(&lock);
388 assert_eq!(*guard, 42);
389 }
390
391 #[test]
392 fn test_write_or_recover_poisoned() {
393 let lock = std::sync::RwLock::new(42);
394 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
395 let _guard = lock.write().unwrap();
396 panic!("intentional poison");
397 }));
398 let mut guard = write_or_recover(&lock);
399 *guard = 100;
400 assert_eq!(*guard, 100);
401 }
402}