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, crate::compat::AdkError>;
11
12    fn validate(&self, _payload: &Value) -> Result<(), crate::compat::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, crate::compat::AdkError> {
26        let jsonl = surface.to_a2ui_jsonl().map_err(|error| {
27            crate::compat::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<(), crate::compat::AdkError> {
39        if payload.get("jsonl").and_then(Value::as_str).is_none() {
40            return Err(crate::compat::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 {
57            thread_id: thread_id.into(),
58            run_id: run_id.into(),
59        }
60    }
61}
62
63impl UiProtocolAdapter for AgUiAdapter {
64    fn protocol(&self) -> UiProtocol {
65        UiProtocol::AgUi
66    }
67
68    fn to_protocol_payload(&self, surface: &UiSurface) -> Result<Value, crate::compat::AdkError> {
69        let events = surface_to_event_stream(surface, self.thread_id.clone(), self.run_id.clone());
70        Ok(json!({
71            "protocol": "ag_ui",
72            "surface_id": surface.surface_id,
73            "events": events,
74        }))
75    }
76}
77
78#[derive(Debug, Clone)]
79pub struct McpAppsAdapter {
80    options: McpAppsRenderOptions,
81}
82
83impl McpAppsAdapter {
84    pub fn new(options: McpAppsRenderOptions) -> Self {
85        Self { options }
86    }
87}
88
89impl UiProtocolAdapter for McpAppsAdapter {
90    fn protocol(&self) -> UiProtocol {
91        UiProtocol::McpApps
92    }
93
94    fn to_protocol_payload(&self, surface: &UiSurface) -> Result<Value, crate::compat::AdkError> {
95        validate_mcp_apps_render_options(&self.options)?;
96        let payload = surface_to_mcp_apps_payload(surface, self.options.clone());
97        Ok(json!({
98            "protocol": "mcp_apps",
99            "surface_id": surface.surface_id,
100            "payload": payload,
101        }))
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use serde_json::json;
109
110    fn test_surface() -> UiSurface {
111        UiSurface::new(
112            "main",
113            "catalog",
114            vec![json!({"id":"root","component":"Column","children":[]})],
115        )
116    }
117
118    #[test]
119    fn a2ui_adapter_emits_jsonl_payload() {
120        let adapter = A2uiAdapter;
121        let payload = adapter
122            .to_protocol_payload(&test_surface())
123            .expect("a2ui payload");
124        adapter.validate(&payload).expect("a2ui validate");
125        assert_eq!(payload["protocol"], "a2ui");
126        assert!(payload["jsonl"].as_str().unwrap().contains("createSurface"));
127    }
128
129    #[test]
130    fn ag_ui_adapter_emits_event_stream_payload() {
131        let adapter = AgUiAdapter::new("thread-main", "run-main");
132        let payload = adapter
133            .to_protocol_payload(&test_surface())
134            .expect("ag ui payload");
135        assert_eq!(payload["protocol"], "ag_ui");
136        assert_eq!(payload["events"][0]["type"], "RUN_STARTED");
137    }
138
139    #[test]
140    fn mcp_apps_adapter_emits_resource_payload() {
141        let adapter = McpAppsAdapter::new(McpAppsRenderOptions::default());
142        let payload = adapter
143            .to_protocol_payload(&test_surface())
144            .expect("mcp payload");
145        assert_eq!(payload["protocol"], "mcp_apps");
146        assert!(
147            payload["payload"]["resource"]["uri"]
148                .as_str()
149                .unwrap()
150                .starts_with("ui://")
151        );
152    }
153
154    #[test]
155    fn mcp_apps_adapter_rejects_invalid_domain_options() {
156        let adapter = McpAppsAdapter::new(McpAppsRenderOptions {
157            domain: Some("ftp://example.com".to_string()),
158            ..Default::default()
159        });
160        let result = adapter.to_protocol_payload(&test_surface());
161        assert!(result.is_err());
162    }
163}