Skip to main content

agent_effects_store/
failure.rs

1//! Classification of execution failures.
2
3use std::time::Duration;
4
5use serde::{Deserialize, Serialize};
6
7/// What kind of failure an action reported.
8///
9/// The classification, not the mere presence of an error, decides whether the
10/// runtime retries, fails or treats the outcome as unknown.
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case", tag = "class")]
13pub enum FailureClass {
14    /// The request did not take effect and may succeed if repeated, e.g. a
15    /// connection refused before anything was sent.
16    Transient,
17    /// The request did not take effect and will not succeed if repeated.
18    Permanent,
19    /// The request may or may not have taken effect, e.g. a timeout after
20    /// the request was sent.
21    Ambiguous,
22    /// The remote system asked the caller to slow down.
23    RateLimited {
24        /// How long the remote system asked the caller to wait, if it said.
25        retry_after: Option<Duration>,
26    },
27    /// The credentials were missing or invalid.
28    Authentication,
29    /// The credentials were valid but not allowed to perform the action.
30    Authorization,
31    /// The remote system rejected the request as malformed.
32    Validation,
33}
34
35/// What the runtime does with a failure.
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum Disposition {
38    /// The effect definitely did not apply; retry if the policy allows.
39    Retry,
40    /// The effect definitely did not apply; stop.
41    Fail,
42    /// The effect may have applied; the outcome is unknown.
43    Unknown,
44}
45
46impl FailureClass {
47    /// How the runtime treats this failure.
48    pub const fn disposition(self) -> Disposition {
49        match self {
50            Self::Transient | Self::RateLimited { .. } => Disposition::Retry,
51            Self::Ambiguous => Disposition::Unknown,
52            Self::Permanent | Self::Authentication | Self::Authorization | Self::Validation => {
53                Disposition::Fail
54            }
55        }
56    }
57
58    /// The minimum delay the remote system asked for, if any.
59    pub const fn retry_after(self) -> Option<Duration> {
60        match self {
61            Self::RateLimited { retry_after } => retry_after,
62            _ => None,
63        }
64    }
65
66    /// Refines a classification with whether the request reached the remote
67    /// system.
68    ///
69    /// Knowing the request was never sent (`Some(false)`, e.g. a refused
70    /// connection or a DNS failure) turns [`Self::Ambiguous`] into
71    /// [`Self::Transient`], because nothing can have been applied. Nothing
72    /// else changes: a sent request that got a definitive answer, such as a
73    /// 503, is still whatever its class says.
74    #[must_use]
75    pub const fn with_request_sent(self, request_sent: Option<bool>) -> Self {
76        match (self, request_sent) {
77            (Self::Ambiguous, Some(false)) => Self::Transient,
78            _ => self,
79        }
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn unsent_requests_are_never_ambiguous() {
89        assert_eq!(
90            FailureClass::Ambiguous.with_request_sent(Some(false)),
91            FailureClass::Transient
92        );
93        assert_eq!(
94            FailureClass::Ambiguous.with_request_sent(Some(true)),
95            FailureClass::Ambiguous
96        );
97        assert_eq!(
98            FailureClass::Ambiguous.with_request_sent(None),
99            FailureClass::Ambiguous
100        );
101        assert_eq!(
102            FailureClass::Transient.with_request_sent(Some(true)),
103            FailureClass::Transient
104        );
105    }
106
107    #[test]
108    fn dispositions() {
109        assert_eq!(FailureClass::Ambiguous.disposition(), Disposition::Unknown);
110        assert_eq!(
111            FailureClass::RateLimited { retry_after: None }.disposition(),
112            Disposition::Retry
113        );
114        assert_eq!(
115            FailureClass::Authentication.disposition(),
116            Disposition::Fail
117        );
118    }
119}