use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum DelegationFailure {
DelegateUnreachable { url: String, message: String },
CapabilityMismatch {
requested: String,
available: Vec<String>,
},
PolicyDenied { policy_id: String, reason: String },
ApprovalRequired { prompt: String },
BudgetExceeded {
limit: f64,
actual: f64,
unit: String,
},
TrustCrossingDenied {
from_domain: String,
to_domain: String,
},
VerificationFailed { check: String, message: String },
ToolDependencyFailed { tool: String, error: String },
PartialCompletion {
completed_steps: usize,
total_steps: usize,
output: Option<Value>,
},
FallbackInvoked {
original_delegate: String,
fallback_delegate: String,
reason: String,
},
Timeout {
deadline_secs: u64,
elapsed_secs: u64,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FailureSeverity {
Warning,
Error,
Fatal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DelegationFailureInfo {
pub failure: DelegationFailure,
pub severity: FailureSeverity,
pub retryable: bool,
pub partial_output: Option<Value>,
pub recommended_fallback: Option<String>,
pub audit_ref: Option<String>,
pub timestamp: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_delegate_unreachable() {
let f = DelegationFailure::DelegateUnreachable {
url: "https://agent.example.com".into(),
message: "connection refused".into(),
};
let json = serde_json::to_string(&f).unwrap();
assert!(json.contains(r#""type":"delegate_unreachable""#));
let back: DelegationFailure = serde_json::from_str(&json).unwrap();
assert_eq!(f, back);
}
#[test]
fn round_trip_failure_info() {
let info = DelegationFailureInfo {
failure: DelegationFailure::Timeout {
deadline_secs: 30,
elapsed_secs: 35,
},
severity: FailureSeverity::Error,
retryable: true,
partial_output: Some(serde_json::json!({"partial": true})),
recommended_fallback: Some("backup-agent".into()),
audit_ref: Some("audit-12345".into()),
timestamp: "2026-03-15T00:00:00Z".into(),
};
let json = serde_json::to_string(&info).unwrap();
let back: DelegationFailureInfo = serde_json::from_str(&json).unwrap();
assert_eq!(back.severity, FailureSeverity::Error);
assert!(back.retryable);
}
#[test]
fn severity_round_trip() {
for sev in [
FailureSeverity::Warning,
FailureSeverity::Error,
FailureSeverity::Fatal,
] {
let json = serde_json::to_string(&sev).unwrap();
let back: FailureSeverity = serde_json::from_str(&json).unwrap();
assert_eq!(sev, back);
}
}
#[test]
fn all_variants_serialize() {
let variants: Vec<DelegationFailure> = vec![
DelegationFailure::DelegateUnreachable {
url: "u".into(),
message: "m".into(),
},
DelegationFailure::CapabilityMismatch {
requested: "r".into(),
available: vec!["a".into()],
},
DelegationFailure::PolicyDenied {
policy_id: "p".into(),
reason: "r".into(),
},
DelegationFailure::ApprovalRequired {
prompt: "ok?".into(),
},
DelegationFailure::BudgetExceeded {
limit: 10.0,
actual: 15.0,
unit: "usd".into(),
},
DelegationFailure::TrustCrossingDenied {
from_domain: "a.com".into(),
to_domain: "b.com".into(),
},
DelegationFailure::VerificationFailed {
check: "sig".into(),
message: "bad".into(),
},
DelegationFailure::ToolDependencyFailed {
tool: "t".into(),
error: "e".into(),
},
DelegationFailure::PartialCompletion {
completed_steps: 3,
total_steps: 5,
output: None,
},
DelegationFailure::FallbackInvoked {
original_delegate: "a".into(),
fallback_delegate: "b".into(),
reason: "r".into(),
},
DelegationFailure::Timeout {
deadline_secs: 10,
elapsed_secs: 12,
},
];
for v in &variants {
let json = serde_json::to_string(v).unwrap();
assert!(json.contains("\"type\""));
let back: DelegationFailure = serde_json::from_str(&json).unwrap();
assert_eq!(*v, back);
}
}
}