adk_ui/interop/
adapter.rs1use super::{
2 McpAppsRenderOptions, UiProtocol, UiSurface, surface_to_event_stream,
3 surface_to_mcp_apps_payload, validate_mcp_apps_render_options,
4};
5use serde_json::{Value, json};
6
7pub trait UiProtocolAdapter {
9 fn protocol(&self) -> UiProtocol;
10 fn to_protocol_payload(&self, surface: &UiSurface) -> Result<Value, adk_core::AdkError>;
11
12 fn validate(&self, _payload: &Value) -> Result<(), adk_core::AdkError> {
13 Ok(())
14 }
15}
16
17#[derive(Debug, Clone, Default)]
18pub struct A2uiAdapter;
19
20impl UiProtocolAdapter for A2uiAdapter {
21 fn protocol(&self) -> UiProtocol {
22 UiProtocol::A2ui
23 }
24
25 fn to_protocol_payload(&self, surface: &UiSurface) -> Result<Value, adk_core::AdkError> {
26 let jsonl = surface.to_a2ui_jsonl().map_err(|error| {
27 adk_core::AdkError::Tool(format!("Failed to encode A2UI JSONL: {}", error))
28 })?;
29 Ok(json!({
30 "protocol": "a2ui",
31 "surface_id": surface.surface_id,
32 "components": surface.components,
33 "data_model": surface.data_model,
34 "jsonl": jsonl,
35 }))
36 }
37
38 fn validate(&self, payload: &Value) -> Result<(), adk_core::AdkError> {
39 if payload.get("jsonl").and_then(Value::as_str).is_none() {
40 return Err(adk_core::AdkError::Tool(
41 "A2UI payload validation failed: missing jsonl string".to_string(),
42 ));
43 }
44 Ok(())
45 }
46}
47
48#[derive(Debug, Clone)]
49pub struct AgUiAdapter {
50 thread_id: String,
51 run_id: String,
52}
53
54impl AgUiAdapter {
55 pub fn new(thread_id: impl Into<String>, run_id: impl Into<String>) -> Self {
56 Self { thread_id: thread_id.into(), run_id: run_id.into() }
57 }
58}
59
60impl UiProtocolAdapter for AgUiAdapter {
61 fn protocol(&self) -> UiProtocol {
62 UiProtocol::AgUi
63 }
64
65 fn to_protocol_payload(&self, surface: &UiSurface) -> Result<Value, adk_core::AdkError> {
66 let events = surface_to_event_stream(surface, self.thread_id.clone(), self.run_id.clone());
67 Ok(json!({
68 "protocol": "ag_ui",
69 "surface_id": surface.surface_id,
70 "events": events,
71 }))
72 }
73}
74
75#[derive(Debug, Clone)]
76pub struct McpAppsAdapter {
77 options: McpAppsRenderOptions,
78}
79
80impl McpAppsAdapter {
81 pub fn new(options: McpAppsRenderOptions) -> Self {
82 Self { options }
83 }
84}
85
86impl UiProtocolAdapter for McpAppsAdapter {
87 fn protocol(&self) -> UiProtocol {
88 UiProtocol::McpApps
89 }
90
91 fn to_protocol_payload(&self, surface: &UiSurface) -> Result<Value, adk_core::AdkError> {
92 validate_mcp_apps_render_options(&self.options)?;
93 let payload = surface_to_mcp_apps_payload(surface, self.options.clone());
94 Ok(json!({
95 "protocol": "mcp_apps",
96 "surface_id": surface.surface_id,
97 "payload": payload,
98 }))
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use serde_json::json;
106
107 fn test_surface() -> UiSurface {
108 UiSurface::new(
109 "main",
110 "catalog",
111 vec![json!({"id":"root","component":"Column","children":[]})],
112 )
113 }
114
115 #[test]
116 fn a2ui_adapter_emits_jsonl_payload() {
117 let adapter = A2uiAdapter;
118 let payload = adapter.to_protocol_payload(&test_surface()).expect("a2ui payload");
119 adapter.validate(&payload).expect("a2ui validate");
120 assert_eq!(payload["protocol"], "a2ui");
121 assert!(payload["jsonl"].as_str().unwrap().contains("createSurface"));
122 }
123
124 #[test]
125 fn ag_ui_adapter_emits_event_stream_payload() {
126 let adapter = AgUiAdapter::new("thread-main", "run-main");
127 let payload = adapter.to_protocol_payload(&test_surface()).expect("ag ui payload");
128 assert_eq!(payload["protocol"], "ag_ui");
129 assert_eq!(payload["events"][0]["type"], "RUN_STARTED");
130 }
131
132 #[test]
133 fn mcp_apps_adapter_emits_resource_payload() {
134 let adapter = McpAppsAdapter::new(McpAppsRenderOptions::default());
135 let payload = adapter.to_protocol_payload(&test_surface()).expect("mcp payload");
136 assert_eq!(payload["protocol"], "mcp_apps");
137 assert!(payload["payload"]["resource"]["uri"].as_str().unwrap().starts_with("ui://"));
138 }
139
140 #[test]
141 fn mcp_apps_adapter_rejects_invalid_domain_options() {
142 let adapter = McpAppsAdapter::new(McpAppsRenderOptions {
143 domain: Some("ftp://example.com".to_string()),
144 ..Default::default()
145 });
146 let result = adapter.to_protocol_payload(&test_surface());
147 assert!(result.is_err());
148 }
149}