Skip to main content

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}