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 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 ACP pool, worker runtime, or vision API.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub enum PoolError {
20    /// Failed to spawn or initialize ACP.
21    Spawn(String),
22    /// JSON-RPC request or response failure.
23    Rpc(String),
24    /// ACP reported a rate limit.
25    RateLimited {
26        /// Server-provided retry delay, when present.
27        retry_after: Option<Duration>,
28    },
29    /// 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    /// Vision API HTTP call failed.
52    VisionApi(String),
53}
54
55impl fmt::Display for PoolError {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        match self {
58            Self::Spawn(message) => write!(f, "spawn acp process: {message}"),
59            Self::Rpc(message) => write!(f, "acp rpc error: {message}"),
60            Self::RateLimited { retry_after } => {
61                write!(f, "acp rate limited")?;
62                if let Some(retry_after) = retry_after {
63                    write!(f, " retry_after={retry_after:?}")?;
64                }
65                Ok(())
66            }
67            Self::QuotaExceeded => write!(f, "acp usage quota exceeded"),
68            Self::WorkerCrashed { worker_id, message } => {
69                write!(f, "rubric worker {worker_id} crashed: {message}")
70            }
71            Self::ParseVerdict(message) => write!(f, "parse rubric verdict: {message}"),
72            Self::Timeout { worker_id, timeout } => {
73                write!(f, "rubric worker {worker_id} timed out after {timeout:?}")
74            }
75            Self::NoLiveWorkers => write!(f, "no live rubric workers"),
76            Self::Closed => write!(f, "rubric pool is closed"),
77            Self::VisionApi(message) => write!(f, "vision api error: {message}"),
78        }
79    }
80}
81
82impl std::error::Error for PoolError {}
83
84/// Errors returned by public one-shot rubric APIs.
85#[derive(Debug)]
86#[non_exhaustive]
87pub enum RubricError {
88    /// The PNG file could not be read.
89    ReadPng {
90        /// Path that failed to read.
91        path: std::path::PathBuf,
92        /// Underlying filesystem error.
93        source: std::io::Error,
94    },
95    /// ACP pool or worker failure.
96    Pool(PoolError),
97    /// Model output could not be parsed as a rubric verdict.
98    ParseVerdict {
99        /// Raw model output.
100        text: String,
101        /// Underlying JSON error.
102        source: serde_json::Error,
103    },
104    /// A parsed verdict failed the assertion.
105    Assertion {
106        /// Screenshot or check name.
107        name: String,
108        /// Failure reason from the verdict.
109        reason: String,
110        /// Reported visual anomalies.
111        anomalies: Vec<String>,
112    },
113}
114
115impl fmt::Display for RubricError {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        match self {
118            Self::ReadPng { path, source } => {
119                write!(f, "read png {}: {source}", path.display())
120            }
121            Self::Pool(error) => error.fmt(f),
122            Self::ParseVerdict { text, source } => {
123                write!(f, "parse verdict from {text:?}: {source}")
124            }
125            Self::Assertion {
126                name,
127                reason,
128                anomalies,
129            } => {
130                write!(f, "[{name}] {reason} (anomalies: {anomalies:?})")
131            }
132        }
133    }
134}
135
136impl std::error::Error for RubricError {
137    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
138        match self {
139            Self::ReadPng { source, .. } => Some(source),
140            Self::Pool(error) => Some(error),
141            Self::ParseVerdict { source, .. } => Some(source),
142            Self::Assertion { .. } => None,
143        }
144    }
145}
146
147impl From<PoolError> for RubricError {
148    fn from(error: PoolError) -> Self {
149        Self::Pool(error)
150    }
151}