agent_tui/common/
error_codes.rs

1//! Semantic error codes for JSON-RPC domain errors.
2//!
3//! Error codes follow the JSON-RPC 2.0 specification:
4//! - -32700 to -32600: Reserved protocol errors
5//! - -32000 to -32099: Server errors (we use -32001 to -32020 for domain errors)
6
7// Session-related errors
8pub const SESSION_NOT_FOUND: i32 = -32001;
9pub const NO_ACTIVE_SESSION: i32 = -32002;
10pub const SESSION_LIMIT: i32 = -32006;
11pub const LOCK_TIMEOUT: i32 = -32007;
12
13// Element-related errors
14pub const ELEMENT_NOT_FOUND: i32 = -32003;
15pub const WRONG_ELEMENT_TYPE: i32 = -32004;
16
17// Input/operation errors
18pub const INVALID_KEY: i32 = -32005;
19pub const PTY_ERROR: i32 = -32008;
20
21// Wait/timing errors
22pub const WAIT_TIMEOUT: i32 = -32013;
23
24// Process errors
25pub const COMMAND_NOT_FOUND: i32 = -32014;
26pub const PERMISSION_DENIED: i32 = -32015;
27
28// Daemon errors
29pub const DAEMON_ERROR: i32 = -32016;
30pub const PERSISTENCE_ERROR: i32 = -32017;
31
32// Legacy generic error (for backwards compatibility)
33pub const GENERIC_ERROR: i32 = -32000;
34
35/// Error category for programmatic handling by AI agents.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum ErrorCategory {
38    /// Resource not found (session, element)
39    NotFound,
40    /// Invalid input parameters
41    InvalidInput,
42    /// Resource busy or locked
43    Busy,
44    /// Internal server error
45    Internal,
46    /// External dependency failure (PTY, process)
47    External,
48    /// Operation timed out
49    Timeout,
50}
51
52impl ErrorCategory {
53    pub fn as_str(&self) -> &'static str {
54        match self {
55            ErrorCategory::NotFound => "not_found",
56            ErrorCategory::InvalidInput => "invalid_input",
57            ErrorCategory::Busy => "busy",
58            ErrorCategory::Internal => "internal",
59            ErrorCategory::External => "external",
60            ErrorCategory::Timeout => "timeout",
61        }
62    }
63}
64
65impl std::str::FromStr for ErrorCategory {
66    type Err = ();
67
68    fn from_str(s: &str) -> Result<Self, Self::Err> {
69        match s {
70            "not_found" => Ok(ErrorCategory::NotFound),
71            "invalid_input" => Ok(ErrorCategory::InvalidInput),
72            "busy" => Ok(ErrorCategory::Busy),
73            "internal" => Ok(ErrorCategory::Internal),
74            "external" => Ok(ErrorCategory::External),
75            "timeout" => Ok(ErrorCategory::Timeout),
76            _ => Err(()),
77        }
78    }
79}
80
81impl std::fmt::Display for ErrorCategory {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        write!(f, "{}", self.as_str())
84    }
85}
86
87/// Returns whether an error code represents a retriable operation.
88///
89/// Retriable errors are transient conditions that may succeed on retry:
90/// - Lock timeouts (another operation in progress)
91/// - Connection issues (daemon busy)
92pub fn is_retryable(code: i32) -> bool {
93    matches!(code, LOCK_TIMEOUT | GENERIC_ERROR)
94}
95
96/// Returns the error category for a given error code.
97pub fn category_for_code(code: i32) -> ErrorCategory {
98    match code {
99        SESSION_NOT_FOUND | NO_ACTIVE_SESSION | ELEMENT_NOT_FOUND => ErrorCategory::NotFound,
100        WRONG_ELEMENT_TYPE | INVALID_KEY => ErrorCategory::InvalidInput,
101        SESSION_LIMIT | LOCK_TIMEOUT => ErrorCategory::Busy,
102        PTY_ERROR | COMMAND_NOT_FOUND | PERMISSION_DENIED | DAEMON_ERROR | PERSISTENCE_ERROR => {
103            ErrorCategory::External
104        }
105        WAIT_TIMEOUT => ErrorCategory::Timeout,
106        _ => ErrorCategory::Internal,
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_is_retryable_lock_timeout() {
116        assert!(is_retryable(LOCK_TIMEOUT));
117    }
118
119    #[test]
120    fn test_is_retryable_generic() {
121        assert!(is_retryable(GENERIC_ERROR));
122    }
123
124    #[test]
125    fn test_not_retryable_session_not_found() {
126        assert!(!is_retryable(SESSION_NOT_FOUND));
127    }
128
129    #[test]
130    fn test_not_retryable_element_not_found() {
131        assert!(!is_retryable(ELEMENT_NOT_FOUND));
132    }
133
134    #[test]
135    fn test_category_for_code_not_found() {
136        assert_eq!(
137            category_for_code(SESSION_NOT_FOUND),
138            ErrorCategory::NotFound
139        );
140        assert_eq!(
141            category_for_code(NO_ACTIVE_SESSION),
142            ErrorCategory::NotFound
143        );
144        assert_eq!(
145            category_for_code(ELEMENT_NOT_FOUND),
146            ErrorCategory::NotFound
147        );
148    }
149
150    #[test]
151    fn test_category_for_code_invalid_input() {
152        assert_eq!(
153            category_for_code(WRONG_ELEMENT_TYPE),
154            ErrorCategory::InvalidInput
155        );
156        assert_eq!(category_for_code(INVALID_KEY), ErrorCategory::InvalidInput);
157    }
158
159    #[test]
160    fn test_category_for_code_busy() {
161        assert_eq!(category_for_code(SESSION_LIMIT), ErrorCategory::Busy);
162        assert_eq!(category_for_code(LOCK_TIMEOUT), ErrorCategory::Busy);
163    }
164
165    #[test]
166    fn test_category_for_code_external() {
167        assert_eq!(category_for_code(PTY_ERROR), ErrorCategory::External);
168        assert_eq!(
169            category_for_code(COMMAND_NOT_FOUND),
170            ErrorCategory::External
171        );
172        assert_eq!(
173            category_for_code(PERMISSION_DENIED),
174            ErrorCategory::External
175        );
176        assert_eq!(category_for_code(DAEMON_ERROR), ErrorCategory::External);
177        assert_eq!(
178            category_for_code(PERSISTENCE_ERROR),
179            ErrorCategory::External
180        );
181    }
182
183    #[test]
184    fn test_category_for_code_timeout() {
185        assert_eq!(category_for_code(WAIT_TIMEOUT), ErrorCategory::Timeout);
186    }
187
188    #[test]
189    fn test_category_as_str() {
190        assert_eq!(ErrorCategory::NotFound.as_str(), "not_found");
191        assert_eq!(ErrorCategory::InvalidInput.as_str(), "invalid_input");
192        assert_eq!(ErrorCategory::Busy.as_str(), "busy");
193        assert_eq!(ErrorCategory::Internal.as_str(), "internal");
194        assert_eq!(ErrorCategory::External.as_str(), "external");
195        assert_eq!(ErrorCategory::Timeout.as_str(), "timeout");
196    }
197
198    #[test]
199    fn test_category_from_str() {
200        assert_eq!(
201            "not_found".parse::<ErrorCategory>(),
202            Ok(ErrorCategory::NotFound)
203        );
204        assert_eq!(
205            "invalid_input".parse::<ErrorCategory>(),
206            Ok(ErrorCategory::InvalidInput)
207        );
208        assert_eq!("busy".parse::<ErrorCategory>(), Ok(ErrorCategory::Busy));
209        assert_eq!(
210            "internal".parse::<ErrorCategory>(),
211            Ok(ErrorCategory::Internal)
212        );
213        assert_eq!(
214            "external".parse::<ErrorCategory>(),
215            Ok(ErrorCategory::External)
216        );
217        assert_eq!(
218            "timeout".parse::<ErrorCategory>(),
219            Ok(ErrorCategory::Timeout)
220        );
221        assert!("unknown".parse::<ErrorCategory>().is_err());
222    }
223}