Skip to main content

github_copilot_sdk/
errors.rs

1//! Crate errors.
2
3use std::backtrace::{Backtrace, BacktraceStatus};
4use std::borrow::{Borrow, Cow};
5use std::fmt;
6use std::time::Duration;
7
8use crate::types::SessionId;
9
10/// Crate-specific [`Result`](std::result::Result).
11pub type Result<T> = std::result::Result<T, Error>;
12
13// ── Repr / Custom ─────────────────────────────────────────────────────────────
14
15/// Internal representation shared by all SDK error structs.
16///
17/// `T` is the `*Kind` enum specific to each error struct. Shared across
18/// [`Error`], [`ProtocolError`], [`SessionError`], [`FsError`],
19/// [`RecvError`], and the crate-internal `EmbeddedCliError`.
20#[derive(Debug)]
21pub(crate) enum Repr<T: fmt::Debug> {
22    Simple(T),
23    SimpleMessage(T, Cow<'static, str>),
24    Custom(Custom<T>),
25    // CustomMessage(Custom<T>, Cow<'static, str>),
26}
27
28/// Custom error representation: a kind tag plus a boxed source error.
29#[derive(Debug)]
30pub(crate) struct Custom<T: fmt::Debug> {
31    pub(crate) kind: T,
32    pub(crate) error: Box<dyn std::error::Error + Send + Sync>,
33}
34
35// ── ProtocolErrorKind ─────────────────────────────────────────
36
37/// Specific protocol-level error kind in the JSON-RPC transport or CLI lifecycle.
38#[derive(Clone, Debug, PartialEq, Eq)]
39#[non_exhaustive]
40pub enum ProtocolErrorKind {
41    /// Missing `Content-Length` header in a JSON-RPC message.
42    MissingContentLength,
43
44    /// Invalid `Content-Length` header value.
45    InvalidContentLength(String),
46
47    /// A pending JSON-RPC request was cancelled (e.g. the response channel was dropped).
48    RequestCancelled,
49
50    /// The CLI process did not report a listening port within the timeout.
51    CliStartupTimeout,
52
53    /// The CLI process exited before reporting a listening port.
54    CliStartupFailed,
55
56    /// The CLI server's protocol version is outside the SDK's supported range.
57    VersionMismatch {
58        /// Version reported by the server.
59        server: u32,
60        /// Minimum version supported by this SDK.
61        min: u32,
62        /// Maximum version supported by this SDK.
63        max: u32,
64    },
65
66    /// The CLI server's protocol version changed between calls.
67    VersionChanged {
68        /// Previously negotiated version.
69        previous: u32,
70        /// Newly reported version.
71        current: u32,
72    },
73}
74
75impl fmt::Display for ProtocolErrorKind {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        match self {
78            ProtocolErrorKind::MissingContentLength => {
79                write!(f, "missing Content-Length header")
80            }
81            ProtocolErrorKind::InvalidContentLength(v) => {
82                write!(f, "invalid Content-Length value: \"{v}\"")
83            }
84            ProtocolErrorKind::RequestCancelled => write!(f, "request cancelled"),
85            ProtocolErrorKind::CliStartupTimeout => {
86                write!(f, "timed out waiting for CLI to report listening port")
87            }
88            ProtocolErrorKind::CliStartupFailed => {
89                write!(f, "CLI exited before reporting listening port")
90            }
91            ProtocolErrorKind::VersionMismatch { server, min, max } => {
92                write!(
93                    f,
94                    "version mismatch: server={server}, supported={min}\u{2013}{max}"
95                )
96            }
97            ProtocolErrorKind::VersionChanged { previous, current } => {
98                write!(f, "version changed: was {previous}, now {current}")
99            }
100        }
101    }
102}
103
104// ── SessionErrorKind ───────────────────────────────────────────
105
106/// Session-scoped error kind.
107#[derive(Clone, Debug, PartialEq, Eq)]
108#[non_exhaustive]
109pub enum SessionErrorKind {
110    /// The CLI could not find the requested session.
111    NotFound(SessionId),
112
113    /// The CLI reported an error during agent execution (via `session.error` event).
114    AgentError,
115
116    /// A `send_and_wait` call exceeded its timeout.
117    Timeout(Duration),
118
119    /// `send` was called while a `send_and_wait` is in flight.
120    SendWhileWaiting,
121
122    /// The session event loop exited before a pending `send_and_wait` completed.
123    EventLoopClosed,
124
125    /// Elicitation is not supported by the host.
126    /// Check `session.capabilities().ui.elicitation` before calling UI methods.
127    ElicitationNotSupported,
128
129    /// The client was started with [`crate::ClientOptions::session_fs`] but this
130    /// session was created without a [`crate::session_fs::SessionFsProvider`]. Set one via
131    /// [`crate::SessionConfig::with_session_fs_provider`] (or
132    /// [`crate::ResumeSessionConfig::with_session_fs_provider`]).
133    SessionFsProviderRequired,
134
135    /// [`crate::ClientOptions::session_fs`] was provided with empty or invalid
136    /// fields. All of `initial_cwd` and `session_state_path` must be non-empty.
137    InvalidSessionFsConfig,
138
139    /// The CLI returned a different session ID than the one the SDK registered.
140    SessionIdMismatch {
141        /// Session ID registered by the SDK before the RPC was sent.
142        requested: SessionId,
143        /// Session ID returned by the CLI.
144        returned: SessionId,
145    },
146}
147
148impl fmt::Display for SessionErrorKind {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        match self {
151            SessionErrorKind::NotFound(id) => write!(f, "session not found: {id}"),
152            SessionErrorKind::AgentError => write!(f, "agent error"),
153            SessionErrorKind::Timeout(d) => write!(f, "timed out after {d:?}"),
154            SessionErrorKind::SendWhileWaiting => {
155                write!(f, "cannot send while send_and_wait is in flight")
156            }
157            SessionErrorKind::EventLoopClosed => {
158                write!(f, "event loop closed before session reached idle")
159            }
160            SessionErrorKind::ElicitationNotSupported => write!(
161                f,
162                "elicitation not supported by host \
163                 \u{2014} check session.capabilities().ui.elicitation first"
164            ),
165            SessionErrorKind::SessionFsProviderRequired => write!(
166                f,
167                "session was created on a client with session_fs configured \
168                 but no SessionFsProvider was supplied"
169            ),
170            SessionErrorKind::InvalidSessionFsConfig => {
171                write!(f, "invalid SessionFsConfig")
172            }
173            SessionErrorKind::SessionIdMismatch {
174                requested,
175                returned,
176            } => write!(
177                f,
178                "CLI returned session ID {returned} after SDK registered {requested}"
179            ),
180        }
181    }
182}
183
184// ── ErrorKind ─────────────────────────────────────────────────────────────────
185
186/// The kind of [`Error`].
187#[derive(Clone, Debug, PartialEq, Eq)]
188#[non_exhaustive]
189pub enum ErrorKind {
190    /// JSON-RPC transport or protocol violation.
191    Protocol(ProtocolErrorKind),
192    /// The CLI returned a JSON-RPC error response.
193    Rpc {
194        /// JSON-RPC error code.
195        code: i32,
196    },
197    /// Session-scoped error (not found, agent error, timeout, etc.).
198    Session(SessionErrorKind),
199    /// I/O error on the stdio transport or during process spawn.
200    Io,
201    /// Failed to serialize or deserialize a JSON-RPC message.
202    Json,
203    /// A required binary was not found on the system.
204    BinaryNotFound {
205        /// Name of the binary.
206        name: String,
207        /// Optional hint for how to resolve the issue.
208        hint: Option<String>,
209    },
210    /// Invalid combination of options or configuration.
211    InvalidConfig,
212}
213
214impl fmt::Display for ErrorKind {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        match self {
217            ErrorKind::Protocol(k) => write!(f, "{k}"),
218            ErrorKind::Rpc { code } => write!(f, "RPC error {code}"),
219            ErrorKind::Session(k) => write!(f, "{k}"),
220            ErrorKind::Io => write!(f, "I/O error"),
221            ErrorKind::Json => write!(f, "JSON error"),
222            ErrorKind::BinaryNotFound {
223                name,
224                hint: Some(h),
225            } => {
226                write!(f, "binary not found: {name} ({h})")
227            }
228            ErrorKind::BinaryNotFound { name, hint: None } => {
229                write!(f, "binary not found: {name}")
230            }
231            ErrorKind::InvalidConfig => write!(f, "invalid configuration"),
232        }
233    }
234}
235
236/// Errors returned by the SDK.
237pub struct Error {
238    repr: Repr<ErrorKind>,
239    // Only `Some` when `RUST_BACKTRACE` is set; boxed so the `Some` variant
240    // doesn't inflate `Error` beyond `clippy::result_large_err` limits.
241    backtrace: Option<Box<Backtrace>>,
242}
243
244impl Error {
245    /// Constructs a new `Error` boxing another [`std::error::Error`].
246    pub(crate) fn new<E>(kind: ErrorKind, error: E) -> Self
247    where
248        E: Into<Box<dyn std::error::Error + Send + Sync>>,
249    {
250        Self {
251            repr: Repr::Custom(Custom {
252                kind,
253                error: error.into(),
254            }),
255            backtrace: capture_backtrace(),
256        }
257    }
258
259    /// The [`ErrorKind`] of this `Error`.
260    pub fn kind(&self) -> &ErrorKind {
261        match &self.repr {
262            Repr::Simple(kind)
263            | Repr::SimpleMessage(kind, ..)
264            | Repr::Custom(Custom { kind, .. }) => kind,
265        }
266    }
267
268    /// The message provided when this `Error` was constructed, or `None`.
269    pub fn message(&self) -> Option<&str> {
270        match &self.repr {
271            Repr::SimpleMessage(_, message) => Some(message.borrow()),
272            _ => None,
273        }
274    }
275
276    /// Create an `Error` with a message.
277    #[must_use]
278    pub fn with_message<C>(kind: ErrorKind, message: C) -> Self
279    where
280        C: Into<Cow<'static, str>>,
281    {
282        Self {
283            repr: Repr::SimpleMessage(kind, message.into()),
284            backtrace: capture_backtrace(),
285        }
286    }
287
288    /// Returns `true` if this error indicates the transport is broken — the CLI
289    /// process exited, the connection was lost, or an I/O failure occurred.
290    /// Callers should discard the client and create a fresh one.
291    pub fn is_transport_failure(&self) -> bool {
292        matches!(self.kind(), ErrorKind::Io)
293            || matches!(
294                self.kind(),
295                ErrorKind::Protocol(ProtocolErrorKind::RequestCancelled)
296            )
297    }
298
299    /// Returns the JSON-RPC error code if this is an [`ErrorKind::Rpc`] error.
300    pub fn rpc_code(&self) -> Option<i32> {
301        match self.kind() {
302            ErrorKind::Rpc { code } => Some(*code),
303            _ => None,
304        }
305    }
306}
307
308impl fmt::Display for Error {
309    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310        match &self.repr {
311            Repr::Simple(kind) => write!(f, "{kind}"),
312            Repr::SimpleMessage(kind, message) if matches!(kind, ErrorKind::Rpc { code: _ }) => {
313                write!(f, "{kind}: {message}")
314            }
315            Repr::SimpleMessage(_, message) => write!(f, "{message}"),
316            Repr::Custom(Custom { kind, error }) if matches!(kind, ErrorKind::Rpc { code: _ }) => {
317                write!(f, "{kind}: {error}")
318            }
319            Repr::Custom(Custom { error, .. }) => write!(f, "{error}"),
320        }
321    }
322}
323
324impl fmt::Debug for Error {
325    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326        let mut dbg = f.debug_struct("Error");
327        dbg.field("context", &self.repr);
328        if let Some(backtrace) = &self.backtrace {
329            return dbg.field("backtrace", backtrace).finish();
330        }
331        dbg.finish_non_exhaustive()
332    }
333}
334
335impl std::error::Error for Error {
336    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
337        match &self.repr {
338            Repr::Custom(Custom { error, .. }) => Some(&**error),
339            _ => None,
340        }
341    }
342}
343
344impl From<ErrorKind> for Error {
345    fn from(kind: ErrorKind) -> Self {
346        Self {
347            repr: Repr::Simple(kind),
348            backtrace: capture_backtrace(),
349        }
350    }
351}
352
353impl From<ProtocolErrorKind> for Error {
354    fn from(kind: ProtocolErrorKind) -> Self {
355        Self::from(ErrorKind::Protocol(kind))
356    }
357}
358
359impl From<SessionErrorKind> for Error {
360    fn from(kind: SessionErrorKind) -> Self {
361        Self::from(ErrorKind::Session(kind))
362    }
363}
364
365impl From<std::io::Error> for Error {
366    fn from(error: std::io::Error) -> Self {
367        Self::new(ErrorKind::Io, error)
368    }
369}
370
371impl From<serde_json::Error> for Error {
372    fn from(error: serde_json::Error) -> Self {
373        Self::new(ErrorKind::Json, error)
374    }
375}
376
377#[inline(always)]
378fn capture_backtrace() -> Option<Box<Backtrace>> {
379    let backtrace = Backtrace::capture();
380    if backtrace.status() == BacktraceStatus::Captured {
381        Some(Box::new(backtrace))
382    } else {
383        None
384    }
385}
386
387/// Aggregate of errors collected during [`crate::Client::stop`].
388///
389/// `Client::stop` performs cooperative shutdown across every active
390/// session before killing the CLI child process. Errors from any
391/// per-session `session.destroy` RPC and from the terminal child-kill
392/// step are collected here rather than short-circuiting on the first
393/// failure, so callers see the full picture of what went wrong during
394/// teardown.
395///
396/// Implements [`std::error::Error`] and forwards to `Display` for the
397/// first error, with a count suffix when there are more.
398#[derive(Debug)]
399pub struct StopErrors(pub(crate) Vec<Error>);
400
401impl StopErrors {
402    /// Borrow the collected errors as a slice, in the order they
403    /// occurred (per-session destroys first, then child-kill last).
404    pub fn errors(&self) -> &[Error] {
405        &self.0
406    }
407
408    /// Consume the aggregate and return the underlying error vector.
409    pub fn into_errors(self) -> Vec<Error> {
410        self.0
411    }
412}
413
414impl fmt::Display for StopErrors {
415    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
416        match self.0.as_slice() {
417            [] => write!(f, "stop completed with no errors"),
418            [only] => write!(f, "stop failed: {only}"),
419            [first, rest @ ..] => write!(
420                f,
421                "stop failed with {n} errors; first: {first}",
422                n = 1 + rest.len(),
423            ),
424        }
425    }
426}
427
428impl std::error::Error for StopErrors {
429    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
430        self.0
431            .first()
432            .map(|e| e as &(dyn std::error::Error + 'static))
433    }
434}