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    /// The CLI could not detach the session.
157    DetachFailed,
158}
159
160impl fmt::Display for SessionErrorKind {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        match self {
163            SessionErrorKind::NotFound(id) => write!(f, "session not found: {id}"),
164            SessionErrorKind::AgentError => write!(f, "agent error"),
165            SessionErrorKind::Timeout(d) => write!(f, "timed out after {d:?}"),
166            SessionErrorKind::SendWhileWaiting => {
167                write!(f, "cannot send while send_and_wait is in flight")
168            }
169            SessionErrorKind::EventLoopClosed => {
170                write!(f, "event loop closed before session reached idle")
171            }
172            SessionErrorKind::ElicitationNotSupported => write!(
173                f,
174                "elicitation not supported by host \
175                 \u{2014} check session.capabilities().ui.elicitation first"
176            ),
177            SessionErrorKind::SessionFsProviderRequired => write!(
178                f,
179                "session was created on a client with session_fs configured \
180                 but no SessionFsProvider was supplied"
181            ),
182            SessionErrorKind::InvalidSessionFsConfig => {
183                write!(f, "invalid SessionFsConfig")
184            }
185            SessionErrorKind::SessionIdMismatch {
186                requested,
187                returned,
188            } => write!(
189                f,
190                "CLI returned session ID {returned} after SDK registered {requested}"
191            ),
192            SessionErrorKind::DetachFailed => write!(f, "failed to detach session"),
193        }
194    }
195}
196
197// ── ErrorKind ─────────────────────────────────────────────────────────────────
198
199/// The kind of [`Error`].
200#[derive(Clone, Debug, PartialEq, Eq)]
201#[non_exhaustive]
202pub enum ErrorKind {
203    /// JSON-RPC transport or protocol violation.
204    Protocol(ProtocolErrorKind),
205    /// The CLI returned a JSON-RPC error response.
206    Rpc {
207        /// JSON-RPC error code.
208        code: i32,
209    },
210    /// Session-scoped error (not found, agent error, timeout, etc.).
211    Session(SessionErrorKind),
212    /// I/O error on the stdio transport or during process spawn.
213    Io,
214    /// Failed to serialize or deserialize a JSON-RPC message.
215    Json,
216    /// A required binary was not found on the system.
217    BinaryNotFound {
218        /// Name of the binary.
219        name: String,
220        /// Optional hint for how to resolve the issue.
221        hint: Option<String>,
222    },
223    /// Invalid combination of options or configuration.
224    InvalidConfig,
225    /// A session-scoped GitHub token provider failed or returned invalid data.
226    GitHubTokenProvider,
227}
228
229impl fmt::Display for ErrorKind {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        match self {
232            ErrorKind::Protocol(k) => write!(f, "{k}"),
233            ErrorKind::Rpc { code } => write!(f, "RPC error {code}"),
234            ErrorKind::Session(k) => write!(f, "{k}"),
235            ErrorKind::Io => write!(f, "I/O error"),
236            ErrorKind::Json => write!(f, "JSON error"),
237            ErrorKind::BinaryNotFound {
238                name,
239                hint: Some(h),
240            } => {
241                write!(f, "binary not found: {name} ({h})")
242            }
243            ErrorKind::BinaryNotFound { name, hint: None } => {
244                write!(f, "binary not found: {name}")
245            }
246            ErrorKind::InvalidConfig => write!(f, "invalid configuration"),
247            ErrorKind::GitHubTokenProvider => write!(f, "GitHub token provider error"),
248        }
249    }
250}
251
252/// Errors returned by the SDK.
253pub struct Error {
254    repr: Repr<ErrorKind>,
255    // Only `Some` when `RUST_BACKTRACE` is set; boxed so the `Some` variant
256    // doesn't inflate `Error` beyond `clippy::result_large_err` limits.
257    backtrace: Option<Box<Backtrace>>,
258}
259
260impl Error {
261    /// Constructs a new `Error` boxing another [`std::error::Error`].
262    pub(crate) fn new<E>(kind: ErrorKind, error: E) -> Self
263    where
264        E: Into<Box<dyn std::error::Error + Send + Sync>>,
265    {
266        Self {
267            repr: Repr::Custom(Custom {
268                kind,
269                error: error.into(),
270            }),
271            backtrace: capture_backtrace(),
272        }
273    }
274
275    /// The [`ErrorKind`] of this `Error`.
276    pub fn kind(&self) -> &ErrorKind {
277        match &self.repr {
278            Repr::Simple(kind)
279            | Repr::SimpleMessage(kind, ..)
280            | Repr::Custom(Custom { kind, .. }) => kind,
281        }
282    }
283
284    /// The message provided when this `Error` was constructed, or `None`.
285    pub fn message(&self) -> Option<&str> {
286        match &self.repr {
287            Repr::SimpleMessage(_, message) => Some(message.borrow()),
288            _ => None,
289        }
290    }
291
292    /// Create an `Error` with a message.
293    #[must_use]
294    pub fn with_message<C>(kind: ErrorKind, message: C) -> Self
295    where
296        C: Into<Cow<'static, str>>,
297    {
298        Self {
299            repr: Repr::SimpleMessage(kind, message.into()),
300            backtrace: capture_backtrace(),
301        }
302    }
303
304    /// Returns `true` if this error indicates the transport is broken — the CLI
305    /// process exited, the connection was lost, or an I/O failure occurred.
306    /// Callers should discard the client and create a fresh one.
307    pub fn is_transport_failure(&self) -> bool {
308        matches!(self.kind(), ErrorKind::Io)
309            || matches!(
310                self.kind(),
311                ErrorKind::Protocol(ProtocolErrorKind::RequestCancelled)
312            )
313    }
314
315    /// Returns the JSON-RPC error code if this is an [`ErrorKind::Rpc`] error.
316    pub fn rpc_code(&self) -> Option<i32> {
317        match self.kind() {
318            ErrorKind::Rpc { code } => Some(*code),
319            _ => None,
320        }
321    }
322}
323
324impl fmt::Display for Error {
325    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326        match &self.repr {
327            Repr::Simple(kind) => write!(f, "{kind}"),
328            Repr::SimpleMessage(kind, message) if matches!(kind, ErrorKind::Rpc { code: _ }) => {
329                write!(f, "{kind}: {message}")
330            }
331            Repr::SimpleMessage(_, message) => write!(f, "{message}"),
332            Repr::Custom(Custom { kind, error }) if matches!(kind, ErrorKind::Rpc { code: _ }) => {
333                write!(f, "{kind}: {error}")
334            }
335            Repr::Custom(Custom { error, .. }) => write!(f, "{error}"),
336        }
337    }
338}
339
340impl fmt::Debug for Error {
341    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342        let mut dbg = f.debug_struct("Error");
343        dbg.field("context", &self.repr);
344        if let Some(backtrace) = &self.backtrace {
345            return dbg.field("backtrace", backtrace).finish();
346        }
347        dbg.finish_non_exhaustive()
348    }
349}
350
351impl std::error::Error for Error {
352    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
353        match &self.repr {
354            Repr::Custom(Custom { error, .. }) => Some(&**error),
355            _ => None,
356        }
357    }
358}
359
360impl From<ErrorKind> for Error {
361    fn from(kind: ErrorKind) -> Self {
362        Self {
363            repr: Repr::Simple(kind),
364            backtrace: capture_backtrace(),
365        }
366    }
367}
368
369impl From<ProtocolErrorKind> for Error {
370    fn from(kind: ProtocolErrorKind) -> Self {
371        Self::from(ErrorKind::Protocol(kind))
372    }
373}
374
375impl From<SessionErrorKind> for Error {
376    fn from(kind: SessionErrorKind) -> Self {
377        Self::from(ErrorKind::Session(kind))
378    }
379}
380
381impl From<std::io::Error> for Error {
382    fn from(error: std::io::Error) -> Self {
383        Self::new(ErrorKind::Io, error)
384    }
385}
386
387impl From<serde_json::Error> for Error {
388    fn from(error: serde_json::Error) -> Self {
389        Self::new(ErrorKind::Json, error)
390    }
391}
392
393#[inline(always)]
394fn capture_backtrace() -> Option<Box<Backtrace>> {
395    let backtrace = Backtrace::capture();
396    if backtrace.status() == BacktraceStatus::Captured {
397        Some(Box::new(backtrace))
398    } else {
399        None
400    }
401}
402
403/// Aggregate of errors collected during [`crate::Client::stop`].
404///
405/// `Client::stop` performs cooperative shutdown across every active
406/// session before killing the CLI child process. Errors from any
407/// per-session `session.detach` RPC and from the terminal child-kill
408/// step are collected here rather than short-circuiting on the first
409/// failure, so callers see the full picture of what went wrong during
410/// teardown.
411///
412/// Implements [`std::error::Error`] and forwards to `Display` for the
413/// first error, with a count suffix when there are more.
414#[derive(Debug)]
415pub struct StopErrors(pub(crate) Vec<Error>);
416
417impl StopErrors {
418    /// Borrow the collected errors as a slice, in the order they
419    /// occurred (per-session destroys first, then child-kill last).
420    pub fn errors(&self) -> &[Error] {
421        &self.0
422    }
423
424    /// Consume the aggregate and return the underlying error vector.
425    pub fn into_errors(self) -> Vec<Error> {
426        self.0
427    }
428}
429
430impl fmt::Display for StopErrors {
431    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
432        match self.0.as_slice() {
433            [] => write!(f, "stop completed with no errors"),
434            [only] => write!(f, "stop failed: {only}"),
435            [first, rest @ ..] => write!(
436                f,
437                "stop failed with {n} errors; first: {first}",
438                n = 1 + rest.len(),
439            ),
440        }
441    }
442}
443
444impl std::error::Error for StopErrors {
445    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
446        self.0
447            .first()
448            .map(|e| e as &(dyn std::error::Error + 'static))
449    }
450}