ag_ui_a2ui/toolkit/
tools.rs1use serde_json::{Value, json};
16
17use crate::catalog::Catalog;
18use crate::constants::{
19 DEFAULT_SURFACE_ID, GENERATE_A2UI_TOOL_NAME, RENDER_A2UI_TOOL_NAME, ROOT_ID,
20};
21
22#[derive(Debug, Clone, PartialEq)]
27pub struct ToolDefinition {
28 pub name: &'static str,
30 pub description: String,
32 pub parameters: Value,
34}
35
36impl ToolDefinition {
37 pub fn to_anthropic_value(&self) -> Value {
47 json!({
48 "name": self.name,
49 "description": self.description,
50 "input_schema": self.parameters,
51 })
52 }
53}
54
55pub fn generate_a2ui_tool() -> ToolDefinition {
62 ToolDefinition {
63 name: GENERATE_A2UI_TOOL_NAME,
64 description: "Render an interactive UI surface for the user. Use this instead of \
65 describing a UI in prose. Set intent to 'create' for a new surface, or \
66 'update' to change a surface already on screen — updating keeps the \
67 existing surface_id and never re-creates it."
68 .to_string(),
69 parameters: json!({
70 "type": "object",
71 "properties": {
72 "intent": {
73 "type": "string",
74 "enum": ["create", "update"],
75 "description": "'create' builds a new surface; 'update' edits the surface \
76 already on screen. Re-creating an existing surface is an \
77 error, so use 'update' whenever one exists.",
78 "default": "create"
79 },
80 "surface_id": {
81 "type": "string",
82 "description": format!(
83 "The surface to target. Required for 'update'; for 'create' it must be \
84 unused. Defaults to '{DEFAULT_SURFACE_ID}'."
85 )
86 },
87 "request": {
88 "type": "string",
89 "description": "What the UI should show or let the user do, in plain \
90 language. Include the concrete data to display."
91 },
92 "design_notes": {
93 "type": "string",
94 "description": "Optional layout or styling guidance, e.g. 'compact list, \
95 primary action at the bottom'."
96 }
97 },
98 "required": ["intent", "request"]
99 }),
100 }
101}
102
103pub fn render_a2ui_tool(catalog: Option<&Catalog>) -> ToolDefinition {
112 let component_type = match catalog {
113 Some(catalog) if !catalog.components.is_empty() => json!({
114 "type": "string",
115 "description": "The component type, from the catalog.",
116 "enum": catalog
117 .components_in_order()
118 .map(|d| d.name.clone())
119 .collect::<Vec<_>>()
120 }),
121 _ => json!({
122 "type": "string",
123 "description": "The component type, e.g. 'Text' or 'Column'."
124 }),
125 };
126
127 ToolDefinition {
128 name: RENDER_A2UI_TOOL_NAME,
129 description: format!(
130 "Emit the UI surface as a flat list of A2UI components. Components are never nested: \
131 a parent names its children by id. Exactly one component must have id '{ROOT_ID}', \
132 and it must come first, with every parent listed before its children."
133 ),
134 parameters: json!({
135 "type": "object",
136 "properties": {
137 "components": {
138 "type": "array",
139 "minItems": 1,
140 "description": format!(
141 "The flat component list. The first entry must be the '{ROOT_ID}' \
142 component; parents come before their children."
143 ),
144 "items": {
145 "type": "object",
146 "properties": {
147 "id": {
148 "type": "string",
149 "description": format!(
150 "Unique id within this surface. Exactly one component uses \
151 '{ROOT_ID}'."
152 )
153 },
154 "component": component_type
155 },
156 "required": ["id", "component"],
157 "additionalProperties": true
158 }
159 },
160 "data_model": {
161 "type": "object",
162 "description": "Data the components bind to. Every {\"path\": \"/pointer\"} \
163 binding in the components must resolve here.",
164 "additionalProperties": true
165 },
166 "message": {
167 "type": "string",
168 "description": "Optional short sentence to show the user alongside the \
169 surface."
170 }
171 },
172 "required": ["components"]
173 }),
174 }
175}
176
177pub fn tool_definitions(catalog: Option<&Catalog>) -> Vec<ToolDefinition> {
179 vec![generate_a2ui_tool(), render_a2ui_tool(catalog)]
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn the_tool_names_are_the_wire_constants() {
188 assert_eq!(generate_a2ui_tool().name, "generate_a2ui");
189 assert_eq!(render_a2ui_tool(None).name, "render_a2ui");
190 }
191
192 #[test]
193 fn the_planner_tool_forces_an_explicit_intent() {
194 let tool = generate_a2ui_tool();
195 let intent = &tool.parameters["properties"]["intent"];
196 assert_eq!(intent["enum"], json!(["create", "update"]));
197 assert_eq!(tool.parameters["required"], json!(["intent", "request"]));
198 assert!(tool.description.contains("never re-creates"));
199 }
200
201 #[test]
202 fn the_render_tool_states_the_adjacency_rules() {
203 let tool = render_a2ui_tool(None);
204 assert!(tool.description.contains("never nested"));
205 assert!(tool.description.contains("'root'"));
206 assert_eq!(
207 tool.parameters["properties"]["components"]["items"]["required"],
208 json!(["id", "component"])
209 );
210 assert_eq!(tool.parameters["properties"]["components"]["minItems"], 1);
211 }
212
213 #[test]
214 fn a_catalog_constrains_the_component_enum() {
215 let catalog = Catalog::basic();
216 let tool = render_a2ui_tool(Some(&catalog));
217 let types = &tool.parameters["properties"]["components"]["items"]["properties"]["component"]
218 ["enum"];
219 assert_eq!(types[0], "Text");
220 assert_eq!(types.as_array().unwrap().len(), 18);
221
222 let open = render_a2ui_tool(None);
224 assert!(
225 open.parameters["properties"]["components"]["items"]["properties"]["component"]["enum"]
226 .is_null()
227 );
228 assert!(render_a2ui_tool(Some(&Catalog::empty("x"))).parameters["properties"]
229 ["components"]["items"]["properties"]["component"]["enum"]
230 .is_null());
231 }
232
233 #[test]
234 fn definitions_render_for_an_api_payload() {
235 let value = generate_a2ui_tool().to_anthropic_value();
236 assert_eq!(value["name"], "generate_a2ui");
237 assert_eq!(value["input_schema"]["type"], "object");
238 assert_eq!(tool_definitions(None).len(), 2);
239 }
240}