1use std::sync::Arc;
2#[cfg(feature = "openai")]
3use std::{io, path::PathBuf};
4
5#[cfg(feature = "openai")]
6use nanocodex_oai_api::ResponseError;
7#[cfg(feature = "openai")]
8pub use nanocodex_oai_api::transport::ResponsesError;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum ExecutionPolicyDisposition {
13 Retry,
15 Reopen,
17 Fatal,
19}
20
21#[derive(Debug, thiserror::Error)]
23pub enum NanocodexError {
24 #[error("invalid task request: {0}")]
26 InvalidRequest(String),
27
28 #[cfg(feature = "openai")]
30 #[error("failed to resolve task workspace {path}: {source}")]
31 ResolveWorkspace {
32 path: PathBuf,
34 #[source]
36 source: io::Error,
37 },
38
39 #[cfg(feature = "openai")]
41 #[error("task workspace is not a directory: {path}")]
42 WorkspaceNotDirectory {
43 path: PathBuf,
45 },
46
47 #[cfg(feature = "openai")]
49 #[error("task workspace path is not valid UTF-8: {path}")]
50 WorkspaceNotUtf8 {
51 path: PathBuf,
53 },
54
55 #[cfg(feature = "openai")]
57 #[error("an active agent session cannot change workspace from {current} to {requested}")]
58 WorkspaceChanged {
59 current: String,
61 requested: String,
63 },
64
65 #[cfg(feature = "openai")]
67 #[error("malformed Responses API event: {detail}")]
68 MalformedResponse {
69 detail: &'static str,
71 },
72
73 #[cfg(feature = "openai")]
75 #[error("invalid Responses attempt state: {detail}")]
76 InvalidAttemptState {
77 detail: &'static str,
79 },
80
81 #[cfg(feature = "openai")]
83 #[error("failed to fingerprint the immutable prompt prefix: {0}")]
84 SerializePromptPrefix(#[source] serde_json::Error),
85
86 #[error("the agent stopped before accepting the command")]
88 AgentStopped,
89
90 #[error("the execution-policy-owned agent stopped and must be reopened")]
93 ExecutionPolicyOwnerStopped,
94
95 #[error("the agent stopped before the turn completed")]
97 TurnStopped,
98
99 #[error(transparent)]
102 Shutdown(Arc<Self>),
103
104 #[error("the targeted turn is queued, completed, or otherwise not active for steering")]
106 TurnNotSteerable,
107
108 #[error("the active turn's steering queue is full")]
110 SteerQueueFull,
111
112 #[error("the targeted turn has already completed or been cancelled")]
114 TurnNotCancellable,
115
116 #[error("the turn was cancelled")]
118 TurnCancelled,
119
120 #[cfg(feature = "openai")]
122 #[error("the agent has no safe conversation boundary to fork")]
123 ForkBeforeCompletedTurn,
124
125 #[cfg(feature = "openai")]
127 #[error("the completed turn belongs to a different conversation lineage")]
128 CheckpointLineageMismatch,
129
130 #[cfg(feature = "openai")]
132 #[error("invalid session snapshot: {0}")]
133 InvalidSessionSnapshot(String),
134
135 #[cfg(feature = "openai")]
137 #[error("{layer} execution policy failed: {source}")]
138 ExecutionPolicy {
139 layer: &'static str,
141 disposition: ExecutionPolicyDisposition,
143 #[source]
145 source: Arc<dyn std::error::Error + Send + Sync>,
146 },
147
148 #[cfg(feature = "openai")]
150 #[error("identified prompt submission requires a configured execution policy")]
151 ExecutionPolicyNotConfigured,
152
153 #[cfg(feature = "openai")]
155 #[error("invalid execution policy state: {0}")]
156 InvalidExecutionPolicy(String),
157
158 #[cfg(feature = "openai")]
161 #[error("execution policy does not implement required capability `{capability}`")]
162 ExecutionPolicyCapabilityUnsupported {
163 capability: &'static str,
165 },
166
167 #[cfg(feature = "openai")]
169 #[error(
170 "cannot {operation} from an agent with an attached execution policy; build the branch with its own execution policy"
171 )]
172 ExecutionPolicyBranchUnsupported {
173 operation: &'static str,
175 },
176
177 #[cfg(feature = "openai")]
179 #[error("execution policy payload is invalid: {0}")]
180 ExecutionPayload(#[source] serde_json::Error),
181
182 #[cfg(feature = "openai")]
184 #[error("a policy-replayed result cannot be used as an in-process fork checkpoint")]
185 ReplayedCheckpointUnavailable,
186
187 #[error("the selected agent backend does not support {capability}")]
189 UnsupportedCapability {
190 capability: &'static str,
192 },
193
194 #[error("agent backend violated the lifecycle contract: {detail}")]
196 BackendContract {
197 detail: &'static str,
199 },
200
201 #[error("{backend} backend failed: {source}")]
203 Backend {
204 backend: &'static str,
206 #[source]
208 source: Arc<dyn std::error::Error + Send + Sync>,
209 },
210
211 #[cfg(feature = "openai")]
213 #[error("durable operation previously failed: {0}")]
214 ReplayedExecutionFailed(String),
215
216 #[cfg(feature = "openai")]
218 #[error("building an agent requires an active Tokio runtime")]
219 TokioRuntimeUnavailable,
220
221 #[cfg(feature = "openai")]
223 #[error("failed to initialize a Codex rollout under {codex_home}: {source}")]
224 InitializeRollout {
225 codex_home: PathBuf,
227 #[source]
229 source: io::Error,
230 },
231
232 #[cfg(feature = "openai")]
234 #[error("failed to persist Codex rollout at {path}: {source}")]
235 PersistRollout {
236 path: PathBuf,
238 #[source]
240 source: io::Error,
241 },
242
243 #[error(transparent)]
245 Event(#[from] nanocodex_oai_api::events::EventError),
246
247 #[cfg(feature = "openai")]
249 #[error(transparent)]
250 Response(#[from] ResponseError),
251
252 #[cfg(feature = "openai")]
254 #[error("failed to build tools for an agent driver: {0}")]
255 Tools(#[from] nanocodex_tools::ToolsBuildError),
256}
257
258impl NanocodexError {
259 #[doc(hidden)]
261 pub fn backend<E>(backend: &'static str, source: E) -> Self
262 where
263 E: std::error::Error + Send + Sync + 'static,
264 {
265 Self::Backend {
266 backend,
267 source: Arc::new(source),
268 }
269 }
270
271 #[cfg(feature = "openai")]
273 #[doc(hidden)]
274 pub fn execution_policy<E>(layer: &'static str, source: E) -> Self
275 where
276 E: std::error::Error + Send + Sync + 'static,
277 {
278 Self::ExecutionPolicy {
279 layer,
280 disposition: ExecutionPolicyDisposition::Fatal,
281 source: Arc::new(source),
282 }
283 }
284
285 #[cfg(feature = "openai")]
287 #[doc(hidden)]
288 pub fn execution_policy_with_disposition<E>(
289 layer: &'static str,
290 disposition: ExecutionPolicyDisposition,
291 source: E,
292 ) -> Self
293 where
294 E: std::error::Error + Send + Sync + 'static,
295 {
296 Self::ExecutionPolicy {
297 layer,
298 disposition,
299 source: Arc::new(source),
300 }
301 }
302
303 #[cfg(feature = "openai")]
305 #[must_use]
306 pub fn execution_policy_disposition(&self) -> Option<ExecutionPolicyDisposition> {
307 match self {
308 Self::ExecutionPolicy { disposition, .. } => Some(*disposition),
309 Self::ExecutionPolicyOwnerStopped => Some(ExecutionPolicyDisposition::Reopen),
310 Self::Shutdown(source) => source.execution_policy_disposition(),
311 _ => None,
312 }
313 }
314
315 #[cfg(feature = "openai")]
318 #[must_use]
319 pub fn responses_error(&self) -> Option<&ResponsesError> {
320 match self {
321 Self::Response(error) => error.responses_error(),
322 Self::Shutdown(error) => error.responses_error(),
323 _ => None,
324 }
325 }
326}
327
328pub type Result<T> = std::result::Result<T, NanocodexError>;
330
331#[cfg(all(test, feature = "openai"))]
332mod tests {
333 use super::{NanocodexError, ResponsesError};
334 use nanocodex_oai_api::{ResponseError, tower::ResponsesServiceError};
335
336 #[test]
337 fn response_error_is_the_single_provider_failure_boundary() {
338 let service = NanocodexError::Response(ResponseError::from(ResponsesServiceError::from(
339 ResponsesError::UnexpectedEnd,
340 )));
341 assert!(matches!(
342 service.responses_error(),
343 Some(ResponsesError::UnexpectedEnd)
344 ));
345
346 let service = ResponsesServiceError::from(ResponsesError::UnexpectedEnd);
347 let error =
348 NanocodexError::Response(ResponseError::from(Box::new(service) as tower::BoxError));
349 assert!(matches!(
350 error.responses_error(),
351 Some(ResponsesError::UnexpectedEnd)
352 ));
353 assert_eq!(
354 error.to_string(),
355 "Responses WebSocket closed without a close frame"
356 );
357 }
358}