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