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