Skip to main content

aacode_rs/
error.rs

1// Copyright (c) 2026 xiefujin <490021684@qq.com>
2// Licensed under GPL-3.0, see LICENSE file for full license terms.
3
4//! Unified error type for aacode-rs.
5
6use std::fmt;
7
8/// The central error type for the agent. Kept dependency-free (no thiserror)
9/// to minimize the compiled binary footprint on mobile targets.
10#[derive(Debug)]
11pub enum AacodeError {
12    /// Configuration problem (missing api key, bad model, etc.)
13    Config(String),
14    /// Network / HTTP transport failure (retryable classification lives in llm).
15    Network(String),
16    /// LLM API returned an error status or malformed payload.
17    Api(String),
18    /// Tool invocation failed (bad args, execution error).
19    Tool(String),
20    /// JSON (de)serialization failure.
21    Json(String),
22    /// Filesystem / IO failure.
23    Io(String),
24    /// The task was cancelled by the caller.
25    Cancelled,
26    /// Anything that does not fit the buckets above.
27    Other(String),
28}
29
30impl fmt::Display for AacodeError {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        match self {
33            AacodeError::Config(m) => write!(f, "config error: {m}"),
34            AacodeError::Network(m) => write!(f, "network error: {m}"),
35            AacodeError::Api(m) => write!(f, "api error: {m}"),
36            AacodeError::Tool(m) => write!(f, "tool error: {m}"),
37            AacodeError::Json(m) => write!(f, "json error: {m}"),
38            AacodeError::Io(m) => write!(f, "io error: {m}"),
39            AacodeError::Cancelled => write!(f, "cancelled"),
40            AacodeError::Other(m) => write!(f, "{m}"),
41        }
42    }
43}
44
45impl std::error::Error for AacodeError {}
46
47impl From<serde_json::Error> for AacodeError {
48    fn from(e: serde_json::Error) -> Self {
49        AacodeError::Json(e.to_string())
50    }
51}
52
53impl From<std::io::Error> for AacodeError {
54    fn from(e: std::io::Error) -> Self {
55        AacodeError::Io(e.to_string())
56    }
57}
58
59/// Convenience result alias used across the crate.
60pub type Result<T> = std::result::Result<T, AacodeError>;
61
62impl AacodeError {
63    /// Whether this error is worth retrying (transient network / 5xx / timeout).
64    pub fn is_retryable(&self) -> bool {
65        match self {
66            AacodeError::Network(_) => true,
67            AacodeError::Json(_) => true,
68            AacodeError::Api(m) => {
69                let l = m.to_lowercase();
70                l.contains("timeout")
71                    || l.contains("timed out")
72                    || l.contains("connection")
73                    || l.contains(" 500")
74                    || l.contains(" 502")
75                    || l.contains(" 503")
76                    || l.contains(" 504")
77                    || l.contains("rate limit")
78            }
79            _ => false,
80        }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn display_formats() {
90        assert_eq!(AacodeError::Cancelled.to_string(), "cancelled");
91        assert!(AacodeError::Config("x".into())
92            .to_string()
93            .contains("config"));
94    }
95
96    #[test]
97    fn retryable_classification() {
98        assert!(AacodeError::Network("reset".into()).is_retryable());
99        assert!(AacodeError::Api("HTTP 503 unavailable".into()).is_retryable());
100        assert!(AacodeError::Api("rate limit exceeded".into()).is_retryable());
101        assert!(AacodeError::Json("malformed response".into()).is_retryable());
102        assert!(!AacodeError::Api("HTTP 401 unauthorized".into()).is_retryable());
103        assert!(!AacodeError::Config("no key".into()).is_retryable());
104    }
105
106    #[test]
107    fn json_error_converts() {
108        let e: AacodeError = serde_json::from_str::<i32>("not json").unwrap_err().into();
109        matches!(e, AacodeError::Json(_));
110    }
111}