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