ag_ui_a2ui/toolkit/
envelope.rs1use serde_json::{Map, Value, json};
15
16use crate::constants::A2UI_OPERATIONS_KEY;
17use crate::error::Result;
18use crate::message::AgentMessage;
19use crate::validate::ValidationError;
20
21pub fn wrap_as_operations_envelope(operations: &[AgentMessage]) -> Result<String> {
36 Ok(serde_json::to_string(&operations_envelope(operations)?)?)
37}
38
39pub fn operations_envelope(operations: &[AgentMessage]) -> Result<Value> {
46 let operations = serde_json::to_value(operations)?;
47 let mut envelope = Map::new();
48 envelope.insert(A2UI_OPERATIONS_KEY.to_string(), operations);
49 Ok(Value::Object(envelope))
50}
51
52pub fn wrap_error_envelope(
81 surface_id: &str,
82 message: &str,
83 errors: &[ValidationError],
84) -> Result<String> {
85 let mut envelope = Map::new();
86 envelope.insert("error".to_string(), json!(message));
87 envelope.insert("code".to_string(), json!("VALIDATION_FAILED"));
88 envelope.insert("surfaceId".to_string(), json!(surface_id));
89 envelope.insert(
92 "path".to_string(),
93 json!(errors.first().map_or("components", |e| e.path.as_str())),
94 );
95 envelope.insert("details".to_string(), serde_json::to_value(errors)?);
96 Ok(serde_json::to_string(&Value::Object(envelope))?)
97}
98
99pub fn is_operations_envelope(value: &Value) -> bool {
105 value.get(A2UI_OPERATIONS_KEY).is_some_and(Value::is_array)
106}
107
108pub fn unwrap_operations_envelope(value: &Value) -> Result<Vec<AgentMessage>> {
116 let operations = value.get(A2UI_OPERATIONS_KEY).ok_or_else(|| {
117 crate::Error::parse(format!("payload has no '{A2UI_OPERATIONS_KEY}' key"))
118 })?;
119 Ok(serde_json::from_value(operations.clone())?)
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use crate::message::Component;
126 use crate::validate::ErrorCode;
127
128 fn ops() -> Vec<AgentMessage> {
129 vec![
130 AgentMessage::create_surface("s1", "cat"),
131 AgentMessage::update_components(
132 "s1",
133 vec![Component::new("root", "Text").with("text", json!("hi"))],
134 ),
135 ]
136 }
137
138 #[test]
139 fn the_envelope_round_trips() {
140 let json = wrap_as_operations_envelope(&ops()).unwrap();
141 let value: Value = serde_json::from_str(&json).unwrap();
142 assert!(is_operations_envelope(&value));
143 assert_eq!(unwrap_operations_envelope(&value).unwrap(), ops());
144 }
145
146 #[test]
147 fn the_envelope_key_is_the_frontend_sniff() {
148 let value: Value =
149 serde_json::from_str(&wrap_as_operations_envelope(&ops()).unwrap()).unwrap();
150 assert!(value.get("a2ui_operations").is_some());
151 assert!(!is_operations_envelope(&json!({"operations": []})));
152 assert!(!is_operations_envelope(&json!({"a2ui_operations": "nope"})));
153 }
154
155 #[test]
156 fn an_empty_batch_is_still_a_valid_envelope() {
157 let json = wrap_as_operations_envelope(&[]).unwrap();
158 assert_eq!(json, r#"{"a2ui_operations":[]}"#);
159 }
160
161 fn errors() -> Vec<ValidationError> {
162 vec![
163 ValidationError::new(
164 ErrorCode::NoRoot,
165 "components",
166 "No component has id 'root'.",
167 ),
168 ValidationError::new(
169 ErrorCode::UnresolvedChild,
170 "components[1].child",
171 "'gone' is not defined.",
172 ),
173 ]
174 }
175
176 #[test]
177 fn the_error_payload_is_a_message_with_the_codes_beside_it() {
178 let json = wrap_error_envelope("s1", "could not build the surface", &errors()).unwrap();
179 let value: Value = serde_json::from_str(&json).unwrap();
180
181 assert_eq!(value["error"], "could not build the surface");
182 assert_eq!(value["code"], "VALIDATION_FAILED");
183 assert_eq!(value["surfaceId"], "s1");
184 assert_eq!(value["path"], "components");
185 assert_eq!(value["details"][1]["code"], "unresolved_child");
186 }
187
188 #[test]
189 fn a_failed_surface_does_not_satisfy_the_frontend_sniff() {
190 let json = wrap_error_envelope("s1", "could not build the surface", &errors()).unwrap();
191 let value: Value = serde_json::from_str(&json).unwrap();
192
193 assert!(!is_operations_envelope(&value), "{value}");
196 assert!(value.get(A2UI_OPERATIONS_KEY).is_none(), "{value}");
197 assert!(unwrap_operations_envelope(&value).is_err());
198 }
199
200 #[test]
201 fn an_error_payload_with_no_errors_still_has_a_path() {
202 let json = wrap_error_envelope("s1", "model returned nothing", &[]).unwrap();
203 let value: Value = serde_json::from_str(&json).unwrap();
204 assert_eq!(value["path"], "components");
205 }
206
207 #[test]
208 fn unwrapping_a_non_envelope_is_an_error() {
209 assert!(unwrap_operations_envelope(&json!({"nope": []})).is_err());
210 }
211}