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