Skip to main content

ag_ui/server/
error.rs

1//! Errors raised while hosting a run.
2//!
3//! Everything an agent can fail with funnels through [`enum@Error`]. The run driver
4//! turns whatever escapes [`Agent::run`](crate::server::Agent::run) into a `RUN_ERROR`
5//! event, so an error is never a panic and never a silently truncated stream.
6
7use std::fmt;
8
9use crate::EventType;
10use thiserror::Error;
11
12/// Result alias used throughout this crate.
13pub type Result<T, E = Error> = core::result::Result<T, E>;
14
15/// Everything that can go wrong while hosting an AG-UI run.
16///
17/// The variant list is `#[non_exhaustive]`: new emitters and verification rules
18/// are expected to add variants without a breaking release.
19#[derive(Debug, Error)]
20#[non_exhaustive]
21pub enum Error {
22    /// A core protocol type rejected a value — for example an `interrupt`
23    /// outcome carrying no interrupts.
24    #[error(transparent)]
25    Protocol(#[from] crate::Error),
26
27    /// State, tool arguments or a tool result could not be converted to or from
28    /// JSON.
29    #[error("JSON conversion failed: {0}")]
30    Json(#[from] serde_json::Error),
31
32    /// The emitted event stream broke the protocol's ordering rules.
33    ///
34    /// Only produced when the `verify` feature is enabled (it is by default).
35    #[error(transparent)]
36    Verification(#[from] VerificationError),
37
38    /// The run was cancelled — usually because the client disconnected.
39    ///
40    /// Every emit after cancellation fails with this, so an agent that uses `?`
41    /// on its emits unwinds promptly without any cancellation code of its own.
42    #[error("the run was cancelled")]
43    Cancelled,
44
45    /// The consumer dropped the event stream, so there is nowhere left to emit.
46    #[error("the event stream was dropped by the consumer")]
47    Disconnected,
48
49    /// The agent itself failed. Build one with [`Error::agent`].
50    #[error("agent error: {0}")]
51    Agent(Box<dyn std::error::Error + Send + Sync>),
52}
53
54impl Error {
55    /// Wraps an arbitrary agent-side error.
56    ///
57    /// ```
58    /// # use ag_ui::server::Error;
59    /// let err = Error::agent("the weather service is down");
60    /// assert_eq!(err.code(), "AGENT_ERROR");
61    /// ```
62    pub fn agent<E>(error: E) -> Self
63    where
64        E: Into<Box<dyn std::error::Error + Send + Sync>>,
65    {
66        Self::Agent(error.into())
67    }
68
69    /// The machine-readable code placed on the `RUN_ERROR` event.
70    pub const fn code(&self) -> &'static str {
71        match self {
72            Self::Protocol(_) => "PROTOCOL",
73            Self::Json(_) => "SERIALIZATION",
74            Self::Verification(_) => "PROTOCOL_VIOLATION",
75            Self::Cancelled => "CANCELLED",
76            Self::Disconnected => "DISCONNECTED",
77            Self::Agent(_) => "AGENT_ERROR",
78        }
79    }
80
81    /// Whether this error means the run was cancelled rather than that it
82    /// failed.
83    pub const fn is_cancelled(&self) -> bool {
84        matches!(self, Self::Cancelled)
85    }
86
87    /// Whether the consumer has gone away, making further emits pointless.
88    pub const fn is_disconnected(&self) -> bool {
89        matches!(self, Self::Disconnected)
90    }
91}
92
93/// The ordering rule an event broke.
94///
95/// Each variant is one check in the state machine described on
96/// [`verify`](crate::server::verify).
97#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
98#[non_exhaustive]
99pub enum Rule {
100    /// No event may follow `RUN_FINISHED` or `RUN_ERROR`.
101    RunEnded,
102    /// A run starts exactly once.
103    DuplicateRunStarted,
104    /// A message, tool call, reasoning block or step was opened twice under the
105    /// same id.
106    DuplicateStart,
107    /// Content or a terminator arrived for something that is not open.
108    NotOpen,
109    /// An event referenced an id that was never introduced.
110    UnknownId,
111    /// `RUN_FINISHED` arrived while something was still open.
112    OpenAtFinish,
113    /// The event is legal but arrived in the wrong place — a tool result before
114    /// its `TOOL_CALL_END`, for instance.
115    OutOfOrder,
116    /// A continuation, terminator or re-open carried a `subagentRunId` that
117    /// disagrees with the subagent that opened the entity — or a tool call was
118    /// tagged with one subagent while its parent message belongs to another.
119    OwnerMismatch,
120}
121
122impl Rule {
123    /// The rule's kebab-case name, as it appears in error messages.
124    pub const fn as_str(&self) -> &'static str {
125        match self {
126            Self::RunEnded => "run-ended",
127            Self::DuplicateRunStarted => "duplicate-run-started",
128            Self::DuplicateStart => "duplicate-start",
129            Self::NotOpen => "not-open",
130            Self::UnknownId => "unknown-id",
131            Self::OpenAtFinish => "open-at-finish",
132            Self::OutOfOrder => "out-of-order",
133            Self::OwnerMismatch => "owner-mismatch",
134        }
135    }
136
137    /// The rule in one sentence, for humans reading a log.
138    pub const fn describe(&self) -> &'static str {
139        match self {
140            Self::RunEnded => "a run emits nothing after RUN_FINISHED or RUN_ERROR",
141            Self::DuplicateRunStarted => "a run emits RUN_STARTED exactly once",
142            Self::DuplicateStart => "an id may only be opened once",
143            Self::NotOpen => "content and terminators require a matching start",
144            Self::UnknownId => "an event may only reference an id it has seen",
145            Self::OpenAtFinish => "everything opened must be closed before RUN_FINISHED",
146            Self::OutOfOrder => "the event arrived before the event it depends on",
147            Self::OwnerMismatch => "an event names the subagent that opened its entity",
148        }
149    }
150}
151
152impl fmt::Display for Rule {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        f.write_str(self.as_str())
155    }
156}
157
158/// One event rejected by the ordering verifier.
159///
160/// The message names the offending event, the rule it broke and the id
161/// involved. In debug builds it also carries a dump of everything still open,
162/// which is usually enough to spot the missing terminator:
163///
164/// ```text
165/// TEXT_MESSAGE_CONTENT breaks rule `not-open` (content and terminators require
166/// a matching start): message "msg-2" is not open [open: messages={"msg-1"}]
167/// ```
168#[derive(Debug, Error)]
169#[error("{event} breaks rule `{rule}` ({}): {detail}", rule.describe())]
170pub struct VerificationError {
171    /// The event that was rejected.
172    pub event: EventType,
173    /// The rule it broke.
174    pub rule: Rule,
175    /// What specifically was wrong, plus — in debug builds — the open-entity
176    /// dump.
177    pub detail: String,
178}
179
180impl VerificationError {
181    /// Builds a rejection.
182    pub fn new(event: EventType, rule: Rule, detail: impl Into<String>) -> Self {
183        Self {
184            event,
185            rule,
186            detail: detail.into(),
187        }
188    }
189}