agent_tui_infra/infra/ipc/
error.rs1use 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}