Skip to main content

visual_rubric/
errors.rs

1use std::fmt;
2use std::time::Duration;
3
4#[derive(Clone, Debug, PartialEq, Eq)]
5/// One observed Codex ACP rate-limit event.
6pub struct RateLimitEvent {
7    /// Worker that observed the rate limit.
8    pub worker_id: usize,
9    /// Retry attempt number.
10    pub attempt: u32,
11    /// Delay applied before the next retry.
12    pub delay: Duration,
13    /// Server-provided retry delay, when present.
14    pub retry_after: Option<Duration>,
15}
16
17/// Errors produced by the Codex ACP pool and worker runtime.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub enum PoolError {
20    /// Failed to spawn or initialize Codex ACP.
21    Spawn(String),
22    /// JSON-RPC request or response failure.
23    Rpc(String),
24    /// Codex ACP reported a rate limit.
25    RateLimited {
26        /// Server-provided retry delay, when present.
27        retry_after: Option<Duration>,
28    },
29    /// Codex ACP reported exhausted usage quota.
30    QuotaExceeded,
31    /// A worker process or thread crashed.
32    WorkerCrashed {
33        /// Worker that crashed.
34        worker_id: usize,
35        /// Crash detail.
36        message: String,
37    },
38    /// A model response could not be parsed into a verdict.
39    ParseVerdict(String),
40    /// A submitted job exceeded its timeout.
41    Timeout {
42        /// Worker that timed out.
43        worker_id: usize,
44        /// Configured timeout.
45        timeout: Duration,
46    },
47    /// No worker is currently available to accept jobs.
48    NoLiveWorkers,
49    /// The pool has been closed.
50    Closed,
51}
52
53impl fmt::Display for PoolError {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        match self {
56            Self::Spawn(message) => write!(f, "spawn codex-acp: {message}"),
57            Self::Rpc(message) => write!(f, "codex-acp rpc error: {message}"),
58            Self::RateLimited { retry_after } => {
59                write!(f, "codex-acp rate limited")?;
60                if let Some(retry_after) = retry_after {
61                    write!(f, " retry_after={retry_after:?}")?;
62                }
63                Ok(())
64            }
65            Self::QuotaExceeded => write!(f, "codex-acp usage quota exceeded"),
66            Self::WorkerCrashed { worker_id, message } => {
67                write!(f, "rubric worker {worker_id} crashed: {message}")
68            }
69            Self::ParseVerdict(message) => write!(f, "parse rubric verdict: {message}"),
70            Self::Timeout { worker_id, timeout } => {
71                write!(f, "rubric worker {worker_id} timed out after {timeout:?}")
72            }
73            Self::NoLiveWorkers => write!(f, "no live rubric workers"),
74            Self::Closed => write!(f, "rubric pool is closed"),
75        }
76    }
77}
78
79impl std::error::Error for PoolError {}
80
81/// Errors returned by public one-shot rubric APIs.
82#[derive(Debug)]
83#[non_exhaustive]
84pub enum RubricError {
85    /// The PNG file could not be read.
86    ReadPng {
87        /// Path that failed to read.
88        path: std::path::PathBuf,
89        /// Underlying filesystem error.
90        source: std::io::Error,
91    },
92    /// Codex ACP pool or worker failure.
93    Pool(PoolError),
94    /// Model output could not be parsed as a rubric verdict.
95    ParseVerdict {
96        /// Raw model output.
97        text: String,
98        /// Underlying JSON error.
99        source: serde_json::Error,
100    },
101    /// A parsed verdict failed the assertion.
102    Assertion {
103        /// Screenshot or check name.
104        name: String,
105        /// Failure reason from the verdict.
106        reason: String,
107        /// Reported visual anomalies.
108        anomalies: Vec<String>,
109    },
110}
111
112impl fmt::Display for RubricError {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        match self {
115            Self::ReadPng { path, source } => {
116                write!(f, "read png {}: {source}", path.display())
117            }
118            Self::Pool(error) => error.fmt(f),
119            Self::ParseVerdict { text, source } => {
120                write!(f, "parse verdict from {text:?}: {source}")
121            }
122            Self::Assertion {
123                name,
124                reason,
125                anomalies,
126            } => {
127                write!(f, "[{name}] {reason} (anomalies: {anomalies:?})")
128            }
129        }
130    }
131}
132
133impl std::error::Error for RubricError {
134    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
135        match self {
136            Self::ReadPng { source, .. } => Some(source),
137            Self::Pool(error) => Some(error),
138            Self::ParseVerdict { source, .. } => Some(source),
139            Self::Assertion { .. } => None,
140        }
141    }
142}
143
144impl From<PoolError> for RubricError {
145    fn from(error: PoolError) -> Self {
146        Self::Pool(error)
147    }
148}