adk_ui/tools/
render_kit.rs1use crate::compat::{Result, Tool, ToolContext};
2use crate::kit::{KitArtifacts, KitGenerator, KitSpec};
3use crate::tools::LegacyProtocolOptions;
4use async_trait::async_trait;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use serde_json::{Value, json};
8use std::sync::Arc;
9
10#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
12pub struct RenderKitParams {
13 #[serde(flatten)]
14 pub spec: KitSpec,
15 #[serde(default)]
17 pub output: Option<String>,
18 #[serde(flatten)]
20 pub protocol: LegacyProtocolOptions,
21}
22
23pub struct RenderKitTool;
25
26impl RenderKitTool {
27 pub fn new() -> Self {
28 Self
29 }
30}
31
32impl Default for RenderKitTool {
33 fn default() -> Self {
34 Self::new()
35 }
36}
37
38#[async_trait]
39impl Tool for RenderKitTool {
40 fn name(&self) -> &str {
41 "render_kit"
42 }
43
44 fn description(&self) -> &str {
45 "Generate a versioned brand-kit draft from a KitSpec. Returns a manifest, supported component catalog, semantic tokens, templates, agent constraints, and scoped theme CSS. Drafts must be reviewed before runtime approval."
46 }
47
48 fn parameters_schema(&self) -> Option<Value> {
49 Some(super::generate_gemini_schema::<RenderKitParams>())
50 }
51
52 async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
53 let params: RenderKitParams = serde_json::from_value(args.clone()).map_err(|e| {
54 crate::compat::AdkError::tool(format!("Invalid parameters: {}. Got: {}", e, args))
55 })?;
56
57 let generator = KitGenerator::new();
58 let artifacts = generator.generate(¶ms.spec);
59 if let Err(errors) = artifacts.manifest.validate() {
60 let details = errors
61 .iter()
62 .map(ToString::to_string)
63 .collect::<Vec<_>>()
64 .join("; ");
65 return Err(crate::compat::AdkError::tool(format!(
66 "Generated kit failed validation: {details}"
67 )));
68 }
69 let payload = format_output(&artifacts, params.output.as_deref());
70
71 let surface_id = params.protocol.resolved_surface_id("kit");
72 let output = match params.protocol.protocol {
73 Some(protocol) => {
74 let protocol = serde_json::to_value(protocol).unwrap_or_else(|_| json!("a2ui"));
75 json!({
76 "protocol": protocol,
77 "surface_id": surface_id.clone(),
78 "payload": payload
79 })
80 }
81 None => payload,
82 };
83 let surface_ref = crate::surface_runtime::next_surface_ref(&ctx, surface_id);
84 crate::surface_runtime::record_surface_ref(&ctx, &surface_ref);
85 Ok(output)
86 }
87}
88
89fn format_output(artifacts: &KitArtifacts, output: Option<&str>) -> Value {
90 match output {
91 Some("catalog_only") => artifacts.catalog.clone(),
92 _ => json!({
93 "manifest": artifacts.manifest,
94 "catalog": artifacts.catalog,
95 "tokens": artifacts.tokens,
96 "templates": artifacts.templates,
97 "agent_context": artifacts.agent_context,
98 "theme_css": artifacts.theme_css,
99 }),
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use crate::compat::{Content, EventActions, ReadonlyContext};
107 use async_trait::async_trait;
108 use std::sync::{Arc, Mutex};
109
110 struct TestContext {
111 content: Content,
112 actions: Mutex<EventActions>,
113 }
114
115 impl TestContext {
116 fn new() -> Self {
117 Self {
118 content: Content::new("user"),
119 actions: Mutex::new(EventActions::default()),
120 }
121 }
122 }
123
124 #[async_trait]
125 impl ReadonlyContext for TestContext {
126 fn invocation_id(&self) -> &str {
127 "test"
128 }
129 fn agent_name(&self) -> &str {
130 "test"
131 }
132 fn user_id(&self) -> &str {
133 "user"
134 }
135 fn app_name(&self) -> &str {
136 "app"
137 }
138 fn session_id(&self) -> &str {
139 "session"
140 }
141 fn branch(&self) -> &str {
142 ""
143 }
144 fn user_content(&self) -> &Content {
145 &self.content
146 }
147 }
148
149 #[async_trait]
150 impl crate::compat::CallbackContext for TestContext {
151 fn artifacts(&self) -> Option<Arc<dyn crate::compat::Artifacts>> {
152 None
153 }
154 }
155
156 #[async_trait]
157 impl ToolContext for TestContext {
158 fn function_call_id(&self) -> &str {
159 "call-123"
160 }
161 fn actions(&self) -> EventActions {
162 self.actions.lock().unwrap().clone()
163 }
164 fn set_actions(&self, actions: EventActions) {
165 *self.actions.lock().unwrap() = actions;
166 }
167 async fn search_memory(&self, _query: &str) -> Result<Vec<crate::compat::MemoryEntry>> {
168 Ok(vec![])
169 }
170 }
171
172 #[tokio::test]
173 async fn render_kit_emits_catalog() {
174 let tool = RenderKitTool::new();
175 let args = serde_json::json!({
176 "name": "Fintech Pro",
177 "version": "0.1.0",
178 "brand": { "vibe": "trustworthy", "industry": "fintech" },
179 "colors": { "primary": "#2F6BFF" },
180 "typography": { "family": "Source Sans 3" },
181 "assets": [{
182 "id": "primary-logo",
183 "kind": "logo",
184 "uri": "/assets/fintech-pro.svg",
185 "mimeType": "image/svg+xml",
186 "alt": "Fintech Pro"
187 }],
188 "provenance": {
189 "source": "imported",
190 "sourceUri": "https://brand.example.com/design-system"
191 },
192 "templates": ["auth_login"]
193 });
194
195 let ctx: Arc<dyn ToolContext> = Arc::new(TestContext::new());
196 let value = tool.execute(ctx, args).await.unwrap();
197 assert_eq!(value["manifest"]["status"], "draft");
198 assert!(value.get("catalog").is_some());
199 assert!(value.get("tokens").is_some());
200 assert!(value["catalog"]["components"]["Card"].is_object());
201 assert_eq!(value["manifest"]["assets"][0]["id"], "primary-logo");
202 }
203
204 #[tokio::test]
205 async fn render_kit_emits_protocol_envelope() {
206 let tool = RenderKitTool::new();
207 let args = serde_json::json!({
208 "name": "Fintech Pro",
209 "version": "0.1.0",
210 "brand": { "vibe": "trustworthy", "industry": "fintech" },
211 "colors": { "primary": "#2F6BFF" },
212 "typography": { "family": "Source Sans 3" },
213 "protocol": "mcp_apps"
214 });
215
216 let ctx: Arc<dyn ToolContext> = Arc::new(TestContext::new());
217 let value = tool.execute(ctx, args).await.unwrap();
218 assert_eq!(value["protocol"], "mcp_apps");
219 assert!(value["payload"]["catalog"].is_object());
220 }
221}