ag_ui_a2ui/error.rs
1//! Error types for the A2UI protocol layer.
2
3use std::fmt;
4
5/// Convenient alias for fallible A2UI operations.
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// Everything that can go wrong while producing, validating, or transporting
9/// A2UI.
10///
11/// The variants are deliberately coarse: A2UI is a wire protocol, so the useful
12/// detail almost always lives in the payload (a [`crate::validate::ValidationError`]
13/// list, a JSON Pointer, the offending literal) rather than in the discriminant.
14#[derive(Debug, thiserror::Error)]
15#[non_exhaustive]
16pub enum Error {
17 /// The bytes were not JSON, or were not JSON of the expected shape.
18 #[error("invalid A2UI JSON: {0}")]
19 Json(#[from] serde_json::Error),
20
21 /// An LLM response could not be split into conversational text and A2UI
22 /// blocks, or a block did not contain usable JSON.
23 #[error("A2UI parse error: {0}")]
24 Parse(String),
25
26 /// A syntactically well-formed payload that violates A2UI semantics.
27 ///
28 /// Carries the full machine-readable error list so a caller can feed it
29 #[cfg_attr(
30 feature = "toolkit",
31 doc = "straight back to a model; see [`crate::toolkit::recovery`]."
32 )]
33 #[cfg_attr(
34 not(feature = "toolkit"),
35 doc = "straight back to a model; see `toolkit::recovery`, behind the `toolkit` feature."
36 )]
37 #[error("A2UI validation failed with {} error(s):\n{errors}", errors.len())]
38 Validation {
39 /// The full machine-readable error list.
40 errors: ValidationErrors,
41 },
42
43 /// A JSON Pointer was malformed, or resolved to a location that cannot be
44 /// written (for example indexing past the end of an array).
45 #[error("invalid data-model pointer {pointer:?}: {reason}")]
46 Pointer {
47 /// The offending pointer, exactly as it appeared on the wire.
48 pointer: String,
49 /// Why it could not be resolved.
50 reason: String,
51 },
52
53 /// A `${...}` expression in a `formatString` template could not be parsed
54 /// or evaluated.
55 #[error("invalid binding expression {expression:?}: {reason}")]
56 Binding {
57 /// The expression body, without the surrounding `${` and `}`.
58 expression: String,
59 /// Why it could not be evaluated.
60 reason: String,
61 },
62
63 /// A catalog document could not be interpreted as an A2UI catalog.
64 #[error("invalid catalog: {0}")]
65 Catalog(String),
66
67 /// A model failed to produce a valid surface within
68 /// [`MAX_A2UI_ATTEMPTS`](crate::constants::MAX_A2UI_ATTEMPTS) attempts.
69 #[error("A2UI generation gave up after {attempts} attempt(s); last errors: {last}")]
70 RecoveryExhausted {
71 /// How many generation attempts were made.
72 attempts: u32,
73 /// The validation errors from the final attempt.
74 last: ValidationErrors,
75 },
76}
77
78impl Error {
79 /// Builds a [`Error::Parse`] from anything printable.
80 pub fn parse(reason: impl fmt::Display) -> Self {
81 Self::Parse(reason.to_string())
82 }
83
84 /// Builds a [`Error::Catalog`] from anything printable.
85 pub fn catalog(reason: impl fmt::Display) -> Self {
86 Self::Catalog(reason.to_string())
87 }
88
89 /// Builds a [`Error::Pointer`] for a pointer that could not be resolved.
90 pub fn pointer(pointer: impl Into<String>, reason: impl fmt::Display) -> Self {
91 Self::Pointer {
92 pointer: pointer.into(),
93 reason: reason.to_string(),
94 }
95 }
96
97 /// Builds a [`Error::Binding`] for an expression that could not be evaluated.
98 pub fn binding(expression: impl Into<String>, reason: impl fmt::Display) -> Self {
99 Self::Binding {
100 expression: expression.into(),
101 reason: reason.to_string(),
102 }
103 }
104}
105
106/// A list of semantic validation errors, rendered one per line.
107///
108/// Newtype rather than a bare `Vec` so that [`Error::Validation`] can carry a
109/// `Display` impl an LLM can read directly.
110#[derive(Debug, Clone, Default, PartialEq, Eq)]
111pub struct ValidationErrors(pub Vec<crate::validate::ValidationError>);
112
113impl ValidationErrors {
114 /// Number of errors in the list.
115 pub fn len(&self) -> usize {
116 self.0.len()
117 }
118
119 /// Whether the list is empty (i.e. the payload validated cleanly).
120 pub fn is_empty(&self) -> bool {
121 self.0.is_empty()
122 }
123
124 /// Borrows the underlying errors.
125 pub fn as_slice(&self) -> &[crate::validate::ValidationError] {
126 &self.0
127 }
128}
129
130impl From<Vec<crate::validate::ValidationError>> for ValidationErrors {
131 fn from(errors: Vec<crate::validate::ValidationError>) -> Self {
132 Self(errors)
133 }
134}
135
136impl fmt::Display for ValidationErrors {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 for (i, error) in self.0.iter().enumerate() {
139 if i > 0 {
140 f.write_str("\n")?;
141 }
142 write!(f, "{error}")?;
143 }
144 Ok(())
145 }
146}