use serde::Serialize;
use std::fmt;
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ErrorCode {
MissingRequiredField,
InvalidFieldValue,
InvalidState,
InvalidPath,
InvalidPrefix,
AgentNotFound,
TaskNotFound,
FileNotFound,
AttachmentNotFound,
AlreadyClaimed,
AlreadyExists,
LockConflict,
DependencyCycle,
TagMismatch,
NotOwner,
DependencyNotSatisfied,
GatesNotSatisfied,
DatabaseError,
InternalError,
UnknownTool,
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum WarningCode {
TaskNotFound,
DependencyNotFound,
UnknownTag,
UnknownPhase,
Duplicate,
Deprecated,
}
#[derive(Debug, Clone, Serialize)]
pub struct ToolWarning {
pub code: WarningCode,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub field: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<String>,
}
impl ToolWarning {
pub fn new(code: WarningCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
field: None,
value: None,
}
}
pub fn with_field(mut self, field: impl Into<String>) -> Self {
self.field = Some(field.into());
self
}
pub fn with_value(mut self, value: impl Into<String>) -> Self {
self.value = Some(value.into());
self
}
pub fn task_not_found(task_id: &str) -> Self {
Self::new(
WarningCode::TaskNotFound,
format!("Task '{}' not found, skipped", task_id),
)
.with_value(task_id)
}
pub fn dependency_not_found(task_id: &str, field: &str) -> Self {
Self::new(
WarningCode::DependencyNotFound,
format!("Dependency target '{}' not found, link skipped", task_id),
)
.with_field(field)
.with_value(task_id)
}
pub fn unknown_tag(tag: &str) -> Self {
Self::new(
WarningCode::UnknownTag,
format!("Tag '{}' is not in known tags list", tag),
)
.with_value(tag)
}
pub fn unknown_phase(phase: &str) -> Self {
Self::new(
WarningCode::UnknownPhase,
format!("Phase '{}' is not in known phases list", phase),
)
.with_value(phase)
}
pub fn duplicate(what: &str) -> Self {
Self::new(WarningCode::Duplicate, format!("{} already exists", what))
}
pub fn deprecated(feature: &str, alternative: &str) -> Self {
Self::new(
WarningCode::Deprecated,
format!("'{}' is deprecated, use '{}' instead", feature, alternative),
)
}
}
impl fmt::Display for ToolWarning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
#[derive(Debug, Serialize)]
pub struct ToolError {
pub code: ErrorCode,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub field: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub blocked_by: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub suggestion: Option<String>,
}
impl ToolError {
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
field: None,
details: None,
blocked_by: None,
suggestion: None,
}
}
pub fn with_field(mut self, field: impl Into<String>) -> Self {
self.field = Some(field.into());
self
}
pub fn with_details(mut self, details: impl Into<String>) -> Self {
self.details = Some(details.into());
self
}
pub fn with_blocked_by(mut self, blocked_by: Vec<String>) -> Self {
self.blocked_by = Some(blocked_by);
self
}
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
self.suggestion = Some(suggestion.into());
self
}
pub fn missing_field(field: &str) -> Self {
Self::new(
ErrorCode::MissingRequiredField,
format!("{} is required", field),
)
.with_field(field)
}
pub fn invalid_value(field: &str, reason: &str) -> Self {
Self::new(ErrorCode::InvalidFieldValue, reason).with_field(field)
}
pub fn agent_not_found(agent_id: &str) -> Self {
Self::new(
ErrorCode::AgentNotFound,
format!("Agent not found: {}", agent_id),
)
}
pub fn task_not_found(task_id: &str) -> Self {
Self::new(
ErrorCode::TaskNotFound,
format!("Task not found: {}", task_id),
)
}
pub fn lock_conflict(resource: &str, held_by: &str) -> Self {
Self::new(
ErrorCode::LockConflict,
format!(
"Lock '{}' is exclusively held by agent '{}'",
resource, held_by
),
)
.with_field("file")
.with_details(format!("held_by: {}", held_by))
.with_suggestion(
"Wait for the lock to be released, or coordinate with the holding agent".to_string(),
)
}
pub fn already_claimed(task_id: &str, owner: &str) -> Self {
Self::new(
ErrorCode::AlreadyClaimed,
format!("Task {} already claimed by {}", task_id, owner),
)
}
pub fn not_owner(task_id: &str, agent_id: &str) -> Self {
Self::new(
ErrorCode::NotOwner,
format!("Agent {} does not own task {}", agent_id, task_id),
)
}
pub fn dependency_cycle(blocker: &str, blocked: &str) -> Self {
Self::new(
ErrorCode::DependencyCycle,
format!(
"Adding dependency {} -> {} would create a cycle",
blocker, blocked
),
)
}
pub fn tag_mismatch(missing: &str) -> Self {
Self::new(
ErrorCode::TagMismatch,
format!("Agent missing required tag(s): {}", missing),
)
}
pub fn deps_not_satisfied(blockers: &[String]) -> Self {
Self::new(
ErrorCode::DependencyNotSatisfied,
format!(
"Task blocked by unsatisfied dependencies: {}",
blockers.join(", ")
),
)
.with_blocked_by(blockers.to_vec())
.with_suggestion(
"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."
.to_string(),
)
}
pub fn gates_not_satisfied(status: &str, gates: &[String]) -> Self {
let gate_list = gates.join(", ");
let how_to_fix: Vec<String> = gates
.iter()
.map(|g| {
let gate_type = g.split(" (").next().unwrap_or(g);
format!(
" - Satisfy '{}': attach(task=<id>, type=\"{}\", content=\"...\")",
gate_type, gate_type
)
})
.collect();
Self::new(
ErrorCode::GatesNotSatisfied,
format!(
"Cannot exit '{}': unsatisfied gates: {}",
status, gate_list
),
)
.with_details(format!(
"How to satisfy:\n{}\n\nOr use force=true with a reason to skip warn-level gates.",
how_to_fix.join("\n")
))
.with_suggestion(
"Attach the required artifacts, then retry the transition. For warn-level gates, you can use update(..., force=true, reason=\"...\") to proceed.".to_string(),
)
}
pub fn invalid_path(path: &str, reason: &str) -> Self {
Self::new(
ErrorCode::InvalidPath,
format!("Invalid path '{}': {}", path, reason),
)
}
pub fn prefix_not_lowercase(prefix: &str) -> Self {
Self::new(
ErrorCode::InvalidPrefix,
format!("Path prefix '{}' must be lowercase", prefix),
)
}
pub fn unknown_prefix(prefix: &str) -> Self {
Self::new(
ErrorCode::InvalidPrefix,
format!("Unknown path prefix: {}", prefix),
)
}
pub fn sandbox_escape(path: &str, root: &str) -> Self {
Self::new(
ErrorCode::InvalidPath,
format!("Path '{}' escapes sandbox root '{}'", path, root),
)
}
pub fn database(err: impl fmt::Display) -> Self {
Self::new(ErrorCode::DatabaseError, err.to_string())
}
pub fn internal(err: impl fmt::Display) -> Self {
Self::new(ErrorCode::InternalError, err.to_string())
}
pub fn unknown_tool(name: &str) -> Self {
Self::new(ErrorCode::UnknownTool, format!("Unknown tool: {}", name))
}
}
impl fmt::Display for ToolError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for ToolError {}
impl From<anyhow::Error> for ToolError {
fn from(err: anyhow::Error) -> Self {
match err.downcast::<ToolError>() {
Ok(tool_err) => tool_err,
Err(err) => ToolError::internal(err),
}
}
}
pub type ToolResult<T> = std::result::Result<T, ToolError>;