Skip to main content

ag_ui/client/
error.rs

1//! Errors this crate can produce.
2
3use thiserror::Error;
4
5/// Everything that can go wrong while consuming an AG-UI stream.
6///
7/// The variant list is `#[non_exhaustive]`: new transports and validation rules
8/// are expected to add variants without a breaking release.
9#[derive(Debug, Error)]
10#[non_exhaustive]
11pub enum Error {
12    /// A frame's payload was not valid JSON, or not a valid [`Event`].
13    ///
14    /// [`Event`]: https://kimsoungryoul.github.io/ag-ui-rust/api/ag_ui/event/enum.Event.html
15    #[error("JSON error: {0}")]
16    Json(#[from] serde_json::Error),
17
18    /// An error raised by the core protocol types.
19    ///
20    /// Core's *protocol* violations do not arrive here: they are flattened into
21    /// [`Error::Protocol`] so that every rule a stream can break has one shape.
22    #[error(transparent)]
23    Core(crate::Error),
24
25    /// The bytes were not well-formed `text/event-stream`.
26    #[error("SSE decode error: {0}")]
27    Decode(String),
28
29    /// The stream parsed, but broke a rule the protocol requires — a content
30    /// event with no open message, a chunk with no id to attach to, events
31    /// after the run finished.
32    #[error("protocol violation: {0}")]
33    Protocol(String),
34
35    /// An RFC 6902 patch could not be applied.
36    ///
37    /// The target document is left exactly as it was: [`json_patch::patch`]
38    /// rolls back the operations it had already applied, so a rejected patch
39    /// never leaves half-mutated state behind.
40    #[error("{target} patch failed: {message}")]
41    Patch {
42        /// What the patch targeted — `"state"`, or `"activity <message id>"`.
43        target: String,
44        /// Why it was rejected, as reported by the patch engine.
45        message: String,
46    },
47
48    /// The application state did not deserialize into the caller's type.
49    ///
50    /// The raw JSON state is still updated and correct; only the typed view is
51    /// unavailable.
52    #[error("state does not match the expected type: {0}")]
53    State(#[source] serde_json::Error),
54
55    /// The agent reported `RUN_ERROR`.
56    #[error("run failed: {message}")]
57    Run {
58        /// What went wrong, for a human.
59        message: String,
60        /// The machine-readable code, when the agent sent one.
61        code: Option<String>,
62    },
63
64    /// The server answered with a status outside 2xx.
65    #[error("HTTP {status}: {body}")]
66    Http {
67        /// The status code.
68        status: u16,
69        /// The response body, truncated to a readable length.
70        body: String,
71    },
72
73    /// The transport failed — a connection reset, a DNS failure, a closed
74    /// channel. Carries the underlying error.
75    #[error("transport error: {0}")]
76    Transport(#[source] Box<dyn std::error::Error + Send + Sync>),
77
78    /// A transport was configured with something it cannot use: an unparseable
79    /// URL, an invalid header name.
80    #[error("invalid client configuration: {0}")]
81    Config(String),
82}
83
84impl From<crate::Error> for Error {
85    fn from(error: crate::Error) -> Self {
86        match error {
87            // A rule broken by the stream is a protocol violation whichever
88            // crate noticed it; a caller matching on `Protocol` should not have
89            // to know that `RunOutcome::validate` lives in core.
90            crate::Error::Protocol(message) => Self::Protocol(message),
91            other => Self::Core(other),
92        }
93    }
94}
95
96impl Error {
97    /// Wraps any error as a [`Error::Transport`].
98    ///
99    /// Transports for exotic runtimes have their own error types; this is how
100    /// they enter this crate's error enum without a variant of their own.
101    pub fn transport(error: impl std::error::Error + Send + Sync + 'static) -> Self {
102        Self::Transport(Box::new(error))
103    }
104
105    /// Builds a [`Error::Protocol`] from anything printable.
106    pub(crate) fn protocol(message: impl Into<String>) -> Self {
107        Self::Protocol(message.into())
108    }
109
110    /// Builds a [`Error::Decode`] from anything printable.
111    pub(crate) fn decode(message: impl Into<String>) -> Self {
112        Self::Decode(message.into())
113    }
114}
115
116/// Result alias used throughout this crate.
117pub type Result<T, E = Error> = core::result::Result<T, E>;