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    /// A session-scoped GitHub token provider failed or returned invalid data.
222    GitHubTokenProvider,
223}
224
225impl fmt::Display for ErrorKind {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        match self {
228            ErrorKind::Protocol(k) => write!(f, "{k}"),
229            ErrorKind::Rpc { code } => write!(f, "RPC error {code}"),
230            ErrorKind::Session(k) => write!(f, "{k}"),
231            ErrorKind::Io => write!(f, "I/O error"),
232            ErrorKind::Json => write!(f, "JSON error"),
233            ErrorKind::BinaryNotFound {
234                name,
235                hint: Some(h),
236            } => {
237                write!(f, "binary not found: {name} ({h})")
238            }
239            ErrorKind::BinaryNotFound { name, hint: None } => {
240                write!(f, "binary not found: {name}")
241            }
242            ErrorKind::InvalidConfig => write!(f, "invalid configuration"),
243            ErrorKind::GitHubTokenProvider => write!(f, "GitHub token provider error"),
244        }
245    }
246}
247
248/// Errors returned by the SDK.
249pub struct Error {
250    repr: Repr<ErrorKind>,
251    // Only `Some` when `RUST_BACKTRACE` is set; boxed so the `Some` variant
252    // doesn't inflate `Error` beyond `clippy::result_large_err` limits.
253    backtrace: Option<Box<Backtrace>>,
254}
255
256impl Error {
257    /// Constructs a new `Error` boxing another [`std::error::Error`].
258    pub(crate) fn new<E>(kind: ErrorKind, error: E) -> Self
259    where
260        E: Into<Box<dyn std::error::Error + Send + Sync>>,
261    {
262        Self {
263            repr: Repr::Custom(Custom {
264                kind,
265                error: error.into(),
266            }),
267            backtrace: capture_backtrace(),
268        }
269    }
270
271    /// The [`ErrorKind`] of this `Error`.
272    pub fn kind(&self) -> &ErrorKind {
273        match &self.repr {
274            Repr::Simple(kind)
275            | Repr::SimpleMessage(kind, ..)
276            | Repr::Custom(Custom { kind, .. }) => kind,
277        }
278    }
279
280    /// The message provided when this `Error` was constructed, or `None`.
281    pub fn message(&self) -> Option<&str> {
282        match &self.repr {
283            Repr::SimpleMessage(_, message) => Some(message.borrow()),
284            _ => None,
285        }
286    }
287
288    /// Create an `Error` with a message.
289    #[must_use]
290    pub fn with_message<C>(kind: ErrorKind, message: C) -> Self
291    where
292        C: Into<Cow<'static, str>>,
293    {
294        Self {
295            repr: Repr::SimpleMessage(kind, message.into()),
296            backtrace: capture_backtrace(),
297        }
298    }
299
300    /// Returns `true` if this error indicates the transport is broken — the CLI
301    /// process exited, the connection was lost, or an I/O failure occurred.
302    /// Callers should discard the client and create a fresh one.
303    pub fn is_transport_failure(&self) -> bool {
304        matches!(self.kind(), ErrorKind::Io)
305            || matches!(
306                self.kind(),
307                ErrorKind::Protocol(ProtocolErrorKind::RequestCancelled)
308            )
309    }
310
311    /// Returns the JSON-RPC error code if this is an [`ErrorKind::Rpc`] error.
312    pub fn rpc_code(&self) -> Option<i32> {
313        match self.kind() {
314            ErrorKind::Rpc { code } => Some(*code),
315            _ => None,
316        }
317    }
318}
319
320impl fmt::Display for Error {
321    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322        match &self.repr {
323            Repr::Simple(kind) => write!(f, "{kind}"),
324            Repr::SimpleMessage(kind, message) if matches!(kind, ErrorKind::Rpc { code: _ }) => {
325                write!(f, "{kind}: {message}")
326            }
327            Repr::SimpleMessage(_, message) => write!(f, "{message}"),
328            Repr::Custom(Custom { kind, error }) if matches!(kind, ErrorKind::Rpc { code: _ }) => {
329                write!(f, "{kind}: {error}")
330            }
331            Repr::Custom(Custom { error, .. }) => write!(f, "{error}"),
332        }
333    }
334}
335
336impl fmt::Debug for Error {
337    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338        let mut dbg = f.debug_struct("Error");
339        dbg.field("context", &self.repr);
340        if let Some(backtrace) = &self.backtrace {
341            return dbg.field("backtrace", backtrace).finish();
342        }
343        dbg.finish_non_exhaustive()
344    }
345}
346
347impl std::error::Error for Error {
348    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
349        match &self.repr {
350            Repr::Custom(Custom { error, .. }) => Some(&**error),
351            _ => None,
352        }
353    }
354}
355
356impl From<ErrorKind> for Error {
357    fn from(kind: ErrorKind) -> Self {
358        Self {
359            repr: Repr::Simple(kind),
360            backtrace: capture_backtrace(),
361        }
362    }
363}
364
365impl From<ProtocolErrorKind> for Error {
366    fn from(kind: ProtocolErrorKind) -> Self {
367        Self::from(ErrorKind::Protocol(kind))
368    }
369}
370
371impl From<SessionErrorKind> for Error {
372    fn from(kind: SessionErrorKind) -> Self {
373        Self::from(ErrorKind::Session(kind))
374    }
375}
376
377impl From<std::io::Error> for Error {
378    fn from(error: std::io::Error) -> Self {
379        Self::new(ErrorKind::Io, error)
380    }
381}
382
383impl From<serde_json::Error> for Error {
384    fn from(error: serde_json::Error) -> Self {
385        Self::new(ErrorKind::Json, error)
386    }
387}
388
389#[inline(always)]
390fn capture_backtrace() -> Option<Box<Backtrace>> {
391    let backtrace = Backtrace::capture();
392    if backtrace.status() == BacktraceStatus::Captured {
393        Some(Box::new(backtrace))
394    } else {
395        None
396    }
397}
398
399/// Aggregate of errors collected during [`crate::Client::stop`].
400///
401/// `Client::stop` performs cooperative shutdown across every active
402/// session before killing the CLI child process. Errors from any
403/// per-session `session.destroy` RPC and from the terminal child-kill
404/// step are collected here rather than short-circuiting on the first
405/// failure, so callers see the full picture of what went wrong during
406/// teardown.
407///
408/// Implements [`std::error::Error`] and forwards to `Display` for the
409/// first error, with a count suffix when there are more.
410#[derive(Debug)]
411pub struct StopErrors(pub(crate) Vec<Error>);
412
413impl StopErrors {
414    /// Borrow the collected errors as a slice, in the order they
415    /// occurred (per-session destroys first, then child-kill last).
416    pub fn errors(&self) -> &[Error] {
417        &self.0
418    }
419
420    /// Consume the aggregate and return the underlying error vector.
421    pub fn into_errors(self) -> Vec<Error> {
422        self.0
423    }
424}
425
426impl fmt::Display for StopErrors {
427    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
428        match self.0.as_slice() {
429            [] => write!(f, "stop completed with no errors"),
430            [only] => write!(f, "stop failed: {only}"),
431            [first, rest @ ..] => write!(
432                f,
433                "stop failed with {n} errors; first: {first}",
434                n = 1 + rest.len(),
435            ),
436        }
437    }
438}
439
440impl std::error::Error for StopErrors {
441    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
442        self.0
443            .first()
444            .map(|e| e as &(dyn std::error::Error + 'static))
445    }
446}