Skip to main content

agent_tui_infra/infra/ipc/
error.rs

1//! IPC error types and helpers.
2
3use serde_json::Value;
4use thiserror::Error;
5use tracing::debug;
6
7use crate::common::error_codes::ErrorCategory;
8
9#[derive(Error, Debug)]
10pub enum ClientError {
11    #[error("Failed to connect to daemon: {0}")]
12    ConnectionFailed(#[from] std::io::Error),
13
14    #[error("Failed to serialize request: {0}")]
15    SerializationFailed(#[from] serde_json::Error),
16
17    #[error("RPC error ({code}): {message}")]
18    RpcError {
19        code: i32,
20        message: String,
21        category: Option<ErrorCategory>,
22        retryable: bool,
23        retry_delay_ms: Option<u64>,
24        context: Option<Value>,
25        suggestion: Option<String>,
26    },
27
28    #[error("Daemon not running")]
29    DaemonNotRunning,
30
31    #[error("Invalid daemon state in {path}: {message}")]
32    DaemonStateInvalid { path: String, message: String },
33
34    #[error("Invalid response from daemon")]
35    InvalidResponse,
36
37    #[error("Failed to send signal to process {pid}: {message}")]
38    SignalFailed {
39        pid: u32,
40        message: String,
41        #[source]
42        source: Option<std::io::Error>,
43    },
44
45    #[error("Unexpected response: {message}")]
46    UnexpectedResponse { message: String },
47}
48
49impl ClientError {
50    pub fn is_retryable(&self) -> bool {
51        match self {
52            ClientError::RpcError { retryable, .. } => *retryable,
53            _ => false,
54        }
55    }
56
57    pub fn retry_delay_ms(&self) -> Option<u64> {
58        match self {
59            ClientError::RpcError { retry_delay_ms, .. } => *retry_delay_ms,
60            _ => None,
61        }
62    }
63
64    pub fn category(&self) -> Option<ErrorCategory> {
65        match self {
66            ClientError::RpcError { category, .. } => *category,
67            _ => None,
68        }
69    }
70
71    pub fn suggestion(&self) -> Option<&str> {
72        match self {
73            ClientError::RpcError { suggestion, .. } => suggestion.as_deref(),
74            ClientError::DaemonNotRunning => Some("Start daemon with: agent-tui daemon start"),
75            ClientError::DaemonStateInvalid { .. } => {
76                Some("Remove the stale daemon lock file and retry.")
77            }
78            _ => None,
79        }
80    }
81
82    pub fn to_json(&self) -> Value {
83        match self {
84            ClientError::RpcError {
85                code,
86                message,
87                category,
88                retryable,
89                retry_delay_ms,
90                context,
91                suggestion,
92            } => {
93                let mut obj = serde_json::json!({
94                    "code": code,
95                    "message": message,
96                    "retryable": retryable,
97                });
98                if let Some(delay_ms) = retry_delay_ms {
99                    obj["retry_delay_ms"] = serde_json::json!(delay_ms);
100                }
101                if let Some(cat) = category {
102                    obj["category"] = serde_json::json!(cat.as_str());
103                }
104                if let Some(ctx) = context {
105                    obj["context"] = ctx.clone();
106                }
107                if let Some(sug) = suggestion {
108                    obj["suggestion"] = serde_json::json!(sug);
109                }
110                obj
111            }
112            ClientError::ConnectionFailed(e) => serde_json::json!({
113                "code": -32000,
114                "message": format!("Connection failed: {}", e),
115                "category": "external",
116                "retryable": true,
117            }),
118            ClientError::DaemonNotRunning => serde_json::json!({
119                "code": -32000,
120                "message": "Daemon not running",
121                "category": "external",
122                "retryable": false,
123                "suggestion": "Start daemon with: agent-tui daemon start",
124            }),
125            ClientError::DaemonStateInvalid { path, message } => serde_json::json!({
126                "code": -32000,
127                "message": format!("Invalid daemon state in {}: {}", path, message),
128                "category": "internal",
129                "retryable": false,
130                "suggestion": "Remove the stale daemon lock file and retry.",
131            }),
132            ClientError::InvalidResponse => serde_json::json!({
133                "code": -32000,
134                "message": "Invalid response from daemon",
135                "category": "internal",
136                "retryable": false,
137            }),
138            ClientError::SerializationFailed(e) => serde_json::json!({
139                "code": -32000,
140                "message": format!("Serialization failed: {}", e),
141                "category": "internal",
142                "retryable": false,
143            }),
144            ClientError::SignalFailed { pid, message, .. } => serde_json::json!({
145                "code": -32000,
146                "message": format!("Failed to send signal to process {}: {}", pid, message),
147                "category": "external",
148                "retryable": false,
149                "exit_code": 74,
150            }),
151            ClientError::UnexpectedResponse { message } => serde_json::json!({
152                "code": -32000,
153                "message": format!("Unexpected response: {}", message),
154                "category": "internal",
155                "retryable": false,
156            }),
157        }
158    }
159
160    pub fn to_json_string(&self) -> String {
161        serde_json::to_string_pretty(&self.to_json()).unwrap_or_else(|err| {
162            debug!(error = %err, "Failed to serialize IPC error to JSON");
163            String::new()
164        })
165    }
166}