1use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15#[serde(tag = "type", rename_all = "snake_case")]
16pub enum DelegationFailure {
17 DelegateUnreachable { url: String, message: String },
19 CapabilityMismatch {
21 requested: String,
22 available: Vec<String>,
23 },
24 PolicyDenied { policy_id: String, reason: String },
26 ApprovalRequired { prompt: String },
28 BudgetExceeded {
30 limit: f64,
31 actual: f64,
32 unit: String,
33 },
34 TrustCrossingDenied {
36 from_domain: String,
37 to_domain: String,
38 },
39 VerificationFailed { check: String, message: String },
41 ToolDependencyFailed { tool: String, error: String },
43 PartialCompletion {
45 completed_steps: usize,
46 total_steps: usize,
47 output: Option<Value>,
48 },
49 FallbackInvoked {
51 original_delegate: String,
52 fallback_delegate: String,
53 reason: String,
54 },
55 Timeout {
57 deadline_secs: u64,
58 elapsed_secs: u64,
59 },
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
64#[serde(rename_all = "snake_case")]
65pub enum FailureSeverity {
66 Warning,
68 Error,
71 Fatal,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct DelegationFailureInfo {
78 pub failure: DelegationFailure,
80 pub severity: FailureSeverity,
82 pub retryable: bool,
84 pub partial_output: Option<Value>,
86 pub recommended_fallback: Option<String>,
88 pub audit_ref: Option<String>,
90 pub timestamp: String,
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn round_trip_delegate_unreachable() {
100 let f = DelegationFailure::DelegateUnreachable {
101 url: "https://agent.example.com".into(),
102 message: "connection refused".into(),
103 };
104 let json = serde_json::to_string(&f).unwrap();
105 assert!(json.contains(r#""type":"delegate_unreachable""#));
106 let back: DelegationFailure = serde_json::from_str(&json).unwrap();
107 assert_eq!(f, back);
108 }
109
110 #[test]
111 fn round_trip_failure_info() {
112 let info = DelegationFailureInfo {
113 failure: DelegationFailure::Timeout {
114 deadline_secs: 30,
115 elapsed_secs: 35,
116 },
117 severity: FailureSeverity::Error,
118 retryable: true,
119 partial_output: Some(serde_json::json!({"partial": true})),
120 recommended_fallback: Some("backup-agent".into()),
121 audit_ref: Some("audit-12345".into()),
122 timestamp: "2026-03-15T00:00:00Z".into(),
123 };
124 let json = serde_json::to_string(&info).unwrap();
125 let back: DelegationFailureInfo = serde_json::from_str(&json).unwrap();
126 assert_eq!(back.severity, FailureSeverity::Error);
127 assert!(back.retryable);
128 }
129
130 #[test]
131 fn severity_round_trip() {
132 for sev in [
133 FailureSeverity::Warning,
134 FailureSeverity::Error,
135 FailureSeverity::Fatal,
136 ] {
137 let json = serde_json::to_string(&sev).unwrap();
138 let back: FailureSeverity = serde_json::from_str(&json).unwrap();
139 assert_eq!(sev, back);
140 }
141 }
142
143 #[test]
144 fn all_variants_serialize() {
145 let variants: Vec<DelegationFailure> = vec![
146 DelegationFailure::DelegateUnreachable {
147 url: "u".into(),
148 message: "m".into(),
149 },
150 DelegationFailure::CapabilityMismatch {
151 requested: "r".into(),
152 available: vec!["a".into()],
153 },
154 DelegationFailure::PolicyDenied {
155 policy_id: "p".into(),
156 reason: "r".into(),
157 },
158 DelegationFailure::ApprovalRequired {
159 prompt: "ok?".into(),
160 },
161 DelegationFailure::BudgetExceeded {
162 limit: 10.0,
163 actual: 15.0,
164 unit: "usd".into(),
165 },
166 DelegationFailure::TrustCrossingDenied {
167 from_domain: "a.com".into(),
168 to_domain: "b.com".into(),
169 },
170 DelegationFailure::VerificationFailed {
171 check: "sig".into(),
172 message: "bad".into(),
173 },
174 DelegationFailure::ToolDependencyFailed {
175 tool: "t".into(),
176 error: "e".into(),
177 },
178 DelegationFailure::PartialCompletion {
179 completed_steps: 3,
180 total_steps: 5,
181 output: None,
182 },
183 DelegationFailure::FallbackInvoked {
184 original_delegate: "a".into(),
185 fallback_delegate: "b".into(),
186 reason: "r".into(),
187 },
188 DelegationFailure::Timeout {
189 deadline_secs: 10,
190 elapsed_secs: 12,
191 },
192 ];
193 for v in &variants {
194 let json = serde_json::to_string(v).unwrap();
195 assert!(json.contains("\"type\""));
196 let back: DelegationFailure = serde_json::from_str(&json).unwrap();
197 assert_eq!(*v, back);
198 }
199 }
200}