1use crate::ServerConfig;
2use crate::auth_bridge::{RequestContextError, RequestContextExtractor};
3use crate::rest::controllers::ui::{
4 McpUiInitializeParams, McpUiMessageParams, McpUiUpdateModelContextParams,
5 initialize_mcp_ui_bridge, mark_mcp_ui_initialized, message_mcp_ui_bridge,
6 update_mcp_ui_bridge_model_context,
7};
8use crate::ui_protocol::{
9 SUPPORTED_UI_PROTOCOLS, UI_PROTOCOL_CAPABILITIES, normalize_runtime_ui_protocol,
10};
11use adk_core::{RequestContext, SessionId, UserId};
12use axum::{
13 Json,
14 extract::{Path, State},
15 http::{HeaderMap, StatusCode},
16 response::sse::{Event, KeepAlive, Sse},
17};
18use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
19use futures::{StreamExt, stream::Stream};
20use serde::{Deserialize, Serialize};
21use serde_json::{Map, Value, json};
22use std::collections::HashMap;
23use std::convert::Infallible;
24use tracing::{Instrument, info, warn};
25use uuid::Uuid;
26
27fn default_streaming_true() -> bool {
28 true
29}
30
31const UI_PROTOCOL_HEADER: &str = "x-adk-ui-protocol";
32const UI_TRANSPORT_HEADER: &str = "x-adk-ui-transport";
33
34#[derive(Clone)]
35pub struct RuntimeController {
36 config: ServerConfig,
37}
38
39impl RuntimeController {
40 pub fn new(config: ServerConfig) -> Self {
41 Self { config }
42 }
43}
44
45#[derive(Serialize, Deserialize, Debug)]
47pub struct Attachment {
48 pub name: String,
49 #[serde(rename = "type")]
50 pub mime_type: String,
51 pub base64: String,
52}
53
54#[derive(Serialize, Deserialize)]
55pub struct RunRequest {
56 pub new_message: String,
57 #[serde(default, alias = "uiProtocol")]
58 pub ui_protocol: Option<String>,
59 #[serde(default)]
60 pub protocol: Option<String>,
61 #[serde(default, alias = "ui_transport")]
62 pub ui_transport: Option<String>,
63 #[serde(default)]
64 pub attachments: Vec<Attachment>,
65}
66
67#[derive(Serialize, Deserialize, Debug)]
69#[serde(rename_all = "camelCase")]
70pub struct RunSseRequest {
71 pub app_name: String,
72 pub user_id: String,
73 pub session_id: String,
74 #[serde(default)]
75 pub new_message: Option<NewMessage>,
76 #[serde(default = "default_streaming_true")]
77 pub streaming: bool,
78 #[serde(default)]
79 pub state_delta: Option<Value>,
80 #[serde(default, alias = "ui_protocol")]
81 pub ui_protocol: Option<String>,
82 #[serde(default)]
83 pub protocol: Option<String>,
84 #[serde(default, alias = "ui_transport")]
85 pub ui_transport: Option<String>,
86 #[serde(default)]
87 pub input: Option<AgUiRunInput>,
88 #[serde(default)]
89 pub ag_ui_input: Option<AgUiRunInput>,
90 #[serde(default)]
91 pub ag_ui_compatibility_event: Option<Value>,
92 #[serde(default)]
93 pub protocol_envelope: Option<Value>,
94 #[serde(default)]
95 pub mcp_apps_request: Option<McpAppsRuntimeEnvelope>,
96 #[serde(default)]
97 pub mcp_apps_initialize: Option<McpAppsRuntimeEnvelope>,
98 #[serde(default)]
99 pub mcp_apps_initialized: Option<Value>,
100 #[serde(default)]
101 pub method: Option<String>,
102 #[serde(default)]
103 pub params: Option<Value>,
104}
105
106#[derive(Serialize, Deserialize, Debug, Clone, Default)]
107#[serde(rename_all = "camelCase")]
108pub struct AgUiInputMessage {
109 #[serde(default)]
110 pub id: Option<String>,
111 #[serde(default)]
112 pub role: Option<String>,
113 #[serde(default)]
114 pub name: Option<String>,
115 #[serde(default)]
116 pub activity_type: Option<String>,
117 #[serde(default)]
118 pub content: Option<Value>,
119 #[serde(default)]
120 pub replace: Option<bool>,
121 #[serde(default)]
122 pub patch: Option<Vec<Value>>,
123}
124
125#[derive(Serialize, Deserialize, Debug, Clone, Default)]
126#[serde(rename_all = "camelCase")]
127pub struct AgUiRunInput {
128 #[serde(default)]
129 pub thread_id: Option<String>,
130 #[serde(default)]
131 pub run_id: Option<String>,
132 #[serde(default)]
133 pub parent_run_id: Option<String>,
134 #[serde(default)]
135 pub state: Option<Value>,
136 #[serde(default)]
137 pub messages: Vec<AgUiInputMessage>,
138 #[serde(default)]
139 pub tools: Vec<Value>,
140 #[serde(default)]
141 pub context: Vec<Value>,
142 #[serde(default)]
143 pub forwarded_props: Option<Value>,
144}
145
146#[derive(Serialize, Deserialize, Debug, Clone)]
147#[serde(rename_all = "camelCase")]
148pub struct McpAppsRuntimeEnvelope {
149 pub method: String,
150 #[serde(default)]
151 pub params: Option<Value>,
152}
153
154#[derive(Serialize, Deserialize, Debug, Clone)]
155pub struct NewMessage {
156 pub role: String,
157 pub parts: Vec<MessagePart>,
158}
159
160#[derive(Serialize, Deserialize, Debug, Clone)]
161pub struct MessagePart {
162 #[serde(default)]
163 pub text: Option<String>,
164 #[serde(default, rename = "inlineData")]
165 pub inline_data: Option<InlineData>,
166}
167
168#[derive(Serialize, Deserialize, Debug, Clone)]
169#[serde(rename_all = "camelCase")]
170pub struct InlineData {
171 pub display_name: Option<String>,
172 pub data: String,
173 pub mime_type: String,
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177enum UiProfile {
178 AdkUi,
179 A2ui,
180 AgUi,
181 McpApps,
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185enum UiTransportMode {
186 LegacyWrapper,
187 ProtocolNative,
188}
189
190impl UiProfile {
191 fn as_str(self) -> &'static str {
192 match self {
193 Self::AdkUi => "adk_ui",
194 Self::A2ui => "a2ui",
195 Self::AgUi => "ag_ui",
196 Self::McpApps => "mcp_apps",
197 }
198 }
199}
200
201type RuntimeError = (StatusCode, String);
202
203fn adk_err_to_runtime(err: adk_core::AdkError) -> RuntimeError {
209 let status =
210 StatusCode::from_u16(err.http_status_code()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
211 let body = err.to_problem_json().to_string();
212 (status, body)
213}
214
215fn parse_ui_profile(raw: &str) -> Option<UiProfile> {
216 match normalize_runtime_ui_protocol(raw)? {
217 "adk_ui" => Some(UiProfile::AdkUi),
218 "a2ui" => Some(UiProfile::A2ui),
219 "ag_ui" => Some(UiProfile::AgUi),
220 "mcp_apps" => Some(UiProfile::McpApps),
221 _ => None,
222 }
223}
224
225fn resolve_ui_profile(
226 headers: &HeaderMap,
227 body_ui_protocol: Option<&str>,
228) -> Result<UiProfile, RuntimeError> {
229 let header_value = headers.get(UI_PROTOCOL_HEADER).and_then(|v| v.to_str().ok());
230 let candidate = header_value.or(body_ui_protocol);
231
232 let Some(raw) = candidate else {
233 return Ok(UiProfile::AdkUi);
234 };
235
236 parse_ui_profile(raw).ok_or_else(|| {
237 let supported = SUPPORTED_UI_PROTOCOLS.join(", ");
238 warn!(
239 requested = %raw,
240 header = %UI_PROTOCOL_HEADER,
241 "unsupported ui protocol requested"
242 );
243 (
244 StatusCode::BAD_REQUEST,
245 format!("Unsupported ui protocol '{}'. Supported profiles: {}", raw, supported),
246 )
247 })
248}
249
250fn parse_ui_transport(raw: &str) -> Option<UiTransportMode> {
251 match raw.trim().to_ascii_lowercase().as_str() {
252 "legacy" | "legacy_wrapper" => Some(UiTransportMode::LegacyWrapper),
253 "native" | "protocol_native" => Some(UiTransportMode::ProtocolNative),
254 _ => None,
255 }
256}
257
258fn resolve_ui_transport(
259 headers: &HeaderMap,
260 body_ui_transport: Option<&str>,
261) -> Result<UiTransportMode, RuntimeError> {
262 let header_value = headers.get(UI_TRANSPORT_HEADER).and_then(|v| v.to_str().ok());
263 let candidate = header_value.or(body_ui_transport);
264
265 let Some(raw) = candidate else {
266 return Ok(UiTransportMode::LegacyWrapper);
267 };
268
269 parse_ui_transport(raw).ok_or_else(|| {
270 warn!(
271 requested = %raw,
272 header = %UI_TRANSPORT_HEADER,
273 "unsupported ui transport requested"
274 );
275 (
276 StatusCode::BAD_REQUEST,
277 format!(
278 "Unsupported ui transport '{}'. Supported values: legacy_wrapper, protocol_native",
279 raw
280 ),
281 )
282 })
283}
284
285fn validate_transport_support(
286 profile: UiProfile,
287 transport: UiTransportMode,
288) -> Result<(), RuntimeError> {
289 if transport == UiTransportMode::ProtocolNative && profile != UiProfile::AgUi {
290 return Err((
291 StatusCode::BAD_REQUEST,
292 "protocol_native transport is currently available only for ag_ui; use the MCP Apps bridge endpoints for mcp_apps".to_string(),
293 ));
294 }
295 Ok(())
296}
297
298fn protocol_from_envelope(envelope: &Value) -> Option<&str> {
299 envelope.as_object().and_then(|object| object.get("protocol")).and_then(|value| value.as_str())
300}
301
302fn serialize_runtime_event(event: &adk_core::Event, profile: UiProfile) -> Option<String> {
303 if profile == UiProfile::AdkUi {
304 return serde_json::to_string(event).ok();
305 }
306
307 serde_json::to_string(&json!({
308 "ui_protocol": profile.as_str(),
309 "event": event
310 }))
311 .ok()
312}
313
314fn infer_sse_request_protocol(req: &RunSseRequest) -> Option<&str> {
315 req.ui_protocol
316 .as_deref()
317 .or(req.protocol.as_deref())
318 .or_else(|| req.protocol_envelope.as_ref().and_then(protocol_from_envelope))
319 .or_else(|| req.ag_ui_input.as_ref().map(|_| "ag_ui"))
320 .or_else(|| req.input.as_ref().map(|_| "ag_ui"))
321 .or_else(|| req.mcp_apps_request.as_ref().map(|_| "mcp_apps"))
322 .or_else(|| req.mcp_apps_initialize.as_ref().map(|_| "mcp_apps"))
323}
324
325fn infer_run_request_protocol(req: &RunRequest) -> Option<&str> {
326 req.ui_protocol.as_deref().or(req.protocol.as_deref())
327}
328
329fn ag_ui_input_from_request(req: &RunSseRequest) -> Option<AgUiRunInput> {
330 req.ag_ui_input.clone().or_else(|| req.input.clone()).or_else(|| {
331 let envelope = req.protocol_envelope.as_ref()?;
332 if protocol_from_envelope(envelope)? != "ag_ui" {
333 return None;
334 }
335 envelope
336 .as_object()
337 .and_then(|object| object.get("input"))
338 .and_then(|value| serde_json::from_value(value.clone()).ok())
339 })
340}
341
342fn mcp_apps_request_from_request(req: &RunSseRequest) -> Option<McpAppsRuntimeEnvelope> {
343 req.mcp_apps_request.clone().or_else(|| {
344 if let Some(method) = req.method.clone() {
345 return Some(McpAppsRuntimeEnvelope { method, params: req.params.clone() });
346 }
347 let envelope = req.protocol_envelope.as_ref()?;
348 if protocol_from_envelope(envelope)? != "mcp_apps" {
349 return None;
350 }
351 let object = envelope.as_object()?;
352 let method = object.get("method")?.as_str()?.to_string();
353 let params = object.get("params").cloned();
354 Some(McpAppsRuntimeEnvelope { method, params })
355 })
356}
357
358fn mcp_apps_initialize_from_request(req: &RunSseRequest) -> Option<McpAppsRuntimeEnvelope> {
359 req.mcp_apps_initialize.clone()
360}
361
362fn extract_text_segments(value: &Value) -> Vec<String> {
363 match value {
364 Value::String(text) => {
365 let trimmed = text.trim();
366 if trimmed.is_empty() { vec![] } else { vec![trimmed.to_string()] }
367 }
368 Value::Array(items) => items
369 .iter()
370 .flat_map(|item| {
371 if let Some(text) = item
372 .as_object()
373 .and_then(|object| object.get("text"))
374 .and_then(|text| text.as_str())
375 {
376 let trimmed = text.trim();
377 if !trimmed.is_empty() {
378 return vec![trimmed.to_string()];
379 }
380 }
381 vec![]
382 })
383 .collect(),
384 Value::Object(object) => object
385 .get("text")
386 .and_then(|text| text.as_str())
387 .map(|text| text.trim().to_string())
388 .filter(|text| !text.is_empty())
389 .into_iter()
390 .collect(),
391 _ => vec![],
392 }
393}
394
395fn new_message_from_ag_ui_input(input: &AgUiRunInput) -> Option<NewMessage> {
396 let selected = input
397 .messages
398 .iter()
399 .rev()
400 .find(|message| message.role.as_deref().unwrap_or("user") == "user")
401 .or_else(|| input.messages.last())?;
402
403 let content = selected.content.as_ref()?;
404 let parts: Vec<MessagePart> = extract_text_segments(content)
405 .into_iter()
406 .map(|text| MessagePart { text: Some(text), inline_data: None })
407 .collect();
408 if parts.is_empty() {
409 return None;
410 }
411
412 Some(NewMessage { role: selected.role.clone().unwrap_or_else(|| "user".to_string()), parts })
413}
414
415fn activity_content_snapshot(value: Option<&Value>) -> Value {
416 match value.cloned() {
417 Some(Value::Object(object)) => Value::Object(object),
418 Some(other) => json!({ "value": other }),
419 None => json!({}),
420 }
421}
422
423fn activity_message_id(message: &AgUiInputMessage) -> String {
424 message.id.clone().unwrap_or_else(|| format!("activity-{}", Uuid::new_v4()))
425}
426
427fn activity_message_type(message: &AgUiInputMessage) -> String {
428 message
429 .activity_type
430 .clone()
431 .or_else(|| message.name.clone())
432 .unwrap_or_else(|| "CUSTOM".to_string())
433}
434
435fn activity_events_from_ag_ui_input(input: &AgUiRunInput) -> Vec<Value> {
436 input
437 .messages
438 .iter()
439 .filter(|message| message.role.as_deref() == Some("activity"))
440 .map(|message| {
441 let timestamp = chrono::Utc::now().timestamp_millis().max(0) as u64;
442 let message_id = activity_message_id(message);
443 let activity_type = activity_message_type(message);
444 if let Some(patch) = &message.patch {
445 json!({
446 "type": "ACTIVITY_DELTA",
447 "messageId": message_id,
448 "activityType": activity_type,
449 "patch": patch,
450 "timestamp": timestamp,
451 })
452 } else {
453 let mut event = json!({
454 "type": "ACTIVITY_SNAPSHOT",
455 "messageId": message_id,
456 "activityType": activity_type,
457 "content": activity_content_snapshot(message.content.as_ref()),
458 "timestamp": timestamp,
459 });
460 if let Some(replace) = message.replace
461 && let Some(object) = event.as_object_mut()
462 {
463 object.insert("replace".to_string(), Value::Bool(replace));
464 }
465 event
466 }
467 })
468 .collect()
469}
470
471fn messages_snapshot_from_ag_ui_input(input: &AgUiRunInput) -> Option<Value> {
472 if input.messages.is_empty() {
473 return None;
474 }
475
476 let filtered: Vec<AgUiInputMessage> = input
477 .messages
478 .iter()
479 .filter(|message| !(message.role.as_deref() == Some("activity") && message.patch.is_some()))
480 .cloned()
481 .collect();
482 if filtered.is_empty() {
483 return None;
484 }
485
486 serde_json::to_value(filtered).ok()
487}
488
489fn object_entries_to_state_delta(object: &Map<String, Value>) -> HashMap<String, Value> {
490 object.iter().map(|(key, value)| (key.clone(), value.clone())).collect()
491}
492
493fn ag_ui_state_delta(input: &AgUiRunInput) -> HashMap<String, Value> {
494 let mut delta = HashMap::new();
495
496 if let Some(state) = input.state.clone() {
497 match state {
498 Value::Object(object) => {
499 delta.extend(object_entries_to_state_delta(&object));
500 }
501 value => {
502 delta.insert("temp:ag_ui_state".to_string(), value);
503 }
504 }
505 }
506
507 if !input.messages.is_empty()
508 && let Ok(value) = serde_json::to_value(&input.messages)
509 {
510 delta.insert("temp:ag_ui_messages".to_string(), value);
511 }
512 if !input.tools.is_empty() {
513 delta.insert("temp:ag_ui_tools".to_string(), Value::Array(input.tools.clone()));
514 }
515 if !input.context.is_empty() {
516 delta.insert("temp:ag_ui_context".to_string(), Value::Array(input.context.clone()));
517 }
518 if let Some(forwarded_props) = input.forwarded_props.clone() {
519 delta.insert("temp:ag_ui_forwarded_props".to_string(), forwarded_props);
520 }
521
522 delta
523}
524
525fn body_state_delta(value: Option<&Value>) -> Result<HashMap<String, Value>, RuntimeError> {
526 let Some(value) = value else {
527 return Ok(HashMap::new());
528 };
529 let object = value.as_object().ok_or_else(|| {
530 (StatusCode::BAD_REQUEST, "stateDelta must be a JSON object when provided".to_string())
531 })?;
532 Ok(object_entries_to_state_delta(object))
533}
534
535fn log_profile_deprecation(profile: UiProfile) {
536 if profile != UiProfile::AdkUi {
537 return;
538 }
539 let Some(spec) = UI_PROTOCOL_CAPABILITIES
540 .iter()
541 .find(|capability| capability.protocol == profile.as_str())
542 .and_then(|capability| capability.deprecation)
543 else {
544 return;
545 };
546
547 warn!(
548 protocol = %profile.as_str(),
549 stage = %spec.stage,
550 announced_on = %spec.announced_on,
551 sunset_target_on = ?spec.sunset_target_on,
552 replacements = ?spec.replacement_protocols,
553 "legacy ui protocol profile selected"
554 );
555}
556
557fn build_content_with_attachments(
559 text: &str,
560 attachments: &[Attachment],
561) -> Result<adk_core::Content, RuntimeError> {
562 let mut content = adk_core::Content::new("user");
563
564 content.parts.push(adk_core::Part::Text { text: text.to_string() });
566
567 for attachment in attachments {
569 match BASE64_STANDARD.decode(&attachment.base64) {
570 Ok(data) => {
571 if data.len() > adk_core::MAX_INLINE_DATA_SIZE {
572 return Err((
573 StatusCode::PAYLOAD_TOO_LARGE,
574 format!(
575 "Attachment '{}' exceeds max inline size of {} bytes",
576 attachment.name,
577 adk_core::MAX_INLINE_DATA_SIZE
578 ),
579 ));
580 }
581 content.parts.push(adk_core::Part::InlineData {
582 mime_type: attachment.mime_type.clone(),
583 data,
584 uri: None,
585 annotations: None,
586 });
587 }
588 Err(e) => {
589 return Err((
590 StatusCode::BAD_REQUEST,
591 format!("Invalid base64 data for attachment '{}': {}", attachment.name, e),
592 ));
593 }
594 }
595 }
596
597 Ok(content)
598}
599
600fn build_content_from_parts(parts: &[MessagePart]) -> Result<adk_core::Content, RuntimeError> {
602 let mut content = adk_core::Content::new("user");
603
604 for part in parts {
605 if let Some(text) = &part.text {
607 content.parts.push(adk_core::Part::Text { text: text.clone() });
608 }
609
610 if let Some(inline_data) = &part.inline_data {
612 match BASE64_STANDARD.decode(&inline_data.data) {
613 Ok(data) => {
614 if data.len() > adk_core::MAX_INLINE_DATA_SIZE {
615 return Err((
616 StatusCode::PAYLOAD_TOO_LARGE,
617 format!(
618 "inline_data exceeds max inline size of {} bytes",
619 adk_core::MAX_INLINE_DATA_SIZE
620 ),
621 ));
622 }
623 content.parts.push(adk_core::Part::InlineData {
624 mime_type: inline_data.mime_type.clone(),
625 data,
626 uri: None,
627 annotations: None,
628 });
629 }
630 Err(e) => {
631 return Err((
632 StatusCode::BAD_REQUEST,
633 format!("Invalid base64 data in inline_data: {}", e),
634 ));
635 }
636 }
637 }
638 }
639
640 Ok(content)
641}
642
643async fn apply_state_delta_to_session(
644 session_service: &std::sync::Arc<dyn adk_session::SessionService>,
645 app_name: &str,
646 user_id: &str,
647 session_id: &str,
648 state_delta: HashMap<String, Value>,
649) -> Result<(), RuntimeError> {
650 if state_delta.is_empty() {
651 return Ok(());
652 }
653
654 let identity = adk_core::AdkIdentity::new(
655 adk_core::AppName::try_from(app_name).map_err(|error| {
656 (
657 StatusCode::BAD_REQUEST,
658 format!("invalid app_name for state delta application: {}", error),
659 )
660 })?,
661 adk_core::UserId::try_from(user_id).map_err(|error| {
662 (
663 StatusCode::BAD_REQUEST,
664 format!("invalid user_id for state delta application: {}", error),
665 )
666 })?,
667 adk_core::SessionId::try_from(session_id).map_err(|error| {
668 (
669 StatusCode::BAD_REQUEST,
670 format!("invalid session_id for state delta application: {}", error),
671 )
672 })?,
673 );
674
675 let mut event = adk_core::Event::new(format!("ui-input-{}", Uuid::new_v4()));
676 event.author = "ui_protocol_bridge".to_string();
677 event.actions.state_delta = state_delta;
678 session_service
679 .append_event_for_identity(adk_session::AppendEventRequest { identity, event })
680 .await
681 .map_err(adk_err_to_runtime)
682}
683
684fn merge_runtime_state_delta(
685 body_delta: HashMap<String, Value>,
686 ag_ui_delta: HashMap<String, Value>,
687) -> HashMap<String, Value> {
688 let mut merged = body_delta;
689 merged.extend(ag_ui_delta);
690 merged
691}
692
693fn json_pointer_escape(segment: &str) -> String {
694 segment.replace('~', "~0").replace('/', "~1")
695}
696
697fn state_delta_to_json_patch(delta: &HashMap<String, Value>) -> Vec<Value> {
698 delta
699 .iter()
700 .map(|(key, value)| {
701 json!({
702 "op": "add",
703 "path": format!("/{}", json_pointer_escape(key)),
704 "value": value
705 })
706 })
707 .collect()
708}
709
710fn timestamp_millis(event: &adk_core::Event) -> u64 {
711 event.timestamp.timestamp_millis().max(0) as u64
712}
713
714fn serialize_ag_ui_tool_call_delta(args: &Value, allow_raw_string_delta: bool) -> String {
715 if allow_raw_string_delta && let Value::String(delta) = args {
716 return delta.clone();
717 }
718
719 serde_json::to_string(args).unwrap_or_else(|_| args.to_string())
720}
721
722fn translate_ag_ui_event(event: &adk_core::Event, thread_id: &str, run_id: &str) -> Vec<Value> {
723 let mut translated = Vec::new();
724 let timestamp = timestamp_millis(event);
725 let is_partial = event.llm_response.partial;
726
727 if !event.actions.state_delta.is_empty() {
728 translated.push(json!({
729 "type": "STATE_DELTA",
730 "delta": state_delta_to_json_patch(&event.actions.state_delta),
731 "timestamp": timestamp,
732 }));
733 }
734
735 if let Some(message) = event.llm_response.error_message.clone() {
736 translated.push(json!({
737 "type": "RUN_ERROR",
738 "threadId": thread_id,
739 "runId": run_id,
740 "message": message,
741 "code": event.llm_response.error_code,
742 "timestamp": timestamp,
743 }));
744 }
745
746 let Some(content) = &event.llm_response.content else {
747 return translated;
748 };
749
750 for (index, part) in content.parts.iter().enumerate() {
751 match part {
752 adk_core::Part::Text { text } if !text.trim().is_empty() => {
753 let message_id = format!("{}-text-{}", event.id, index);
754 if is_partial {
755 translated.push(json!({
756 "type": "TEXT_MESSAGE_CHUNK",
757 "messageId": message_id,
758 "role": "assistant",
759 "delta": text,
760 "timestamp": timestamp,
761 }));
762 } else {
763 translated.push(json!({
764 "type": "TEXT_MESSAGE_START",
765 "messageId": message_id,
766 "role": "assistant",
767 "timestamp": timestamp,
768 }));
769 translated.push(json!({
770 "type": "TEXT_MESSAGE_CONTENT",
771 "messageId": format!("{}-text-{}", event.id, index),
772 "delta": text,
773 "timestamp": timestamp,
774 }));
775 translated.push(json!({
776 "type": "TEXT_MESSAGE_END",
777 "messageId": format!("{}-text-{}", event.id, index),
778 "timestamp": timestamp,
779 }));
780 }
781 }
782 adk_core::Part::Thinking { thinking, .. } if !thinking.trim().is_empty() => {
783 let message_id = format!("{}-reasoning-{}", event.id, index);
784 if is_partial {
785 translated.push(json!({
786 "type": "REASONING_MESSAGE_CHUNK",
787 "messageId": message_id,
788 "delta": thinking,
789 "timestamp": timestamp,
790 }));
791 } else {
792 let reasoning_id = format!("{}-reasoning-phase-{}", event.id, index);
793 translated.push(json!({
794 "type": "REASONING_START",
795 "messageId": reasoning_id,
796 "timestamp": timestamp,
797 }));
798 translated.push(json!({
799 "type": "REASONING_MESSAGE_START",
800 "messageId": message_id,
801 "role": "assistant",
802 "timestamp": timestamp,
803 }));
804 translated.push(json!({
805 "type": "REASONING_MESSAGE_CONTENT",
806 "messageId": format!("{}-reasoning-{}", event.id, index),
807 "delta": thinking,
808 "timestamp": timestamp,
809 }));
810 translated.push(json!({
811 "type": "REASONING_MESSAGE_END",
812 "messageId": format!("{}-reasoning-{}", event.id, index),
813 "timestamp": timestamp,
814 }));
815 translated.push(json!({
816 "type": "REASONING_END",
817 "messageId": reasoning_id,
818 "timestamp": timestamp,
819 }));
820 }
821 }
822 adk_core::Part::FunctionCall { name, args, id, .. } => {
823 let tool_call_id =
824 id.clone().unwrap_or_else(|| format!("{}-tool-call-{}", event.id, index));
825 let raw_chunk_supported = is_partial && matches!(args, Value::String(_));
826 let args_delta = serialize_ag_ui_tool_call_delta(args, raw_chunk_supported);
827 if raw_chunk_supported {
828 translated.push(json!({
829 "type": "TOOL_CALL_CHUNK",
830 "toolCallId": tool_call_id,
831 "toolCallName": name,
832 "delta": args_delta,
833 "timestamp": timestamp,
834 }));
835 } else {
836 translated.push(json!({
837 "type": "TOOL_CALL_START",
838 "toolCallId": tool_call_id,
839 "toolCallName": name,
840 "timestamp": timestamp,
841 }));
842 translated.push(json!({
843 "type": "TOOL_CALL_ARGS",
844 "toolCallId": id.clone().unwrap_or_else(|| format!("{}-tool-call-{}", event.id, index)),
845 "delta": args_delta,
846 "timestamp": timestamp,
847 }));
848 translated.push(json!({
849 "type": "TOOL_CALL_END",
850 "toolCallId": id.clone().unwrap_or_else(|| format!("{}-tool-call-{}", event.id, index)),
851 "timestamp": timestamp,
852 }));
853 }
854 }
855 adk_core::Part::FunctionResponse { function_response, id, .. } => {
856 let tool_call_id =
857 id.clone().unwrap_or_else(|| format!("{}-tool-result-{}", event.id, index));
858 let response_content = serde_json::to_string(&function_response.response)
859 .unwrap_or_else(|_| function_response.response.to_string());
860 translated.push(json!({
861 "type": "TOOL_CALL_RESULT",
862 "messageId": format!("msg-{}", tool_call_id),
863 "toolCallId": tool_call_id,
864 "toolCallName": function_response.name,
865 "content": response_content,
866 "role": "tool",
867 "timestamp": timestamp,
868 }));
869 }
870 _ => {}
871 }
872 }
873
874 translated
875}
876
877async fn extract_request_context(
883 extractor: Option<&dyn RequestContextExtractor>,
884 headers: &HeaderMap,
885) -> Result<Option<RequestContext>, RuntimeError> {
886 let Some(extractor) = extractor else {
887 return Ok(None);
888 };
889
890 let mut builder = axum::http::Request::builder();
892 for (name, value) in headers {
893 builder = builder.header(name, value);
894 }
895 let (parts, _) = builder
896 .body(())
897 .map_err(|e| {
898 (StatusCode::INTERNAL_SERVER_ERROR, format!("failed to build request parts: {e}"))
899 })?
900 .into_parts();
901
902 match extractor.extract(&parts).await {
903 Ok(ctx) => Ok(Some(ctx)),
904 Err(RequestContextError::MissingAuth) => {
905 Err((StatusCode::UNAUTHORIZED, "missing authorization".to_string()))
906 }
907 Err(RequestContextError::InvalidToken(msg)) => {
908 Err((StatusCode::UNAUTHORIZED, format!("invalid token: {msg}")))
909 }
910 Err(RequestContextError::ExtractionFailed(msg)) => {
911 Err((StatusCode::INTERNAL_SERVER_ERROR, format!("auth extraction failed: {msg}")))
912 }
913 }
914}
915
916fn bridge_params_with_identity(
917 app_name: &str,
918 user_id: &str,
919 session_id: &str,
920 params: Option<Value>,
921) -> Value {
922 let mut object = params.and_then(|value| value.as_object().cloned()).unwrap_or_default();
923 object.insert("appName".to_string(), Value::String(app_name.to_string()));
924 object.insert("userId".to_string(), Value::String(user_id.to_string()));
925 object.insert("sessionId".to_string(), Value::String(session_id.to_string()));
926 Value::Object(object)
927}
928
929fn deserialize_bridge_params<T: for<'de> Deserialize<'de>>(
930 app_name: &str,
931 user_id: &str,
932 session_id: &str,
933 params: Option<Value>,
934) -> Result<T, RuntimeError> {
935 serde_json::from_value(bridge_params_with_identity(app_name, user_id, session_id, params))
936 .map_err(|error| {
937 (StatusCode::BAD_REQUEST, format!("invalid protocol-native bridge payload: {}", error))
938 })
939}
940
941fn maybe_mark_mcp_ui_initialized(
942 app_name: &str,
943 user_id: &str,
944 session_id: &str,
945 initialized_notification: Option<&Value>,
946) -> Result<(), RuntimeError> {
947 let Some(value) = initialized_notification else {
948 return Ok(());
949 };
950 let method = value
951 .as_object()
952 .and_then(|object| object.get("method"))
953 .and_then(|value| value.as_str())
954 .unwrap_or_default();
955 if method == "ui/notifications/initialized" {
956 mark_mcp_ui_initialized(app_name, user_id, session_id)?;
957 }
958 Ok(())
959}
960
961fn apply_mcp_apps_runtime_envelope(
962 app_name: &str,
963 user_id: &str,
964 session_id: &str,
965 envelope: McpAppsRuntimeEnvelope,
966) -> Result<(), RuntimeError> {
967 match envelope.method.as_str() {
968 "ui/initialize" => {
969 let params = deserialize_bridge_params::<McpUiInitializeParams>(
970 app_name,
971 user_id,
972 session_id,
973 envelope.params,
974 )?;
975 initialize_mcp_ui_bridge(params)?;
976 Ok(())
977 }
978 "ui/message" => {
979 let params = deserialize_bridge_params::<McpUiMessageParams>(
980 app_name,
981 user_id,
982 session_id,
983 envelope.params,
984 )?;
985 message_mcp_ui_bridge(params)?;
986 Ok(())
987 }
988 "ui/update-model-context" => {
989 let params = deserialize_bridge_params::<McpUiUpdateModelContextParams>(
990 app_name,
991 user_id,
992 session_id,
993 envelope.params,
994 )?;
995 update_mcp_ui_bridge_model_context(params)?;
996 Ok(())
997 }
998 "ui/notifications/initialized" => {
999 mark_mcp_ui_initialized(app_name, user_id, session_id)?;
1000 Ok(())
1001 }
1002 method => Err((
1003 StatusCode::BAD_REQUEST,
1004 format!("unsupported MCP Apps runtime bridge method '{}'", method),
1005 )),
1006 }
1007}
1008
1009fn direct_ag_ui_events(event: &adk_core::Event, thread_id: &str, run_id: &str) -> Vec<String> {
1010 translate_ag_ui_event(event, thread_id, run_id)
1011 .into_iter()
1012 .filter_map(|item| serde_json::to_string(&item).ok())
1013 .collect()
1014}
1015
1016fn build_runtime_sse_stream<S>(
1017 mut event_stream: S,
1018 profile: UiProfile,
1019 transport: UiTransportMode,
1020 session_id: String,
1021 ag_ui_input: Option<AgUiRunInput>,
1022) -> std::pin::Pin<Box<dyn Stream<Item = Result<Event, Infallible>> + Send>>
1023where
1024 S: Stream<Item = adk_core::Result<adk_core::Event>> + Send + 'static + Unpin,
1025{
1026 let selected_thread_id =
1027 ag_ui_input.as_ref().and_then(|input| input.thread_id.clone()).unwrap_or(session_id);
1028 let selected_run_input = ag_ui_input.clone();
1029 let selected_parent_run_id = ag_ui_input.as_ref().and_then(|input| input.parent_run_id.clone());
1030 let selected_initial_state = ag_ui_input.as_ref().and_then(|input| input.state.clone());
1031 let selected_messages_snapshot =
1032 ag_ui_input.as_ref().and_then(messages_snapshot_from_ag_ui_input);
1033 let selected_activity_events =
1034 ag_ui_input.as_ref().map(activity_events_from_ag_ui_input).unwrap_or_default();
1035
1036 Box::pin(async_stream::stream! {
1037 let native_ag_ui = profile == UiProfile::AgUi && transport == UiTransportMode::ProtocolNative;
1038 let mut started = false;
1039 let mut active_run_id = ag_ui_input.as_ref().and_then(|input| input.run_id.clone());
1040
1041 while let Some(item) = event_stream.next().await {
1042 match item {
1043 Ok(event) => {
1044 if native_ag_ui {
1045 let run_id = active_run_id
1046 .get_or_insert_with(|| event.invocation_id.clone())
1047 .clone();
1048 if !started {
1049 let mut started_event = json!({
1050 "type": "RUN_STARTED",
1051 "threadId": selected_thread_id,
1052 "runId": run_id,
1053 });
1054 if let Some(parent_run_id) = selected_parent_run_id.clone()
1055 && let Some(object) = started_event.as_object_mut()
1056 {
1057 object.insert("parentRunId".to_string(), Value::String(parent_run_id));
1058 }
1059 if let Some(run_input) = selected_run_input.clone()
1060 && let Ok(value) = serde_json::to_value(run_input)
1061 && let Some(object) = started_event.as_object_mut()
1062 {
1063 object.insert("input".to_string(), value);
1064 }
1065 yield Ok(Event::default().data(started_event.to_string()));
1066
1067 if let Some(snapshot) = selected_initial_state.clone() {
1068 yield Ok(Event::default().data(json!({
1069 "type": "STATE_SNAPSHOT",
1070 "snapshot": snapshot,
1071 }).to_string()));
1072 }
1073 if let Some(messages) = selected_messages_snapshot.clone() {
1074 yield Ok(Event::default().data(json!({
1075 "type": "MESSAGES_SNAPSHOT",
1076 "messages": messages,
1077 }).to_string()));
1078 }
1079 for activity_event in selected_activity_events.clone() {
1080 yield Ok(Event::default().data(activity_event.to_string()));
1081 }
1082 started = true;
1083 }
1084
1085 for payload in direct_ag_ui_events(&event, &selected_thread_id, &run_id) {
1086 yield Ok(Event::default().data(payload));
1087 }
1088 } else if let Some(payload) = serialize_runtime_event(&event, profile) {
1089 yield Ok(Event::default().data(payload));
1090 }
1091 }
1092 Err(error) => {
1093 if native_ag_ui {
1094 let run_id =
1095 active_run_id.unwrap_or_else(|| format!("run-{}", Uuid::new_v4()));
1096 if !started {
1097 yield Ok(Event::default().data(json!({
1098 "type": "RUN_STARTED",
1099 "threadId": selected_thread_id,
1100 "runId": run_id,
1101 }).to_string()));
1102 }
1103 yield Ok(Event::default().data(json!({
1104 "type": "RUN_ERROR",
1105 "threadId": selected_thread_id,
1106 "runId": run_id,
1107 "message": error.to_string(),
1108 }).to_string()));
1109 }
1110 return;
1111 }
1112 }
1113 }
1114
1115 if native_ag_ui {
1116 let run_id = active_run_id.unwrap_or_else(|| format!("run-{}", Uuid::new_v4()));
1117 if !started {
1118 yield Ok(Event::default().data(json!({
1119 "type": "RUN_STARTED",
1120 "threadId": selected_thread_id,
1121 "runId": run_id,
1122 }).to_string()));
1123 }
1124 yield Ok(Event::default().data(json!({
1125 "type": "RUN_FINISHED",
1126 "threadId": selected_thread_id,
1127 "runId": run_id,
1128 }).to_string()));
1129 }
1130 })
1131}
1132
1133pub async fn run_sse(
1134 State(controller): State<RuntimeController>,
1135 Path((app_name, user_id, session_id)): Path<(String, String, String)>,
1136 headers: HeaderMap,
1137 Json(req): Json<RunRequest>,
1138) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, RuntimeError> {
1139 let ui_profile = resolve_ui_profile(&headers, infer_run_request_protocol(&req))?;
1140 let transport = resolve_ui_transport(&headers, req.ui_transport.as_deref())?;
1141 validate_transport_support(ui_profile, transport)?;
1142 let span = tracing::info_span!("run_sse", session_id = %session_id, app_name = %app_name, user_id = %user_id);
1143
1144 async move {
1145 log_profile_deprecation(ui_profile);
1146 info!(
1147 ui_protocol = %ui_profile.as_str(),
1148 ui_transport = ?transport,
1149 "resolved ui protocol profile for runtime request"
1150 );
1151
1152 let request_context = extract_request_context(
1156 controller.config.request_context_extractor.as_deref(),
1157 &headers,
1158 )
1159 .await?;
1160
1161 let effective_user_id = request_context.as_ref().map_or(user_id, |rc| rc.user_id.clone());
1167
1168 controller
1170 .config
1171 .session_service
1172 .get(adk_session::GetRequest {
1173 app_name: app_name.clone(),
1174 user_id: effective_user_id.clone(),
1175 session_id: session_id.clone(),
1176 num_recent_events: None,
1177 after: None,
1178 })
1179 .await
1180 .map_err(|_| (StatusCode::NOT_FOUND, "session not found".to_string()))?;
1181
1182 let agent = controller
1184 .config
1185 .agent_loader
1186 .load_agent(&app_name)
1187 .await
1188 .map_err(adk_err_to_runtime)?;
1189
1190 let mut runner_builder = adk_runner::Runner::builder()
1192 .app_name(app_name.clone())
1193 .agent(agent)
1194 .session_service(controller.config.session_service.clone());
1195 if let Some(ref artifact_service) = controller.config.artifact_service {
1196 runner_builder = runner_builder.artifact_service(artifact_service.clone());
1197 }
1198 if let Some(ref memory_service) = controller.config.memory_service {
1199 runner_builder = runner_builder.memory_service(memory_service.clone());
1200 }
1201 if let Some(ref compaction_config) = controller.config.compaction_config {
1202 runner_builder = runner_builder.compaction_config(compaction_config.clone());
1203 }
1204 if let Some(ref context_cache_config) = controller.config.context_cache_config {
1205 runner_builder = runner_builder.context_cache_config(context_cache_config.clone());
1206 }
1207 if let Some(ref cache_capable) = controller.config.cache_capable {
1208 runner_builder = runner_builder.cache_capable(cache_capable.clone());
1209 }
1210 if let Some(request_context) = request_context {
1211 runner_builder = runner_builder.request_context(request_context);
1212 }
1213 let runner = runner_builder.build().map_err(adk_err_to_runtime)?;
1214
1215 let content = build_content_with_attachments(&req.new_message, &req.attachments)?;
1217
1218 if !req.attachments.is_empty() {
1220 info!(attachment_count = req.attachments.len(), "processing request with attachments");
1221 }
1222
1223 let typed_user_id =
1225 UserId::new(effective_user_id).map_err(|err| adk_err_to_runtime(err.into()))?;
1226 let typed_session_id =
1227 SessionId::new(session_id.clone()).map_err(|err| adk_err_to_runtime(err.into()))?;
1228 let event_stream = runner
1229 .run(typed_user_id, typed_session_id, content)
1230 .await
1231 .map_err(adk_err_to_runtime)?;
1232
1233 let sse_stream =
1235 build_runtime_sse_stream(event_stream, ui_profile, transport, session_id.clone(), None);
1236
1237 Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
1238 }
1239 .instrument(span)
1240 .await
1241}
1242
1243pub async fn run_sse_compat(
1246 State(controller): State<RuntimeController>,
1247 headers: HeaderMap,
1248 Json(req): Json<RunSseRequest>,
1249) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, RuntimeError> {
1250 let ui_profile = resolve_ui_profile(&headers, infer_sse_request_protocol(&req))?;
1251 let transport = resolve_ui_transport(&headers, req.ui_transport.as_deref())?;
1252 validate_transport_support(ui_profile, transport)?;
1253 let app_name = req.app_name.clone();
1254 let user_id = req.user_id.clone();
1255 let session_id = req.session_id.clone();
1256 let ag_ui_input = ag_ui_input_from_request(&req);
1257 let mcp_apps_request = mcp_apps_request_from_request(&req);
1258 let mcp_apps_initialize = mcp_apps_initialize_from_request(&req);
1259
1260 info!(
1261 app_name = %app_name,
1262 user_id = %user_id,
1263 session_id = %session_id,
1264 ui_protocol = %ui_profile.as_str(),
1265 ui_transport = ?transport,
1266 "POST /run_sse request received"
1267 );
1268 log_profile_deprecation(ui_profile);
1269
1270 let request_context =
1274 extract_request_context(controller.config.request_context_extractor.as_deref(), &headers)
1275 .await?;
1276
1277 let effective_user_id = request_context.as_ref().map_or(user_id, |rc| rc.user_id.clone());
1283
1284 let resolved_new_message = req
1285 .new_message
1286 .clone()
1287 .or_else(|| ag_ui_input.as_ref().and_then(new_message_from_ag_ui_input))
1288 .ok_or_else(|| {
1289 (
1290 StatusCode::BAD_REQUEST,
1291 "newMessage is required unless protocol-native ag_ui input supplies a user message"
1292 .to_string(),
1293 )
1294 })?;
1295
1296 let content = build_content_from_parts(&resolved_new_message.parts)?;
1298
1299 let text_parts: Vec<_> =
1301 resolved_new_message.parts.iter().filter(|p| p.text.is_some()).collect();
1302 let data_parts: Vec<_> =
1303 resolved_new_message.parts.iter().filter(|p| p.inline_data.is_some()).collect();
1304 if !data_parts.is_empty() {
1305 info!(
1306 text_parts = text_parts.len(),
1307 inline_data_parts = data_parts.len(),
1308 "processing request with inline data"
1309 );
1310 }
1311
1312 let merged_state_delta = merge_runtime_state_delta(
1313 body_state_delta(req.state_delta.as_ref())?,
1314 ag_ui_input.as_ref().map(ag_ui_state_delta).unwrap_or_default(),
1315 );
1316
1317 let session_result = controller
1319 .config
1320 .session_service
1321 .get(adk_session::GetRequest {
1322 app_name: app_name.clone(),
1323 user_id: effective_user_id.clone(),
1324 session_id: session_id.clone(),
1325 num_recent_events: None,
1326 after: None,
1327 })
1328 .await;
1329
1330 if session_result.is_err() {
1332 controller
1333 .config
1334 .session_service
1335 .create(adk_session::CreateRequest {
1336 app_name: app_name.clone(),
1337 user_id: effective_user_id.clone(),
1338 session_id: Some(session_id.clone()),
1339 state: merged_state_delta.clone(),
1340 })
1341 .await
1342 .map_err(adk_err_to_runtime)?;
1343 } else {
1344 apply_state_delta_to_session(
1345 &controller.config.session_service,
1346 &app_name,
1347 &effective_user_id,
1348 &session_id,
1349 merged_state_delta.clone(),
1350 )
1351 .await?;
1352 }
1353
1354 if ui_profile == UiProfile::McpApps {
1355 if let Some(initialize) = mcp_apps_initialize {
1356 apply_mcp_apps_runtime_envelope(
1357 &app_name,
1358 &effective_user_id,
1359 &session_id,
1360 initialize,
1361 )?;
1362 }
1363 if let Some(request) = mcp_apps_request {
1364 apply_mcp_apps_runtime_envelope(&app_name, &effective_user_id, &session_id, request)?;
1365 }
1366 maybe_mark_mcp_ui_initialized(
1367 &app_name,
1368 &effective_user_id,
1369 &session_id,
1370 req.mcp_apps_initialized.as_ref(),
1371 )?;
1372 }
1373
1374 let agent =
1376 controller.config.agent_loader.load_agent(&app_name).await.map_err(adk_err_to_runtime)?;
1377
1378 let streaming_mode =
1380 if req.streaming { adk_core::StreamingMode::SSE } else { adk_core::StreamingMode::None };
1381
1382 let mut runner_builder = adk_runner::Runner::builder()
1383 .app_name(app_name)
1384 .agent(agent)
1385 .session_service(controller.config.session_service.clone())
1386 .run_config(adk_core::RunConfig::builder().streaming_mode(streaming_mode).build());
1387 if let Some(ref artifact_service) = controller.config.artifact_service {
1388 runner_builder = runner_builder.artifact_service(artifact_service.clone());
1389 }
1390 if let Some(ref memory_service) = controller.config.memory_service {
1391 runner_builder = runner_builder.memory_service(memory_service.clone());
1392 }
1393 if let Some(ref compaction_config) = controller.config.compaction_config {
1394 runner_builder = runner_builder.compaction_config(compaction_config.clone());
1395 }
1396 if let Some(ref context_cache_config) = controller.config.context_cache_config {
1397 runner_builder = runner_builder.context_cache_config(context_cache_config.clone());
1398 }
1399 if let Some(ref cache_capable) = controller.config.cache_capable {
1400 runner_builder = runner_builder.cache_capable(cache_capable.clone());
1401 }
1402 if let Some(request_context) = request_context {
1403 runner_builder = runner_builder.request_context(request_context);
1404 }
1405 let runner = runner_builder.build().map_err(adk_err_to_runtime)?;
1406
1407 let typed_user_id =
1409 UserId::new(effective_user_id).map_err(|err| adk_err_to_runtime(err.into()))?;
1410 let typed_session_id =
1411 SessionId::new(session_id.clone()).map_err(|err| adk_err_to_runtime(err.into()))?;
1412 let event_stream =
1413 runner.run(typed_user_id, typed_session_id, content).await.map_err(adk_err_to_runtime)?;
1414
1415 let sse_stream = build_runtime_sse_stream(
1417 event_stream,
1418 ui_profile,
1419 transport,
1420 session_id.clone(),
1421 ag_ui_input,
1422 );
1423
1424 Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
1425}