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}