Skip to main content

task_graph_mcp/
error.rs

1//! Structured error types for tool responses.
2
3use serde::Serialize;
4use std::fmt;
5
6/// Error codes for programmatic error handling.
7#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
8#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
9pub enum ErrorCode {
10    // Validation errors (4xx-like)
11    MissingRequiredField,
12    InvalidFieldValue,
13    InvalidState,
14
15    // Not found errors
16    AgentNotFound,
17    TaskNotFound,
18    FileNotFound,
19    AttachmentNotFound,
20
21    // Conflict errors
22    AlreadyClaimed,
23    AlreadyExists,
24    DependencyCycle,
25    TagMismatch,
26    NotOwner,
27    DependencyNotSatisfied,
28
29    // Internal errors
30    DatabaseError,
31    InternalError,
32    UnknownTool,
33}
34
35/// Structured error for tool responses.
36#[derive(Debug, Serialize)]
37pub struct ToolError {
38    pub code: ErrorCode,
39    pub message: String,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub field: Option<String>,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub details: Option<String>,
44}
45
46impl ToolError {
47    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
48        Self {
49            code,
50            message: message.into(),
51            field: None,
52            details: None,
53        }
54    }
55
56    pub fn with_field(mut self, field: impl Into<String>) -> Self {
57        self.field = Some(field.into());
58        self
59    }
60
61    pub fn with_details(mut self, details: impl Into<String>) -> Self {
62        self.details = Some(details.into());
63        self
64    }
65
66    // Convenience constructors
67
68    pub fn missing_field(field: &str) -> Self {
69        Self::new(
70            ErrorCode::MissingRequiredField,
71            format!("{} is required", field),
72        )
73        .with_field(field)
74    }
75
76    pub fn invalid_value(field: &str, reason: &str) -> Self {
77        Self::new(ErrorCode::InvalidFieldValue, reason).with_field(field)
78    }
79
80    pub fn agent_not_found(agent_id: &str) -> Self {
81        Self::new(
82            ErrorCode::AgentNotFound,
83            format!("Agent not found: {}", agent_id),
84        )
85    }
86
87    pub fn task_not_found(task_id: &str) -> Self {
88        Self::new(
89            ErrorCode::TaskNotFound,
90            format!("Task not found: {}", task_id),
91        )
92    }
93
94    pub fn already_claimed(task_id: &str, owner: &str) -> Self {
95        Self::new(
96            ErrorCode::AlreadyClaimed,
97            format!("Task {} already claimed by {}", task_id, owner),
98        )
99    }
100
101    pub fn not_owner(task_id: &str, agent_id: &str) -> Self {
102        Self::new(
103            ErrorCode::NotOwner,
104            format!("Agent {} does not own task {}", agent_id, task_id),
105        )
106    }
107
108    pub fn dependency_cycle(blocker: &str, blocked: &str) -> Self {
109        Self::new(
110            ErrorCode::DependencyCycle,
111            format!(
112                "Adding dependency {} -> {} would create a cycle",
113                blocker, blocked
114            ),
115        )
116    }
117
118    pub fn tag_mismatch(missing: &str) -> Self {
119        Self::new(
120            ErrorCode::TagMismatch,
121            format!("Agent missing required tag(s): {}", missing),
122        )
123    }
124
125    pub fn deps_not_satisfied(blockers: &[String]) -> Self {
126        Self::new(
127            ErrorCode::DependencyNotSatisfied,
128            format!("Task blocked by: {}", blockers.join(", ")),
129        )
130    }
131
132    pub fn database(err: impl fmt::Display) -> Self {
133        Self::new(ErrorCode::DatabaseError, err.to_string())
134    }
135
136    pub fn internal(err: impl fmt::Display) -> Self {
137        Self::new(ErrorCode::InternalError, err.to_string())
138    }
139
140    pub fn unknown_tool(name: &str) -> Self {
141        Self::new(ErrorCode::UnknownTool, format!("Unknown tool: {}", name))
142    }
143}
144
145impl fmt::Display for ToolError {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        write!(f, "{}", self.message)
148    }
149}
150
151impl std::error::Error for ToolError {}
152
153// Allow using ? with anyhow errors by converting them
154impl From<anyhow::Error> for ToolError {
155    fn from(err: anyhow::Error) -> Self {
156        // Try to downcast to ToolError first
157        match err.downcast::<ToolError>() {
158            Ok(tool_err) => tool_err,
159            Err(err) => ToolError::internal(err),
160        }
161    }
162}
163
164
165/// Result type for tool operations.
166pub type ToolResult<T> = std::result::Result<T, ToolError>;