use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum DelegationError {
LlmCallFailed {
provider: String,
model: String,
reason: String,
},
SubagentUnavailable { name: String, state: String },
PolicyDenied { rule: String, reason: String },
Timeout { duration_ms: u64 },
}
impl DelegationError {
pub fn to_detail_json(&self) -> String {
serde_json::to_string(self)
.unwrap_or_else(|_| format!("{{\"type\":\"Unknown\",\"message\":\"{self}\"}}"))
}
pub fn error_type(&self) -> &'static str {
match self {
Self::LlmCallFailed { .. } => "LlmCallFailed",
Self::SubagentUnavailable { .. } => "SubagentUnavailable",
Self::PolicyDenied { .. } => "PolicyDenied",
Self::Timeout { .. } => "Timeout",
}
}
}
impl std::fmt::Display for DelegationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::LlmCallFailed {
provider,
model,
reason,
} => {
write!(f, "LLM call to {provider}/{model} failed: {reason}")
}
Self::SubagentUnavailable { name, state } => {
write!(f, "Subagent '{name}' unavailable (state: {state})")
}
Self::PolicyDenied { rule, reason } => {
write!(f, "Policy denied by rule '{rule}': {reason}")
}
Self::Timeout { duration_ms } => {
write!(f, "Delegation timed out after {duration_ms}ms")
}
}
}
}
impl std::error::Error for DelegationError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_variants_serialize_to_tagged_json() {
let err = DelegationError::LlmCallFailed {
provider: "anthropic".into(),
model: "claude-3-opus".into(),
reason: "rate limited".into(),
};
let json = serde_json::to_string(&err).unwrap();
assert!(json.contains("\"type\":\"LlmCallFailed\""));
assert!(json.contains("\"provider\":\"anthropic\""));
}
#[test]
fn subagent_unavailable_serializes() {
let err = DelegationError::SubagentUnavailable {
name: "code-analyst".into(),
state: "stopped".into(),
};
let json = serde_json::to_string(&err).unwrap();
assert!(json.contains("\"type\":\"SubagentUnavailable\""));
assert!(json.contains("\"name\":\"code-analyst\""));
}
#[test]
fn policy_denied_serializes() {
let err = DelegationError::PolicyDenied {
rule: "max_concurrent_delegations".into(),
reason: "limit of 4 exceeded".into(),
};
let json = serde_json::to_string(&err).unwrap();
assert!(json.contains("\"type\":\"PolicyDenied\""));
}
#[test]
fn timeout_serializes() {
let err = DelegationError::Timeout { duration_ms: 30000 };
let json = serde_json::to_string(&err).unwrap();
assert!(json.contains("\"type\":\"Timeout\""));
assert!(json.contains("30000"));
}
#[test]
fn display_is_human_readable() {
let err = DelegationError::LlmCallFailed {
provider: "anthropic".into(),
model: "claude-3-opus".into(),
reason: "rate limited".into(),
};
let display = err.to_string();
assert!(display.contains("anthropic"));
assert!(display.contains("rate limited"));
}
#[test]
fn round_trip_through_json() {
let errors = vec![
DelegationError::LlmCallFailed {
provider: "openai".into(),
model: "gpt-4".into(),
reason: "context length exceeded".into(),
},
DelegationError::SubagentUnavailable {
name: "writer".into(),
state: "booting".into(),
},
DelegationError::PolicyDenied {
rule: "restricted_tool".into(),
reason: "tool not allowed".into(),
},
DelegationError::Timeout { duration_ms: 60000 },
];
for err in &errors {
let json = serde_json::to_string(err).unwrap();
let back: DelegationError = serde_json::from_str(&json).unwrap();
assert_eq!(err.to_string(), back.to_string());
}
}
#[test]
fn to_detail_json_produces_valid_json() {
let err = DelegationError::Timeout { duration_ms: 5000 };
let json_str = err.to_detail_json();
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
assert_eq!(parsed["type"], "Timeout");
assert_eq!(parsed["duration_ms"], 5000);
}
}