1use serde::Serialize;
4use std::fmt;
5
6#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
8#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
9pub enum ErrorCode {
10 MissingRequiredField,
12 InvalidFieldValue,
13 InvalidState,
14 InvalidPath,
15 InvalidPrefix,
16
17 AgentNotFound,
19 TaskNotFound,
20 FileNotFound,
21 AttachmentNotFound,
22
23 AlreadyClaimed,
25 AlreadyExists,
26 DependencyCycle,
27 TagMismatch,
28 NotOwner,
29 DependencyNotSatisfied,
30 GatesNotSatisfied,
31
32 DatabaseError,
34 InternalError,
35 UnknownTool,
36}
37
38#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
40#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
41pub enum WarningCode {
42 TaskNotFound,
44 DependencyNotFound,
46 UnknownTag,
48 UnknownPhase,
50 Duplicate,
52 Deprecated,
54}
55
56#[derive(Debug, Clone, Serialize)]
58pub struct ToolWarning {
59 pub code: WarningCode,
60 pub message: String,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub field: Option<String>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub value: Option<String>,
65}
66
67impl ToolWarning {
68 pub fn new(code: WarningCode, message: impl Into<String>) -> Self {
69 Self {
70 code,
71 message: message.into(),
72 field: None,
73 value: None,
74 }
75 }
76
77 pub fn with_field(mut self, field: impl Into<String>) -> Self {
78 self.field = Some(field.into());
79 self
80 }
81
82 pub fn with_value(mut self, value: impl Into<String>) -> Self {
83 self.value = Some(value.into());
84 self
85 }
86
87 pub fn task_not_found(task_id: &str) -> Self {
90 Self::new(
91 WarningCode::TaskNotFound,
92 format!("Task '{}' not found, skipped", task_id),
93 )
94 .with_value(task_id)
95 }
96
97 pub fn dependency_not_found(task_id: &str, field: &str) -> Self {
98 Self::new(
99 WarningCode::DependencyNotFound,
100 format!("Dependency target '{}' not found, link skipped", task_id),
101 )
102 .with_field(field)
103 .with_value(task_id)
104 }
105
106 pub fn unknown_tag(tag: &str) -> Self {
107 Self::new(
108 WarningCode::UnknownTag,
109 format!("Tag '{}' is not in known tags list", tag),
110 )
111 .with_value(tag)
112 }
113
114 pub fn unknown_phase(phase: &str) -> Self {
115 Self::new(
116 WarningCode::UnknownPhase,
117 format!("Phase '{}' is not in known phases list", phase),
118 )
119 .with_value(phase)
120 }
121
122 pub fn duplicate(what: &str) -> Self {
123 Self::new(WarningCode::Duplicate, format!("{} already exists", what))
124 }
125
126 pub fn deprecated(feature: &str, alternative: &str) -> Self {
127 Self::new(
128 WarningCode::Deprecated,
129 format!("'{}' is deprecated, use '{}' instead", feature, alternative),
130 )
131 }
132}
133
134impl fmt::Display for ToolWarning {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 write!(f, "{}", self.message)
137 }
138}
139
140#[derive(Debug, Serialize)]
142pub struct ToolError {
143 pub code: ErrorCode,
144 pub message: String,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub field: Option<String>,
147 #[serde(skip_serializing_if = "Option::is_none")]
148 pub details: Option<String>,
149}
150
151impl ToolError {
152 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
153 Self {
154 code,
155 message: message.into(),
156 field: None,
157 details: None,
158 }
159 }
160
161 pub fn with_field(mut self, field: impl Into<String>) -> Self {
162 self.field = Some(field.into());
163 self
164 }
165
166 pub fn with_details(mut self, details: impl Into<String>) -> Self {
167 self.details = Some(details.into());
168 self
169 }
170
171 pub fn missing_field(field: &str) -> Self {
174 Self::new(
175 ErrorCode::MissingRequiredField,
176 format!("{} is required", field),
177 )
178 .with_field(field)
179 }
180
181 pub fn invalid_value(field: &str, reason: &str) -> Self {
182 Self::new(ErrorCode::InvalidFieldValue, reason).with_field(field)
183 }
184
185 pub fn agent_not_found(agent_id: &str) -> Self {
186 Self::new(
187 ErrorCode::AgentNotFound,
188 format!("Agent not found: {}", agent_id),
189 )
190 }
191
192 pub fn task_not_found(task_id: &str) -> Self {
193 Self::new(
194 ErrorCode::TaskNotFound,
195 format!("Task not found: {}", task_id),
196 )
197 }
198
199 pub fn already_claimed(task_id: &str, owner: &str) -> Self {
200 Self::new(
201 ErrorCode::AlreadyClaimed,
202 format!("Task {} already claimed by {}", task_id, owner),
203 )
204 }
205
206 pub fn not_owner(task_id: &str, agent_id: &str) -> Self {
207 Self::new(
208 ErrorCode::NotOwner,
209 format!("Agent {} does not own task {}", agent_id, task_id),
210 )
211 }
212
213 pub fn dependency_cycle(blocker: &str, blocked: &str) -> Self {
214 Self::new(
215 ErrorCode::DependencyCycle,
216 format!(
217 "Adding dependency {} -> {} would create a cycle",
218 blocker, blocked
219 ),
220 )
221 }
222
223 pub fn tag_mismatch(missing: &str) -> Self {
224 Self::new(
225 ErrorCode::TagMismatch,
226 format!("Agent missing required tag(s): {}", missing),
227 )
228 }
229
230 pub fn deps_not_satisfied(blockers: &[String]) -> Self {
231 Self::new(
232 ErrorCode::DependencyNotSatisfied,
233 format!("Task blocked by: {}", blockers.join(", ")),
234 )
235 }
236
237 pub fn gates_not_satisfied(status: &str, gates: &[String]) -> Self {
238 Self::new(
239 ErrorCode::GatesNotSatisfied,
240 format!(
241 "Cannot exit '{}': unsatisfied gates: {}",
242 status,
243 gates.join(", ")
244 ),
245 )
246 }
247
248 pub fn invalid_path(path: &str, reason: &str) -> Self {
249 Self::new(
250 ErrorCode::InvalidPath,
251 format!("Invalid path '{}': {}", path, reason),
252 )
253 }
254
255 pub fn prefix_not_lowercase(prefix: &str) -> Self {
256 Self::new(
257 ErrorCode::InvalidPrefix,
258 format!("Path prefix '{}' must be lowercase", prefix),
259 )
260 }
261
262 pub fn unknown_prefix(prefix: &str) -> Self {
263 Self::new(
264 ErrorCode::InvalidPrefix,
265 format!("Unknown path prefix: {}", prefix),
266 )
267 }
268
269 pub fn sandbox_escape(path: &str, root: &str) -> Self {
270 Self::new(
271 ErrorCode::InvalidPath,
272 format!("Path '{}' escapes sandbox root '{}'", path, root),
273 )
274 }
275
276 pub fn database(err: impl fmt::Display) -> Self {
277 Self::new(ErrorCode::DatabaseError, err.to_string())
278 }
279
280 pub fn internal(err: impl fmt::Display) -> Self {
281 Self::new(ErrorCode::InternalError, err.to_string())
282 }
283
284 pub fn unknown_tool(name: &str) -> Self {
285 Self::new(ErrorCode::UnknownTool, format!("Unknown tool: {}", name))
286 }
287}
288
289impl fmt::Display for ToolError {
290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291 write!(f, "{}", self.message)
292 }
293}
294
295impl std::error::Error for ToolError {}
296
297impl From<anyhow::Error> for ToolError {
299 fn from(err: anyhow::Error) -> Self {
300 match err.downcast::<ToolError>() {
302 Ok(tool_err) => tool_err,
303 Err(err) => ToolError::internal(err),
304 }
305 }
306}
307
308pub type ToolResult<T> = std::result::Result<T, ToolError>;