1use std::collections::BTreeMap;
27
28use serde_json::Value;
29
30use crate::constants::DEFAULT_SURFACE_ID;
31use crate::message::{AgentMessage, AgentPayload, Component};
32use crate::toolkit::envelope::{is_operations_envelope, unwrap_operations_envelope};
33use crate::toolkit::parser::{has_a2ui_parts, unwrap_response};
34
35#[derive(Debug, Clone, Default, PartialEq)]
40pub struct HistoryMessage {
41 pub role: String,
43 pub content: String,
45 pub data: Option<Value>,
48}
49
50impl HistoryMessage {
51 pub fn text(role: impl Into<String>, content: impl Into<String>) -> Self {
53 Self {
54 role: role.into(),
55 content: content.into(),
56 data: None,
57 }
58 }
59
60 pub fn data(role: impl Into<String>, data: Value) -> Self {
62 Self {
63 role: role.into(),
64 content: String::new(),
65 data: Some(data),
66 }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq)]
72pub struct PriorSurface {
73 pub surface_id: String,
75 pub catalog_id: Option<String>,
78 pub components: Vec<Component>,
81 pub data_model: Value,
83 pub deleted: bool,
85}
86
87impl PriorSurface {
88 pub fn component(&self, id: &str) -> Option<&Component> {
90 self.components.iter().find(|c| c.id == id)
91 }
92}
93
94pub fn find_prior_surface(messages: &[HistoryMessage]) -> Option<PriorSurface> {
102 find_prior_surface_by_id(messages, None)
103}
104
105pub fn find_prior_surface_by_id(
110 messages: &[HistoryMessage],
111 surface_id: Option<&str>,
112) -> Option<PriorSurface> {
113 let per_message: Vec<Vec<AgentMessage>> = messages.iter().map(extract_operations).collect();
114
115 let target = match surface_id {
117 Some(id) => id.to_string(),
118 None => per_message
119 .iter()
120 .rev()
121 .flat_map(|ops| ops.iter().rev())
122 .find_map(|op| op.surface_id().map(str::to_string))?,
123 };
124
125 let mut components: BTreeMap<String, (usize, Component)> = BTreeMap::new();
126 let mut order = 0usize;
127 let mut data_model = Value::Null;
128 let mut catalog_id = None;
129 let mut deleted = false;
130 let mut seen = false;
131
132 for op in per_message.iter().flatten() {
134 if op.surface_id() != Some(target.as_str()) {
135 continue;
136 }
137 seen = true;
138 match &op.payload {
139 AgentPayload::CreateSurface(create) => {
140 catalog_id = Some(create.catalog_id.clone());
141 components.clear();
143 data_model = Value::Null;
144 deleted = false;
145 }
146 AgentPayload::UpdateComponents(update) => {
147 deleted = false;
148 for component in &update.components {
149 match components.get_mut(&component.id) {
150 Some((_, existing)) => *existing = component.clone(),
153 None => {
154 components.insert(component.id.clone(), (order, component.clone()));
155 order += 1;
156 }
157 }
158 }
159 }
160 AgentPayload::UpdateDataModel(update) => {
161 let _ = update.apply(&mut data_model);
162 }
163 AgentPayload::DeleteSurface(_) => {
164 deleted = true;
165 components.clear();
166 data_model = Value::Null;
167 }
168 _ => {}
169 }
170 }
171
172 if !seen {
173 return None;
174 }
175
176 let mut ordered: Vec<(usize, Component)> = components.into_values().collect();
177 ordered.sort_by_key(|(position, _)| *position);
178
179 Some(PriorSurface {
180 surface_id: target,
181 catalog_id,
182 components: ordered.into_iter().map(|(_, c)| c).collect(),
183 data_model,
184 deleted,
185 })
186}
187
188pub fn next_surface_id(messages: &[HistoryMessage], base: &str) -> String {
193 let base = if base.is_empty() {
194 DEFAULT_SURFACE_ID
195 } else {
196 base
197 };
198 let used: Vec<String> = messages
199 .iter()
200 .flat_map(extract_operations)
201 .filter_map(|op| op.surface_id().map(str::to_string))
202 .collect();
203 if !used.iter().any(|id| id == base) {
204 return base.to_string();
205 }
206 (2..)
207 .map(|n| format!("{base}-{n}"))
208 .find(|candidate| !used.contains(candidate))
209 .unwrap_or_else(|| format!("{base}-{}", used.len() + 1))
210}
211
212fn extract_operations(message: &HistoryMessage) -> Vec<AgentMessage> {
214 let mut out = Vec::new();
215
216 if let Some(data) = &message.data {
217 collect_from_value(data, &mut out);
218 }
219
220 let content = message.content.trim();
221 if content.is_empty() {
222 return out;
223 }
224 if let Ok(value) = serde_json::from_str::<Value>(content) {
225 collect_from_value(&value, &mut out);
226 }
227 if has_a2ui_parts(content) {
228 if let Ok(parts) = unwrap_response(content) {
229 for part in parts {
230 let Some(raw) = part.raw else { continue };
231 if let Ok(value) = serde_json::from_str::<Value>(&raw) {
232 collect_from_value(&value, &mut out);
233 }
234 }
235 }
236 }
237 out
238}
239
240fn collect_from_value(value: &Value, out: &mut Vec<AgentMessage>) {
241 if value.get("error").is_some() {
248 return;
249 }
250 if is_operations_envelope(value) {
251 if let Ok(operations) = unwrap_operations_envelope(value) {
252 out.extend(operations);
253 }
254 return;
255 }
256 match value {
257 Value::Array(items) => {
258 for item in items {
259 if let Ok(message) = serde_json::from_value::<AgentMessage>(item.clone()) {
260 out.push(message);
261 }
262 }
263 }
264 Value::Object(_) => {
265 if let Ok(message) = serde_json::from_value::<AgentMessage>(value.clone()) {
266 out.push(message);
267 }
268 }
269 _ => {}
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use crate::toolkit::envelope::{wrap_as_operations_envelope, wrap_error_envelope};
277 use crate::toolkit::ops::{Intent, SurfaceSpec, assemble_ops};
278 use serde_json::json;
279
280 fn rendered(surface: &str, text: &str) -> HistoryMessage {
281 let spec = SurfaceSpec::new(surface)
282 .with_components(vec![
283 Component::new("root", "Column").with("children", json!(["label"])),
284 Component::new("label", "Text").with("text", json!({"path": "/title"})),
285 ])
286 .with_data_model(json!({"title": text}));
287 let envelope = wrap_as_operations_envelope(&assemble_ops(Intent::Create, &spec)).unwrap();
288 HistoryMessage::text("assistant", envelope)
289 }
290
291 #[test]
292 fn a_surface_is_recovered_from_an_operations_envelope() {
293 let history = vec![
294 HistoryMessage::text("user", "show me the cart"),
295 rendered("cart", "Your cart"),
296 ];
297 let prior = find_prior_surface(&history).unwrap();
298 assert_eq!(prior.surface_id, "cart");
299 assert_eq!(
300 prior.catalog_id.as_deref(),
301 Some(crate::constants::BASIC_CATALOG_ID)
302 );
303 assert_eq!(prior.components.len(), 2);
304 assert_eq!(
305 prior.component("label").unwrap().prop("text"),
306 Some(&json!({"path": "/title"}))
307 );
308 assert_eq!(prior.data_model, json!({"title": "Your cart"}));
309 assert!(!prior.deleted);
310 }
311
312 #[test]
313 fn the_newest_surface_wins() {
314 let history = vec![
315 rendered("first", "one"),
316 HistoryMessage::text("user", "now show the other one"),
317 rendered("second", "two"),
318 ];
319 assert_eq!(find_prior_surface(&history).unwrap().surface_id, "second");
320 assert_eq!(
321 find_prior_surface_by_id(&history, Some("first"))
322 .unwrap()
323 .data_model,
324 json!({"title": "one"})
325 );
326 }
327
328 #[test]
329 fn later_updates_are_folded_onto_the_original() {
330 let mut history = vec![rendered("cart", "Your cart")];
331 let update = SurfaceSpec::new("cart")
332 .with_components(vec![
333 Component::new("label", "Text").with("text", json!("Checkout")),
334 Component::new("extra", "Text").with("text", json!("free shipping")),
335 ])
336 .with_data_model(json!("Updated"))
337 .with_data_path("/title");
338 history.push(HistoryMessage::text(
339 "assistant",
340 wrap_as_operations_envelope(&assemble_ops(Intent::Update, &update)).unwrap(),
341 ));
342
343 let prior = find_prior_surface(&history).unwrap();
344 let ids: Vec<&str> = prior.components.iter().map(|c| c.id.as_str()).collect();
346 assert_eq!(ids, vec!["root", "label", "extra"]);
347 assert_eq!(
348 prior.component("label").unwrap().prop("text"),
349 Some(&json!("Checkout"))
350 );
351 assert_eq!(prior.data_model, json!({"title": "Updated"}));
352 assert_eq!(
353 prior.catalog_id.as_deref(),
354 Some(crate::constants::BASIC_CATALOG_ID)
355 );
356 }
357
358 #[test]
359 fn raw_a2ui_json_blocks_are_recognized_too() {
360 let block = format!(
361 "Here you go\n<a2ui-json>{}</a2ui-json>",
362 serde_json::to_string(&assemble_ops(
363 Intent::Create,
364 &SurfaceSpec::new("inline").with_components(vec![
365 Component::new("root", "Text").with("text", json!("x"))
366 ])
367 ))
368 .unwrap()
369 );
370 let prior = find_prior_surface(&[HistoryMessage::text("assistant", block)]).unwrap();
371 assert_eq!(prior.surface_id, "inline");
372 assert_eq!(prior.components.len(), 1);
373 }
374
375 #[test]
376 fn structured_payloads_are_read_before_text() {
377 let envelope: Value = serde_json::from_str(
378 &wrap_as_operations_envelope(&assemble_ops(
379 Intent::Create,
380 &SurfaceSpec::new("tool-result").with_components(vec![
381 Component::new("root", "Text").with("text", json!("x")),
382 ]),
383 ))
384 .unwrap(),
385 )
386 .unwrap();
387 let prior = find_prior_surface(&[HistoryMessage::data("tool", envelope)]).unwrap();
388 assert_eq!(prior.surface_id, "tool-result");
389 }
390
391 #[test]
392 fn a_deleted_surface_is_reported_as_deleted() {
393 let mut history = vec![rendered("cart", "Your cart")];
394 history.push(HistoryMessage::text(
395 "assistant",
396 wrap_as_operations_envelope(&[AgentMessage::delete_surface("cart")]).unwrap(),
397 ));
398 let prior = find_prior_surface(&history).unwrap();
399 assert!(prior.deleted);
400 assert!(prior.components.is_empty());
401 }
402
403 #[test]
404 fn recreating_a_surface_id_starts_it_over() {
405 let history = vec![rendered("cart", "old"), rendered("cart", "new")];
406 let prior = find_prior_surface(&history).unwrap();
407 assert_eq!(prior.data_model, json!({"title": "new"}));
408 assert_eq!(prior.components.len(), 2);
409 }
410
411 #[test]
412 fn a_failed_surface_is_never_read_back_as_a_prior_one() {
413 let failure = wrap_error_envelope("cart", "could not build the surface", &[]).unwrap();
414 let as_text = HistoryMessage::text("assistant", failure.as_str());
415 let as_data = HistoryMessage::data("tool", serde_json::from_str(&failure).unwrap());
416
417 assert!(find_prior_surface(std::slice::from_ref(&as_text)).is_none());
420 assert!(find_prior_surface(&[as_data]).is_none());
421 assert!(find_prior_surface_by_id(std::slice::from_ref(&as_text), Some("cart")).is_none());
422
423 let rendered_only = vec![rendered("cart", "Your cart")];
426 let then_failed = vec![rendered("cart", "Your cart"), as_text];
427 assert_eq!(
428 find_prior_surface(&then_failed),
429 find_prior_surface(&rendered_only)
430 );
431 assert_eq!(next_surface_id(&then_failed, "cart"), "cart-2");
432 }
433
434 #[test]
435 fn a_failure_that_also_carries_operations_is_still_not_a_surface() {
436 let mut failed = serde_json::from_str::<Value>(
442 &wrap_as_operations_envelope(&crate::toolkit::ops::assemble_ops(
443 Intent::Create,
444 &SurfaceSpec::new("cart").with_components(vec![
445 Component::new("root", "Text").with("text", json!("x")),
446 ]),
447 ))
448 .unwrap(),
449 )
450 .unwrap();
451
452 for error in [json!("could not build the surface"), json!({"code": "X"})] {
453 failed["error"] = error;
454 let as_data = HistoryMessage::data("tool", failed.clone());
455 let as_text = HistoryMessage::text("assistant", failed.to_string());
456 assert!(find_prior_surface(&[as_data]).is_none(), "{failed}");
457 assert!(find_prior_surface(&[as_text]).is_none(), "{failed}");
458 }
459 }
460
461 #[test]
462 fn history_without_a2ui_yields_nothing() {
463 let history = vec![
464 HistoryMessage::text("user", "hello"),
465 HistoryMessage::text("assistant", "hi there"),
466 HistoryMessage::text("assistant", r#"{"some": "unrelated json"}"#),
467 ];
468 assert!(find_prior_surface(&history).is_none());
469 assert!(find_prior_surface_by_id(&history, Some("cart")).is_none());
470 }
471
472 #[test]
473 fn next_surface_id_avoids_ids_already_used() {
474 let history = vec![rendered("cart", "one")];
475 assert_eq!(next_surface_id(&history, "cart"), "cart-2");
476 assert_eq!(next_surface_id(&history, "other"), "other");
477 assert_eq!(next_surface_id(&[], ""), DEFAULT_SURFACE_ID);
478 }
479}