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 #[serde(skip_serializing_if = "Option::is_none")]
150 pub blocked_by: Option<Vec<String>>,
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub suggestion: Option<String>,
153}
154
155impl ToolError {
156 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
157 Self {
158 code,
159 message: message.into(),
160 field: None,
161 details: None,
162 blocked_by: None,
163 suggestion: None,
164 }
165 }
166
167 pub fn with_field(mut self, field: impl Into<String>) -> Self {
168 self.field = Some(field.into());
169 self
170 }
171
172 pub fn with_details(mut self, details: impl Into<String>) -> Self {
173 self.details = Some(details.into());
174 self
175 }
176
177 pub fn with_blocked_by(mut self, blocked_by: Vec<String>) -> Self {
178 self.blocked_by = Some(blocked_by);
179 self
180 }
181
182 pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
183 self.suggestion = Some(suggestion.into());
184 self
185 }
186
187 pub fn missing_field(field: &str) -> Self {
190 Self::new(
191 ErrorCode::MissingRequiredField,
192 format!("{} is required", field),
193 )
194 .with_field(field)
195 }
196
197 pub fn invalid_value(field: &str, reason: &str) -> Self {
198 Self::new(ErrorCode::InvalidFieldValue, reason).with_field(field)
199 }
200
201 pub fn agent_not_found(agent_id: &str) -> Self {
202 Self::new(
203 ErrorCode::AgentNotFound,
204 format!("Agent not found: {}", agent_id),
205 )
206 }
207
208 pub fn task_not_found(task_id: &str) -> Self {
209 Self::new(
210 ErrorCode::TaskNotFound,
211 format!("Task not found: {}", task_id),
212 )
213 }
214
215 pub fn already_claimed(task_id: &str, owner: &str) -> Self {
216 Self::new(
217 ErrorCode::AlreadyClaimed,
218 format!("Task {} already claimed by {}", task_id, owner),
219 )
220 }
221
222 pub fn not_owner(task_id: &str, agent_id: &str) -> Self {
223 Self::new(
224 ErrorCode::NotOwner,
225 format!("Agent {} does not own task {}", agent_id, task_id),
226 )
227 }
228
229 pub fn dependency_cycle(blocker: &str, blocked: &str) -> Self {
230 Self::new(
231 ErrorCode::DependencyCycle,
232 format!(
233 "Adding dependency {} -> {} would create a cycle",
234 blocker, blocked
235 ),
236 )
237 }
238
239 pub fn tag_mismatch(missing: &str) -> Self {
240 Self::new(
241 ErrorCode::TagMismatch,
242 format!("Agent missing required tag(s): {}", missing),
243 )
244 }
245
246 pub fn deps_not_satisfied(blockers: &[String]) -> Self {
247 Self::new(
248 ErrorCode::DependencyNotSatisfied,
249 format!(
250 "Task blocked by unsatisfied dependencies: {}",
251 blockers.join(", ")
252 ),
253 )
254 .with_blocked_by(blockers.to_vec())
255 .with_suggestion(
256 "Wait for blocking tasks to complete, or call thinking() regularly to maintain heartbeat"
257 .to_string(),
258 )
259 }
260
261 pub fn gates_not_satisfied(status: &str, gates: &[String]) -> Self {
262 Self::new(
263 ErrorCode::GatesNotSatisfied,
264 format!(
265 "Cannot exit '{}': unsatisfied gates: {}",
266 status,
267 gates.join(", ")
268 ),
269 )
270 }
271
272 pub fn invalid_path(path: &str, reason: &str) -> Self {
273 Self::new(
274 ErrorCode::InvalidPath,
275 format!("Invalid path '{}': {}", path, reason),
276 )
277 }
278
279 pub fn prefix_not_lowercase(prefix: &str) -> Self {
280 Self::new(
281 ErrorCode::InvalidPrefix,
282 format!("Path prefix '{}' must be lowercase", prefix),
283 )
284 }
285
286 pub fn unknown_prefix(prefix: &str) -> Self {
287 Self::new(
288 ErrorCode::InvalidPrefix,
289 format!("Unknown path prefix: {}", prefix),
290 )
291 }
292
293 pub fn sandbox_escape(path: &str, root: &str) -> Self {
294 Self::new(
295 ErrorCode::InvalidPath,
296 format!("Path '{}' escapes sandbox root '{}'", path, root),
297 )
298 }
299
300 pub fn database(err: impl fmt::Display) -> Self {
301 Self::new(ErrorCode::DatabaseError, err.to_string())
302 }
303
304 pub fn internal(err: impl fmt::Display) -> Self {
305 Self::new(ErrorCode::InternalError, err.to_string())
306 }
307
308 pub fn unknown_tool(name: &str) -> Self {
309 Self::new(ErrorCode::UnknownTool, format!("Unknown tool: {}", name))
310 }
311}
312
313impl fmt::Display for ToolError {
314 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315 write!(f, "{}", self.message)
316 }
317}
318
319impl std::error::Error for ToolError {}
320
321impl From<anyhow::Error> for ToolError {
323 fn from(err: anyhow::Error) -> Self {
324 match err.downcast::<ToolError>() {
326 Ok(tool_err) => tool_err,
327 Err(err) => ToolError::internal(err),
328 }
329 }
330}
331
332pub type ToolResult<T> = std::result::Result<T, ToolError>;