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