1use ag_ui::{Message, Tool};
17use serde_json::Value;
18
19use crate::toolkit::history::{HistoryMessage, PriorSurface, find_prior_surface};
20use crate::toolkit::tools::ToolDefinition;
21
22impl From<&Message> for HistoryMessage {
28 fn from(message: &Message) -> Self {
29 let role = message.role().as_str();
30 match message {
31 Message::Developer(m) => Self::text(role, m.content.clone()),
32 Message::System(m) => Self::text(role, m.content.clone()),
33 Message::Assistant(m) => Self::text(role, m.content.clone().unwrap_or_default()),
34 Message::User(m) => Self::text(role, m.content.to_text()),
35 Message::Tool(m) => Self::text(role, m.content.clone()),
36 Message::Reasoning(m) => Self::text(role, m.content.clone()),
37 Message::Activity(m) => Self::data(role, Value::Object(m.content.clone())),
38 }
39 }
40}
41
42impl From<ToolDefinition> for Tool {
44 fn from(definition: ToolDefinition) -> Self {
45 Self::new(
46 definition.name,
47 definition.description,
48 definition.parameters,
49 )
50 }
51}
52
53pub fn find_prior_surface_in(messages: &[Message]) -> Option<PriorSurface> {
86 let history: Vec<HistoryMessage> = messages.iter().map(HistoryMessage::from).collect();
87 find_prior_surface(&history)
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use crate::toolkit::envelope::wrap_as_operations_envelope;
94 use crate::toolkit::tools::{generate_a2ui_tool, render_a2ui_tool};
95 use crate::{AgentMessage, Component};
96 use ag_ui::{ActivityMessage, InputContent, JsonObject, UserContent};
97 use serde_json::json;
98
99 #[test]
100 fn every_role_maps_to_the_field_the_scan_reads() {
101 let mut activity = JsonObject::new();
102 activity.insert("step".into(), json!("searching"));
103
104 let cases: Vec<(Message, &str, &str, bool)> = vec![
105 (
106 Message::system("m", "be brief"),
107 "system",
108 "be brief",
109 false,
110 ),
111 (
112 Message::developer("m", "internal"),
113 "developer",
114 "internal",
115 false,
116 ),
117 (
118 Message::assistant("m", "on it"),
119 "assistant",
120 "on it",
121 false,
122 ),
123 (Message::user("m", "hello"), "user", "hello", false),
124 (
125 Message::tool("m", "call-1", "{\"ok\":true}"),
126 "tool",
127 "{\"ok\":true}",
128 false,
129 ),
130 (
131 Message::Activity(ActivityMessage {
132 id: "m".into(),
133 activity_type: "web_search".into(),
134 content: activity,
135 ..Default::default()
136 }),
137 "activity",
138 "",
139 true,
140 ),
141 ];
142
143 for (message, role, content, has_data) in cases {
144 let entry = HistoryMessage::from(&message);
145 assert_eq!(entry.role, role);
146 assert_eq!(entry.content, content, "content of {role}");
147 assert_eq!(entry.data.is_some(), has_data, "data of {role}");
148 }
149 }
150
151 #[test]
152 fn a_multimodal_turn_contributes_its_text() {
153 let message = Message::user(
154 "m",
155 UserContent::from(vec![
156 InputContent::text("look at this"),
157 InputContent::text("and this"),
158 ]),
159 );
160 assert_eq!(
161 HistoryMessage::from(&message).content,
162 "look at this\nand this"
163 );
164 }
165
166 #[test]
167 fn a_thread_with_no_a2ui_recovers_nothing() {
168 let thread = [
169 Message::user("m-1", "hello"),
170 Message::assistant("m-2", "hi there"),
171 ];
172 assert!(find_prior_surface_in(&thread).is_none());
173 }
174
175 #[test]
176 fn a_deleted_surface_is_recovered_as_deleted() {
177 let envelope = wrap_as_operations_envelope(&[
178 AgentMessage::create_surface("board", "basic"),
179 AgentMessage::update_components(
180 "board",
181 vec![Component::new("root", "Text").with("text", json!("hi"))],
182 ),
183 AgentMessage::delete_surface("board"),
184 ])
185 .expect("operations serialize");
186
187 let thread = [Message::tool("m-1", "call-1", envelope)];
188 let prior = find_prior_surface_in(&thread).expect("the surface was rendered");
189 assert!(prior.deleted);
190 }
191
192 #[test]
193 fn both_toolkit_tools_convert_into_offerable_tools() {
194 for definition in [generate_a2ui_tool(), render_a2ui_tool(None)] {
195 let name = definition.name;
196 let tool = Tool::from(definition);
197 assert_eq!(tool.name, name);
198 assert!(!tool.description.is_empty());
199 assert_eq!(tool.parameters["type"], "object");
200 }
201 }
202}