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}