Skip to main content

acp_cli/
error.rs

1use thiserror::Error;
2
3#[derive(Debug, Error)]
4pub enum AcpCliError {
5    #[error("agent error: {0}")]
6    Agent(String),
7
8    #[error("usage error: {0}")]
9    Usage(String),
10
11    #[error("timeout after {0}s")]
12    Timeout(u64),
13
14    #[error("no session found for {agent} in {cwd}")]
15    NoSession { agent: String, cwd: String },
16
17    #[error("permission denied: {0}")]
18    PermissionDenied(String),
19
20    #[error("interrupted")]
21    Interrupted,
22
23    #[error(transparent)]
24    Io(#[from] std::io::Error),
25
26    #[error("acp connection failed: {0}")]
27    Connection(String),
28}
29
30impl AcpCliError {
31    pub fn exit_code(&self) -> i32 {
32        match self {
33            Self::Agent(_) | Self::Io(_) | Self::Connection(_) => 1,
34            Self::Usage(_) => 2,
35            Self::Timeout(_) => 3,
36            Self::NoSession { .. } => 4,
37            Self::PermissionDenied(_) => 5,
38            Self::Interrupted => 130,
39        }
40    }
41}
42
43/// Returns `true` for errors that are safe to retry (network/connection failures
44/// before any agent output was produced). This includes agent process spawn
45/// failures, ACP initialization/session-creation failures, and bridge channel
46/// closures — all mapped to `Connection` in the bridge layer.
47///
48/// Non-retriable errors include semantic ACP failures (permission denied,
49/// session not found), user-initiated interrupts, timeouts (which may imply
50/// partial side effects), and I/O errors unrelated to the network layer.
51pub fn is_transient(err: &AcpCliError) -> bool {
52    matches!(err, AcpCliError::Connection(_))
53}
54
55pub type Result<T> = std::result::Result<T, AcpCliError>;
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn connection_error_is_transient() {
63        assert!(is_transient(&AcpCliError::Connection("refused".into())));
64    }
65
66    #[test]
67    fn agent_error_is_not_transient() {
68        assert!(!is_transient(&AcpCliError::Agent(
69            "permission denied".into()
70        )));
71    }
72
73    #[test]
74    fn timeout_is_not_transient() {
75        // Timeout may indicate side effects already occurred; not retried by default.
76        assert!(!is_transient(&AcpCliError::Timeout(30)));
77    }
78
79    #[test]
80    fn interrupted_is_not_transient() {
81        assert!(!is_transient(&AcpCliError::Interrupted));
82    }
83
84    #[test]
85    fn io_error_is_not_transient() {
86        assert!(!is_transient(&AcpCliError::Io(std::io::Error::new(
87            std::io::ErrorKind::BrokenPipe,
88            "broken pipe"
89        ))));
90    }
91}