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, 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}