1use std::{io, path::PathBuf, sync::Arc};
2
3use nanocodex_oai_api::ResponseError;
4pub use nanocodex_oai_api::transport::ResponsesError;
5
6#[derive(Debug, thiserror::Error)]
8pub enum NanocodexError {
9 #[error("invalid task request: {0}")]
11 InvalidRequest(String),
12
13 #[error("failed to resolve task workspace {path}: {source}")]
15 ResolveWorkspace {
16 path: PathBuf,
18 #[source]
20 source: io::Error,
21 },
22
23 #[error("task workspace is not a directory: {path}")]
25 WorkspaceNotDirectory {
26 path: PathBuf,
28 },
29
30 #[error("task workspace path is not valid UTF-8: {path}")]
32 WorkspaceNotUtf8 {
33 path: PathBuf,
35 },
36
37 #[error("an active agent session cannot change workspace from {current} to {requested}")]
39 WorkspaceChanged {
40 current: String,
42 requested: String,
44 },
45
46 #[error("malformed Responses API event: {detail}")]
48 MalformedResponse {
49 detail: &'static str,
51 },
52
53 #[error("invalid Responses attempt state: {detail}")]
55 InvalidAttemptState {
56 detail: &'static str,
58 },
59
60 #[error("failed to fingerprint the immutable prompt prefix: {0}")]
62 SerializePromptPrefix(#[source] serde_json::Error),
63
64 #[error("the agent stopped before accepting the command")]
66 AgentStopped,
67
68 #[error("the agent stopped before the turn completed")]
70 TurnStopped,
71
72 #[error(transparent)]
75 Shutdown(Arc<Self>),
76
77 #[error("the targeted turn is queued, completed, or otherwise not active for steering")]
79 TurnNotSteerable,
80
81 #[error("the active turn's steering queue is full")]
83 SteerQueueFull,
84
85 #[error("the targeted turn has already completed or been cancelled")]
87 TurnNotCancellable,
88
89 #[error("the turn was cancelled")]
91 TurnCancelled,
92
93 #[error("the agent has no safe conversation boundary to fork")]
95 ForkBeforeCompletedTurn,
96
97 #[error("the completed turn belongs to a different conversation lineage")]
99 CheckpointLineageMismatch,
100
101 #[error("invalid session snapshot: {0}")]
103 InvalidSessionSnapshot(String),
104
105 #[error("building an agent requires an active Tokio runtime")]
107 TokioRuntimeUnavailable,
108
109 #[error("failed to initialize a Codex rollout under {codex_home}: {source}")]
111 InitializeRollout {
112 codex_home: PathBuf,
114 #[source]
116 source: io::Error,
117 },
118
119 #[error("failed to persist Codex rollout at {path}: {source}")]
121 PersistRollout {
122 path: PathBuf,
124 #[source]
126 source: io::Error,
127 },
128
129 #[error(transparent)]
131 Event(#[from] nanocodex_oai_api::events::EventError),
132
133 #[error(transparent)]
135 Response(#[from] ResponseError),
136
137 #[cfg(not(target_family = "wasm"))]
139 #[error("failed to build tools for an agent driver: {0}")]
140 Tools(#[from] nanocodex_tools::ToolsBuildError),
141}
142
143impl NanocodexError {
144 #[must_use]
147 pub fn responses_error(&self) -> Option<&ResponsesError> {
148 match self {
149 Self::Response(error) => error.responses_error(),
150 Self::Shutdown(error) => error.responses_error(),
151 _ => None,
152 }
153 }
154}
155
156pub type Result<T> = std::result::Result<T, NanocodexError>;
158
159#[cfg(test)]
160mod tests {
161 use super::{NanocodexError, ResponsesError};
162 use nanocodex_oai_api::{ResponseError, tower::ResponsesServiceError};
163
164 #[test]
165 fn response_error_is_the_single_provider_failure_boundary() {
166 let service = NanocodexError::Response(ResponseError::from(ResponsesServiceError::from(
167 ResponsesError::UnexpectedEnd,
168 )));
169 assert!(matches!(
170 service.responses_error(),
171 Some(ResponsesError::UnexpectedEnd)
172 ));
173
174 let service = ResponsesServiceError::from(ResponsesError::UnexpectedEnd);
175 let error =
176 NanocodexError::Response(ResponseError::from(Box::new(service) as tower::BoxError));
177 assert!(matches!(
178 error.responses_error(),
179 Some(ResponsesError::UnexpectedEnd)
180 ));
181 assert_eq!(
182 error.to_string(),
183 "Responses WebSocket closed without a close frame"
184 );
185 }
186}