1use thiserror::Error;
2
3#[derive(Debug, Error)]
4#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
5#[non_exhaustive]
6pub enum AtdError {
7 #[error("tool not found: {tool_id}")]
8 ToolNotFound {
9 tool_id: String,
10 suggestions: Vec<String>,
11 },
12
13 #[error("invalid arguments for {tool_id}: field `{field}` — {reason}")]
14 InvalidArguments {
15 tool_id: String,
16 field: String,
17 reason: String,
18 },
19
20 #[error("capability denied for {tool_id}: required={required:?} granted={granted:?}")]
21 CapabilityDenied {
22 tool_id: String,
23 required: Vec<String>,
24 granted: Vec<String>,
25 },
26
27 #[error("no binding available for {tool_id}: tried={tried:?} ({reason})")]
28 BindingUnavailable {
29 tool_id: String,
30 tried: Vec<String>,
31 reason: String,
32 },
33
34 #[error("tool execution failed: {tool_id}")]
35 #[cfg_attr(feature = "schema", schemars(skip))]
38 ToolExecutionFailed {
39 tool_id: String,
40 #[source]
41 inner: Box<dyn std::error::Error + Send + Sync>,
42 },
43
44 #[error("timed out calling {tool_id} after {after_ms}ms")]
45 Timeout { tool_id: String, after_ms: u64 },
46
47 #[error("server unreachable: {0}")]
48 #[cfg_attr(feature = "schema", schemars(skip))]
49 ServerUnreachable(#[from] std::io::Error),
50
51 #[error("not implemented: {feature}")]
52 NotImplemented { feature: String },
53
54 #[error("protocol error: expected {expected}, got {got}")]
55 ProtocolError { expected: String, got: String },
56
57 #[error("pagination limit exceeded: fetched {pages_fetched} pages / {bytes_fetched} bytes")]
61 #[cfg_attr(feature = "schema", schemars(skip))]
62 PaginationLimitExceeded {
63 pages_fetched: u32,
64 bytes_fetched: usize,
65 },
66
67 #[error("page merge failed: {reason}")]
71 MergeFailed { reason: String },
72}
73
74impl AtdError {
75 pub fn is_retryable(&self) -> bool {
76 matches!(
77 self,
78 AtdError::Timeout { .. }
79 | AtdError::ServerUnreachable(_)
80 | AtdError::BindingUnavailable { .. }
81 )
82 }
83
84 pub fn suggest_fix(&self) -> Option<String> {
85 match self {
86 AtdError::ToolNotFound { suggestions, .. } if !suggestions.is_empty() => {
87 Some(format!("did you mean '{}'?", suggestions[0]))
88 }
89 AtdError::ToolNotFound { .. } => {
90 Some("try `atd list --query <keyword>` to find available tools".into())
91 }
92 AtdError::CapabilityDenied { tool_id, .. } => Some(format!(
93 "run `atd allow {tool_id}` to grant for this session"
94 )),
95 AtdError::ServerUnreachable(_) => {
96 Some("is the ANOS daemon running? try `anos daemon status`".into())
97 }
98 AtdError::Timeout { tool_id, .. } => {
99 Some(format!("increase timeout or retry; tool_id={tool_id}"))
100 }
101 _ => None,
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn tool_not_found_suggests_candidate() {
112 let e = AtdError::ToolNotFound {
113 tool_id: "fs.red".into(),
114 suggestions: vec!["fs.read".into()],
115 };
116 assert_eq!(e.suggest_fix().unwrap(), "did you mean 'fs.read'?");
117 assert!(!e.is_retryable());
118 }
119
120 #[test]
121 fn tool_not_found_without_suggestions_hints_discovery() {
122 let e = AtdError::ToolNotFound {
123 tool_id: "xx".into(),
124 suggestions: vec![],
125 };
126 assert!(e.suggest_fix().unwrap().contains("atd list"));
127 }
128
129 #[test]
130 fn timeout_is_retryable() {
131 let e = AtdError::Timeout {
132 tool_id: "fs.read".into(),
133 after_ms: 5000,
134 };
135 assert!(e.is_retryable());
136 }
137
138 #[test]
139 fn io_error_converts_to_server_unreachable() {
140 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "no");
141 let e: AtdError = io_err.into();
142 assert!(matches!(e, AtdError::ServerUnreachable(_)));
143 assert!(e.is_retryable());
144 }
145
146 #[test]
147 fn display_includes_tool_id() {
148 let e = AtdError::InvalidArguments {
149 tool_id: "fs.read".into(),
150 field: "path".into(),
151 reason: "must be string".into(),
152 };
153 let s = format!("{e}");
154 assert!(s.contains("fs.read"));
155 assert!(s.contains("path"));
156 }
157}