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 LockConflict,
27 DependencyCycle,
28 TagMismatch,
29 NotOwner,
30 DependencyNotSatisfied,
31 GatesNotSatisfied,
32
33 DatabaseError,
35 InternalError,
36 UnknownTool,
37}
38
39#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
41#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
42pub enum WarningCode {
43 TaskNotFound,
45 DependencyNotFound,
47 UnknownTag,
49 UnknownPhase,
51 Duplicate,
53 Deprecated,
55}
56
57#[derive(Debug, Clone, Serialize)]
59pub struct ToolWarning {
60 pub code: WarningCode,
61 pub message: String,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub field: Option<String>,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub value: Option<String>,
66}
67
68impl ToolWarning {
69 pub fn new(code: WarningCode, message: impl Into<String>) -> Self {
70 Self {
71 code,
72 message: message.into(),
73 field: None,
74 value: None,
75 }
76 }
77
78 pub fn with_field(mut self, field: impl Into<String>) -> Self {
79 self.field = Some(field.into());
80 self
81 }
82
83 pub fn with_value(mut self, value: impl Into<String>) -> Self {
84 self.value = Some(value.into());
85 self
86 }
87
88 pub fn task_not_found(task_id: &str) -> Self {
91 Self::new(
92 WarningCode::TaskNotFound,
93 format!("Task '{}' not found, skipped", task_id),
94 )
95 .with_value(task_id)
96 }
97
98 pub fn dependency_not_found(task_id: &str, field: &str) -> Self {
99 Self::new(
100 WarningCode::DependencyNotFound,
101 format!("Dependency target '{}' not found, link skipped", task_id),
102 )
103 .with_field(field)
104 .with_value(task_id)
105 }
106
107 pub fn unknown_tag(tag: &str) -> Self {
108 Self::new(
109 WarningCode::UnknownTag,
110 format!("Tag '{}' is not in known tags list", tag),
111 )
112 .with_value(tag)
113 }
114
115 pub fn unknown_phase(phase: &str) -> Self {
116 Self::new(
117 WarningCode::UnknownPhase,
118 format!("Phase '{}' is not in known phases list", phase),
119 )
120 .with_value(phase)
121 }
122
123 pub fn duplicate(what: &str) -> Self {
124 Self::new(WarningCode::Duplicate, format!("{} already exists", what))
125 }
126
127 pub fn deprecated(feature: &str, alternative: &str) -> Self {
128 Self::new(
129 WarningCode::Deprecated,
130 format!("'{}' is deprecated, use '{}' instead", feature, alternative),
131 )
132 }
133}
134
135impl fmt::Display for ToolWarning {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 write!(f, "{}", self.message)
138 }
139}
140
141#[derive(Debug, Serialize)]
143pub struct ToolError {
144 pub code: ErrorCode,
145 pub message: String,
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub field: Option<String>,
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub details: Option<String>,
150 #[serde(skip_serializing_if = "Option::is_none")]
151 pub blocked_by: Option<Vec<String>>,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub suggestion: Option<String>,
154}
155
156impl ToolError {
157 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
158 Self {
159 code,
160 message: message.into(),
161 field: None,
162 details: None,
163 blocked_by: None,
164 suggestion: None,
165 }
166 }
167
168 pub fn with_field(mut self, field: impl Into<String>) -> Self {
169 self.field = Some(field.into());
170 self
171 }
172
173 pub fn with_details(mut self, details: impl Into<String>) -> Self {
174 self.details = Some(details.into());
175 self
176 }
177
178 pub fn with_blocked_by(mut self, blocked_by: Vec<String>) -> Self {
179 self.blocked_by = Some(blocked_by);
180 self
181 }
182
183 pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
184 self.suggestion = Some(suggestion.into());
185 self
186 }
187
188 pub fn missing_field(field: &str) -> Self {
191 Self::new(
192 ErrorCode::MissingRequiredField,
193 format!("{} is required", field),
194 )
195 .with_field(field)
196 }
197
198 pub fn invalid_value(field: &str, reason: &str) -> Self {
199 Self::new(ErrorCode::InvalidFieldValue, reason).with_field(field)
200 }
201
202 pub fn agent_not_found(agent_id: &str) -> Self {
203 Self::new(
204 ErrorCode::AgentNotFound,
205 format!("Agent not found: {}", agent_id),
206 )
207 }
208
209 pub fn task_not_found(task_id: &str) -> Self {
210 Self::new(
211 ErrorCode::TaskNotFound,
212 format!("Task not found: {}", task_id),
213 )
214 }
215
216 pub fn lock_conflict(resource: &str, held_by: &str) -> Self {
217 Self::new(
218 ErrorCode::LockConflict,
219 format!(
220 "Lock '{}' is exclusively held by agent '{}'",
221 resource, held_by
222 ),
223 )
224 .with_field("file")
225 .with_details(format!("held_by: {}", held_by))
226 .with_suggestion(
227 "Wait for the lock to be released, or coordinate with the holding agent".to_string(),
228 )
229 }
230
231 pub fn already_claimed(task_id: &str, owner: &str) -> Self {
232 Self::new(
233 ErrorCode::AlreadyClaimed,
234 format!("Task {} already claimed by {}", task_id, owner),
235 )
236 }
237
238 pub fn not_owner(task_id: &str, agent_id: &str) -> Self {
239 Self::new(
240 ErrorCode::NotOwner,
241 format!("Agent {} does not own task {}", agent_id, task_id),
242 )
243 }
244
245 pub fn dependency_cycle(blocker: &str, blocked: &str) -> Self {
246 Self::new(
247 ErrorCode::DependencyCycle,
248 format!(
249 "Adding dependency {} -> {} would create a cycle",
250 blocker, blocked
251 ),
252 )
253 }
254
255 pub fn tag_mismatch(missing: &str) -> Self {
256 Self::new(
257 ErrorCode::TagMismatch,
258 format!("Agent missing required tag(s): {}", missing),
259 )
260 }
261
262 pub fn deps_not_satisfied(blockers: &[String]) -> Self {
263 Self::new(
264 ErrorCode::DependencyNotSatisfied,
265 format!(
266 "Task blocked by unsatisfied dependencies: {}",
267 blockers.join(", ")
268 ),
269 )
270 .with_blocked_by(blockers.to_vec())
271 .with_suggestion(
272 "Wait for blocking tasks to complete. Meanwhile: (1) call list_tasks(ready=true) to find unblocked work, (2) use scan(task=<id>, direction=\"before\") to inspect the dependency chain, (3) call thinking() regularly to maintain heartbeat while waiting."
273 .to_string(),
274 )
275 }
276
277 pub fn gates_not_satisfied(status: &str, gates: &[String]) -> Self {
278 let gate_list = gates.join(", ");
279 let how_to_fix: Vec<String> = gates
280 .iter()
281 .map(|g| {
282 let gate_type = g.split(" (").next().unwrap_or(g);
284 format!(
285 " - Satisfy '{}': attach(task=<id>, type=\"{}\", content=\"...\")",
286 gate_type, gate_type
287 )
288 })
289 .collect();
290 Self::new(
291 ErrorCode::GatesNotSatisfied,
292 format!(
293 "Cannot exit '{}': unsatisfied gates: {}",
294 status, gate_list
295 ),
296 )
297 .with_details(format!(
298 "How to satisfy:\n{}\n\nOr use force=true with a reason to skip warn-level gates.",
299 how_to_fix.join("\n")
300 ))
301 .with_suggestion(
302 "Attach the required artifacts, then retry the transition. For warn-level gates, you can use update(..., force=true, reason=\"...\") to proceed.".to_string(),
303 )
304 }
305
306 pub fn invalid_path(path: &str, reason: &str) -> Self {
307 Self::new(
308 ErrorCode::InvalidPath,
309 format!("Invalid path '{}': {}", path, reason),
310 )
311 }
312
313 pub fn prefix_not_lowercase(prefix: &str) -> Self {
314 Self::new(
315 ErrorCode::InvalidPrefix,
316 format!("Path prefix '{}' must be lowercase", prefix),
317 )
318 }
319
320 pub fn unknown_prefix(prefix: &str) -> Self {
321 Self::new(
322 ErrorCode::InvalidPrefix,
323 format!("Unknown path prefix: {}", prefix),
324 )
325 }
326
327 pub fn sandbox_escape(path: &str, root: &str) -> Self {
328 Self::new(
329 ErrorCode::InvalidPath,
330 format!("Path '{}' escapes sandbox root '{}'", path, root),
331 )
332 }
333
334 pub fn database(err: impl fmt::Display) -> Self {
335 Self::new(ErrorCode::DatabaseError, err.to_string())
336 }
337
338 pub fn internal(err: impl fmt::Display) -> Self {
339 Self::new(ErrorCode::InternalError, err.to_string())
340 }
341
342 pub fn unknown_tool(name: &str) -> Self {
343 Self::new(ErrorCode::UnknownTool, format!("Unknown tool: {}", name))
344 }
345}
346
347impl fmt::Display for ToolError {
348 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
349 write!(f, "{}", self.message)
350 }
351}
352
353impl std::error::Error for ToolError {}
354
355impl From<anyhow::Error> for ToolError {
357 fn from(err: anyhow::Error) -> Self {
358 match err.downcast::<ToolError>() {
360 Ok(tool_err) => tool_err,
361 Err(err) => ToolError::internal(err),
362 }
363 }
364}
365
366pub type ToolResult<T> = std::result::Result<T, ToolError>;