Skip to main content

adk_ui/interop/
adapter.rs

1use 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
7/// Shared interface for converting canonical UI surfaces into protocol payloads.
8pub 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}