Skip to main content

ops_rs/
failure.rs

1//! The failure taxonomy — WHOSE problem a failure is.
2//!
3//! Defined ONCE here in `ops` (the leaf crate of the error path — capdag
4//! depends on ops, so this is the deepest shared home) and re-exported by
5//! capdag as the cartridge-contract surface (`capdag::AttributionClass`).
6//! Cartridge error enums declare a `attribution_class()` per variant beside
7//! `error_code()`; the bifaci ERR frame carries the class over the wire (all
8//! four language runtimes mirror it); the orchestrator and the engine carry
9//! it structurally to the run record. No layer ever infers another layer's
10//! class from message text — an error that reaches a boundary without a
11//! declared class is a contract violation. Attribution is always emitted at
12//! source and is never inferred from message text.
13//! See `docs/failure-taxonomy.md` (repo root) for the full architecture and
14//! `capdag/docs/17.2-error-handling.md` for the protocol contract.
15
16use serde::{Deserialize, Serialize};
17
18/// Whose problem a failure is. Declared at the error's DEFINITION site,
19/// carried structurally through every hop.
20///
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "lowercase")]
23pub enum AttributionClass {
24    /// Deterministic on the INPUT (context overflow, invalid request,
25    /// unsupported format). The user's to fix; retrying can never succeed —
26    /// tasks failing with this class are marked permanently failed.
27    Input,
28    /// A compute resource was exhausted (GPU VRAM, host memory). Often
29    /// transient (another process holding memory) — retryable.
30    Resource,
31    /// The environment failed (network, registry, model download/integrity,
32    /// cartridge process death). Transient by nature — retryable.
33    Environment,
34    /// Everything else: a defect in the engine or a cartridge. Ours, said
35    /// plainly. Retryable (races un-race), but never blamed on the user.
36    Internal,
37}
38
39impl AttributionClass {
40    /// The wire token — used in the ERR frame meta, the machine_runs
41    /// columns, the gRPC proto, and the loom. One vocabulary everywhere.
42    pub fn as_str(&self) -> &'static str {
43        match self {
44            AttributionClass::Input => "input",
45            AttributionClass::Resource => "resource",
46            AttributionClass::Environment => "environment",
47            AttributionClass::Internal => "internal",
48        }
49    }
50
51    /// Parse a wire token. Callers must reject `None` as a protocol error.
52    pub fn from_wire(token: &str) -> Option<AttributionClass> {
53        match token {
54            "input" => Some(AttributionClass::Input),
55            "resource" => Some(AttributionClass::Resource),
56            "environment" => Some(AttributionClass::Environment),
57            "internal" => Some(AttributionClass::Internal),
58            _ => None,
59        }
60    }
61
62    /// Whether retrying can NEVER succeed: the failure is a deterministic
63    /// function of the input. Resource/environment/internal stay retryable
64    /// (memory frees up, networks recover, races un-race).
65    pub fn is_permanent(&self) -> bool {
66        matches!(self, AttributionClass::Input)
67    }
68}
69
70impl std::fmt::Display for AttributionClass {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.write_str(self.as_str())
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    // TEST1730: the wire vocabulary round-trips exactly and rejects unknowns.
81    #[test]
82    fn test1730_wire_tokens_round_trip() {
83        for class in [
84            AttributionClass::Input,
85            AttributionClass::Resource,
86            AttributionClass::Environment,
87            AttributionClass::Internal,
88        ] {
89            assert_eq!(AttributionClass::from_wire(class.as_str()), Some(class));
90        }
91        assert_eq!(AttributionClass::from_wire("user-error"), None);
92        assert_eq!(AttributionClass::from_wire(""), None);
93    }
94
95    // TEST1731: only Input is permanent — the retry machinery keys on this.
96    #[test]
97    fn test1731_only_input_is_permanent() {
98        assert!(AttributionClass::Input.is_permanent());
99        assert!(!AttributionClass::Resource.is_permanent());
100        assert!(!AttributionClass::Environment.is_permanent());
101        assert!(!AttributionClass::Internal.is_permanent());
102    }
103}