Skip to main content

jamjet_protocols/
failure.rs

1//! Typed failure taxonomy for delegated agent operations (B.2).
2//!
3//! Defines canonical failure types that can occur when one agent delegates
4//! work to another. Each failure carries severity, retryable flag,
5//! optional partial output, and a recommended fallback action.
6
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10/// Canonical delegation failure variants.
11///
12/// Serialises with `"type"` as the tag field, e.g.
13/// `{"type": "delegate_unreachable", "url": "...", "message": "..."}`.
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15#[serde(tag = "type", rename_all = "snake_case")]
16pub enum DelegationFailure {
17    /// The delegated agent could not be reached.
18    DelegateUnreachable { url: String, message: String },
19    /// The requested capability is not among those the delegate advertises.
20    CapabilityMismatch {
21        requested: String,
22        available: Vec<String>,
23    },
24    /// A governance policy denied the delegation.
25    PolicyDenied { policy_id: String, reason: String },
26    /// The delegation requires human approval before it can proceed.
27    ApprovalRequired { prompt: String },
28    /// The delegation would exceed an allocated budget.
29    BudgetExceeded {
30        limit: f64,
31        actual: f64,
32        unit: String,
33    },
34    /// A trust-boundary crossing was denied.
35    TrustCrossingDenied {
36        from_domain: String,
37        to_domain: String,
38    },
39    /// A verification check (signature, hash, attestation) failed.
40    VerificationFailed { check: String, message: String },
41    /// A tool dependency required by the delegate failed.
42    ToolDependencyFailed { tool: String, error: String },
43    /// The delegation partially completed before failing.
44    PartialCompletion {
45        completed_steps: usize,
46        total_steps: usize,
47        output: Option<Value>,
48    },
49    /// The runtime fell back to an alternate delegate.
50    FallbackInvoked {
51        original_delegate: String,
52        fallback_delegate: String,
53        reason: String,
54    },
55    /// The delegation timed out.
56    Timeout {
57        deadline_secs: u64,
58        elapsed_secs: u64,
59    },
60}
61
62/// Severity classification for delegation failures.
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
64#[serde(rename_all = "snake_case")]
65pub enum FailureSeverity {
66    /// Non-fatal; the workflow can continue with degraded output.
67    Warning,
68    /// An error that prevents this delegation from succeeding but does not
69    /// necessarily terminate the workflow.
70    Error,
71    /// A fatal error that should abort the entire workflow.
72    Fatal,
73}
74
75/// Enriched failure envelope carrying metadata alongside the failure variant.
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct DelegationFailureInfo {
78    /// The specific failure that occurred.
79    pub failure: DelegationFailure,
80    /// How severe the failure is.
81    pub severity: FailureSeverity,
82    /// Whether the caller should retry.
83    pub retryable: bool,
84    /// Partial output produced before the failure, if any.
85    pub partial_output: Option<Value>,
86    /// A recommended fallback agent or strategy identifier.
87    pub recommended_fallback: Option<String>,
88    /// An opaque reference for the audit log.
89    pub audit_ref: Option<String>,
90    /// ISO-8601 timestamp of when the failure occurred.
91    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}