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 serialize_runtime_error(error: &adk_core::AdkError, profile: UiProfile) -> Option<String> {
315 let mut event = adk_core::Event::new(String::new());
316 event.llm_response.error_code = Some(error.code.to_string());
317 event.llm_response.error_message = Some(error.to_string());
318 serialize_runtime_event(&event, profile)
319}
320
321fn infer_sse_request_protocol(req: &RunSseRequest) -> Option<&str> {
322 req.ui_protocol
323 .as_deref()
324 .or(req.protocol.as_deref())
325 .or_else(|| req.protocol_envelope.as_ref().and_then(protocol_from_envelope))
326 .or_else(|| req.ag_ui_input.as_ref().map(|_| "ag_ui"))
327 .or_else(|| req.input.as_ref().map(|_| "ag_ui"))
328 .or_else(|| req.mcp_apps_request.as_ref().map(|_| "mcp_apps"))
329 .or_else(|| req.mcp_apps_initialize.as_ref().map(|_| "mcp_apps"))
330}
331
332fn infer_run_request_protocol(req: &RunRequest) -> Option<&str> {
333 req.ui_protocol.as_deref().or(req.protocol.as_deref())
334}
335
336fn ag_ui_input_from_request(req: &RunSseRequest) -> Option<AgUiRunInput> {
337 req.ag_ui_input.clone().or_else(|| req.input.clone()).or_else(|| {
338 let envelope = req.protocol_envelope.as_ref()?;
339 if protocol_from_envelope(envelope)? != "ag_ui" {
340 return None;
341 }
342 envelope
343 .as_object()
344 .and_then(|object| object.get("input"))
345 .and_then(|value| serde_json::from_value(value.clone()).ok())
346 })
347}
348
349fn mcp_apps_request_from_request(req: &RunSseRequest) -> Option<McpAppsRuntimeEnvelope> {
350 req.mcp_apps_request.clone().or_else(|| {
351 if let Some(method) = req.method.clone() {
352 return Some(McpAppsRuntimeEnvelope { method, params: req.params.clone() });
353 }
354 let envelope = req.protocol_envelope.as_ref()?;
355 if protocol_from_envelope(envelope)? != "mcp_apps" {
356 return None;
357 }
358 let object = envelope.as_object()?;
359 let method = object.get("method")?.as_str()?.to_string();
360 let params = object.get("params").cloned();
361 Some(McpAppsRuntimeEnvelope { method, params })
362 })
363}
364
365fn mcp_apps_initialize_from_request(req: &RunSseRequest) -> Option<McpAppsRuntimeEnvelope> {
366 req.mcp_apps_initialize.clone()
367}
368
369fn extract_text_segments(value: &Value) -> Vec<String> {
370 match value {
371 Value::String(text) => {
372 let trimmed = text.trim();
373 if trimmed.is_empty() { vec![] } else { vec![trimmed.to_string()] }
374 }
375 Value::Array(items) => items
376 .iter()
377 .flat_map(|item| {
378 if let Some(text) = item
379 .as_object()
380 .and_then(|object| object.get("text"))
381 .and_then(|text| text.as_str())
382 {
383 let trimmed = text.trim();
384 if !trimmed.is_empty() {
385 return vec![trimmed.to_string()];
386 }
387 }
388 vec![]
389 })
390 .collect(),
391 Value::Object(object) => object
392 .get("text")
393 .and_then(|text| text.as_str())
394 .map(|text| text.trim().to_string())
395 .filter(|text| !text.is_empty())
396 .into_iter()
397 .collect(),
398 _ => vec![],
399 }
400}
401
402fn new_message_from_ag_ui_input(input: &AgUiRunInput) -> Option<NewMessage> {
403 let selected = input
404 .messages
405 .iter()
406 .rev()
407 .find(|message| message.role.as_deref().unwrap_or("user") == "user")
408 .or_else(|| input.messages.last())?;
409
410 let content = selected.content.as_ref()?;
411 let parts: Vec<MessagePart> = extract_text_segments(content)
412 .into_iter()
413 .map(|text| MessagePart { text: Some(text), inline_data: None })
414 .collect();
415 if parts.is_empty() {
416 return None;
417 }
418
419 Some(NewMessage { role: selected.role.clone().unwrap_or_else(|| "user".to_string()), parts })
420}
421
422fn activity_content_snapshot(value: Option<&Value>) -> Value {
423 match value.cloned() {
424 Some(Value::Object(object)) => Value::Object(object),
425 Some(other) => json!({ "value": other }),
426 None => json!({}),
427 }
428}
429
430fn activity_message_id(message: &AgUiInputMessage) -> String {
431 message.id.clone().unwrap_or_else(|| format!("activity-{}", Uuid::new_v4()))
432}
433
434fn activity_message_type(message: &AgUiInputMessage) -> String {
435 message
436 .activity_type
437 .clone()
438 .or_else(|| message.name.clone())
439 .unwrap_or_else(|| "CUSTOM".to_string())
440}
441
442fn activity_events_from_ag_ui_input(input: &AgUiRunInput) -> Vec<Value> {
443 input
444 .messages
445 .iter()
446 .filter(|message| message.role.as_deref() == Some("activity"))
447 .map(|message| {
448 let timestamp = chrono::Utc::now().timestamp_millis().max(0) as u64;
449 let message_id = activity_message_id(message);
450 let activity_type = activity_message_type(message);
451 if let Some(patch) = &message.patch {
452 json!({
453 "type": "ACTIVITY_DELTA",
454 "messageId": message_id,
455 "activityType": activity_type,
456 "patch": patch,
457 "timestamp": timestamp,
458 })
459 } else {
460 let mut event = json!({
461 "type": "ACTIVITY_SNAPSHOT",
462 "messageId": message_id,
463 "activityType": activity_type,
464 "content": activity_content_snapshot(message.content.as_ref()),
465 "timestamp": timestamp,
466 });
467 if let Some(replace) = message.replace
468 && let Some(object) = event.as_object_mut()
469 {
470 object.insert("replace".to_string(), Value::Bool(replace));
471 }
472 event
473 }
474 })
475 .collect()
476}
477
478fn messages_snapshot_from_ag_ui_input(input: &AgUiRunInput) -> Option<Value> {
479 if input.messages.is_empty() {
480 return None;
481 }
482
483 let filtered: Vec<AgUiInputMessage> = input
484 .messages
485 .iter()
486 .filter(|message| !(message.role.as_deref() == Some("activity") && message.patch.is_some()))
487 .cloned()
488 .collect();
489 if filtered.is_empty() {
490 return None;
491 }
492
493 serde_json::to_value(filtered).ok()
494}
495
496fn object_entries_to_state_delta(object: &Map<String, Value>) -> HashMap<String, Value> {
497 object.iter().map(|(key, value)| (key.clone(), value.clone())).collect()
498}
499
500fn ag_ui_state_delta(input: &AgUiRunInput) -> HashMap<String, Value> {
501 let mut delta = HashMap::new();
502
503 if let Some(state) = input.state.clone() {
504 match state {
505 Value::Object(object) => {
506 delta.extend(object_entries_to_state_delta(&object));
507 }
508 value => {
509 delta.insert("temp:ag_ui_state".to_string(), value);
510 }
511 }
512 }
513
514 if !input.messages.is_empty()
515 && let Ok(value) = serde_json::to_value(&input.messages)
516 {
517 delta.insert("temp:ag_ui_messages".to_string(), value);
518 }
519 if !input.tools.is_empty() {
520 delta.insert("temp:ag_ui_tools".to_string(), Value::Array(input.tools.clone()));
521 }
522 if !input.context.is_empty() {
523 delta.insert("temp:ag_ui_context".to_string(), Value::Array(input.context.clone()));
524 }
525 if let Some(forwarded_props) = input.forwarded_props.clone() {
526 delta.insert("temp:ag_ui_forwarded_props".to_string(), forwarded_props);
527 }
528
529 delta
530}
531
532fn body_state_delta(value: Option<&Value>) -> Result<HashMap<String, Value>, RuntimeError> {
533 let Some(value) = value else {
534 return Ok(HashMap::new());
535 };
536 let object = value.as_object().ok_or_else(|| {
537 (StatusCode::BAD_REQUEST, "stateDelta must be a JSON object when provided".to_string())
538 })?;
539 Ok(object_entries_to_state_delta(object))
540}
541
542fn log_profile_deprecation(profile: UiProfile) {
543 if profile != UiProfile::AdkUi {
544 return;
545 }
546 let Some(spec) = UI_PROTOCOL_CAPABILITIES
547 .iter()
548 .find(|capability| capability.protocol == profile.as_str())
549 .and_then(|capability| capability.deprecation)
550 else {
551 return;
552 };
553
554 warn!(
555 protocol = %profile.as_str(),
556 stage = %spec.stage,
557 announced_on = %spec.announced_on,
558 sunset_target_on = ?spec.sunset_target_on,
559 replacements = ?spec.replacement_protocols,
560 "legacy ui protocol profile selected"
561 );
562}
563
564fn build_content_with_attachments(
566 text: &str,
567 attachments: &[Attachment],
568) -> Result<adk_core::Content, RuntimeError> {
569 let mut content = adk_core::Content::new("user");
570
571 content.parts.push(adk_core::Part::Text { text: text.to_string() });
573
574 for attachment in attachments {
576 match BASE64_STANDARD.decode(&attachment.base64) {
577 Ok(data) => {
578 if data.len() > adk_core::MAX_INLINE_DATA_SIZE {
579 return Err((
580 StatusCode::PAYLOAD_TOO_LARGE,
581 format!(
582 "Attachment '{}' exceeds max inline size of {} bytes",
583 attachment.name,
584 adk_core::MAX_INLINE_DATA_SIZE
585 ),
586 ));
587 }
588 content.parts.push(adk_core::Part::InlineData {
589 mime_type: attachment.mime_type.clone(),
590 data,
591 uri: None,
592 annotations: None,
593 });
594 }
595 Err(e) => {
596 return Err((
597 StatusCode::BAD_REQUEST,
598 format!("Invalid base64 data for attachment '{}': {}", attachment.name, e),
599 ));
600 }
601 }
602 }
603
604 Ok(content)
605}
606
607fn build_content_from_parts(parts: &[MessagePart]) -> Result<adk_core::Content, RuntimeError> {
609 let mut content = adk_core::Content::new("user");
610
611 for part in parts {
612 if let Some(text) = &part.text {
614 content.parts.push(adk_core::Part::Text { text: text.clone() });
615 }
616
617 if let Some(inline_data) = &part.inline_data {
619 match BASE64_STANDARD.decode(&inline_data.data) {
620 Ok(data) => {
621 if data.len() > adk_core::MAX_INLINE_DATA_SIZE {
622 return Err((
623 StatusCode::PAYLOAD_TOO_LARGE,
624 format!(
625 "inline_data exceeds max inline size of {} bytes",
626 adk_core::MAX_INLINE_DATA_SIZE
627 ),
628 ));
629 }
630 content.parts.push(adk_core::Part::InlineData {
631 mime_type: inline_data.mime_type.clone(),
632 data,
633 uri: None,
634 annotations: None,
635 });
636 }
637 Err(e) => {
638 return Err((
639 StatusCode::BAD_REQUEST,
640 format!("Invalid base64 data in inline_data: {}", e),
641 ));
642 }
643 }
644 }
645 }
646
647 Ok(content)
648}
649
650async fn apply_state_delta_to_session(
651 session_service: &std::sync::Arc<dyn adk_session::SessionService>,
652 app_name: &str,
653 user_id: &str,
654 session_id: &str,
655 state_delta: HashMap<String, Value>,
656) -> Result<(), RuntimeError> {
657 if state_delta.is_empty() {
658 return Ok(());
659 }
660
661 let identity = adk_core::AdkIdentity::new(
662 adk_core::AppName::try_from(app_name).map_err(|error| {
663 (
664 StatusCode::BAD_REQUEST,
665 format!("invalid app_name for state delta application: {}", error),
666 )
667 })?,
668 adk_core::UserId::try_from(user_id).map_err(|error| {
669 (
670 StatusCode::BAD_REQUEST,
671 format!("invalid user_id for state delta application: {}", error),
672 )
673 })?,
674 adk_core::SessionId::try_from(session_id).map_err(|error| {
675 (
676 StatusCode::BAD_REQUEST,
677 format!("invalid session_id for state delta application: {}", error),
678 )
679 })?,
680 );
681
682 let mut event = adk_core::Event::new(format!("ui-input-{}", Uuid::new_v4()));
683 event.author = "ui_protocol_bridge".to_string();
684 event.actions.state_delta = state_delta;
685 session_service
686 .append_event_for_identity(adk_session::AppendEventRequest { identity, event })
687 .await
688 .map_err(adk_err_to_runtime)
689}
690
691fn merge_runtime_state_delta(
692 body_delta: HashMap<String, Value>,
693 ag_ui_delta: HashMap<String, Value>,
694) -> HashMap<String, Value> {
695 let mut merged = body_delta;
696 merged.extend(ag_ui_delta);
697 merged
698}
699
700fn json_pointer_escape(segment: &str) -> String {
701 segment.replace('~', "~0").replace('/', "~1")
702}
703
704fn state_delta_to_json_patch(delta: &HashMap<String, Value>) -> Vec<Value> {
705 delta
706 .iter()
707 .map(|(key, value)| {
708 json!({
709 "op": "add",
710 "path": format!("/{}", json_pointer_escape(key)),
711 "value": value
712 })
713 })
714 .collect()
715}
716
717fn timestamp_millis(event: &adk_core::Event) -> u64 {
718 event.timestamp.timestamp_millis().max(0) as u64
719}
720
721fn serialize_ag_ui_tool_call_delta(args: &Value, allow_raw_string_delta: bool) -> String {
722 if allow_raw_string_delta && let Value::String(delta) = args {
723 return delta.clone();
724 }
725
726 serde_json::to_string(args).unwrap_or_else(|_| args.to_string())
727}
728
729fn translate_ag_ui_event(
730 event: &adk_core::Event,
731 thread_id: &str,
732 run_id: &str,
733 open: &AgUiMessages,
734) -> Vec<Value> {
735 let mut translated = Vec::new();
736 let timestamp = timestamp_millis(event);
737 let is_partial = event.llm_response.partial;
738
739 if !event.actions.state_delta.is_empty() {
740 translated.push(json!({
741 "type": "STATE_DELTA",
742 "delta": state_delta_to_json_patch(&event.actions.state_delta),
743 "timestamp": timestamp,
744 }));
745 }
746
747 if let Some(message) = event.llm_response.error_message.clone() {
748 translated.push(json!({
749 "type": "RUN_ERROR",
750 "threadId": thread_id,
751 "runId": run_id,
752 "message": message,
753 "code": event.llm_response.error_code,
754 "timestamp": timestamp,
755 }));
756 }
757
758 let Some(content) = &event.llm_response.content else {
759 return translated;
760 };
761
762 let mut text_seen = false;
763 let mut thinking_seen = false;
764 for (index, part) in content.parts.iter().enumerate() {
765 match part {
766 adk_core::Part::Text { .. } if !text_seen => {
767 text_seen = true;
768 let text: String = content
769 .parts
770 .iter()
771 .filter_map(|part| match part {
772 adk_core::Part::Text { text } => Some(text.as_str()),
773 _ => None,
774 })
775 .collect();
776 if text.is_empty() && (is_partial || open.text.is_none()) {
777 continue;
778 }
779 let message_id =
780 open.text.clone().unwrap_or_else(|| format!("{}-text-{}", event.id, index));
781 if is_partial {
782 translated.push(json!({
783 "type": "TEXT_MESSAGE_CHUNK",
784 "messageId": message_id,
785 "role": "assistant",
786 "delta": text,
787 "timestamp": timestamp,
788 }));
789 } else {
790 if open.text.is_none() {
791 translated.push(json!({
792 "type": "TEXT_MESSAGE_START",
793 "messageId": message_id,
794 "role": "assistant",
795 "timestamp": timestamp,
796 }));
797 }
798 if !text.is_empty() {
799 translated.push(json!({
800 "type": "TEXT_MESSAGE_CONTENT",
801 "messageId": message_id,
802 "delta": text,
803 "timestamp": timestamp,
804 }));
805 }
806 translated.push(json!({
807 "type": "TEXT_MESSAGE_END",
808 "messageId": message_id,
809 "timestamp": timestamp,
810 }));
811 }
812 }
813 adk_core::Part::Thinking { .. } if !thinking_seen => {
814 thinking_seen = true;
815 let thinking: String = content
816 .parts
817 .iter()
818 .filter_map(|part| match part {
819 adk_core::Part::Thinking { thinking, .. } => Some(thinking.as_str()),
820 _ => None,
821 })
822 .collect();
823 if thinking.is_empty() && (is_partial || open.reasoning.is_none()) {
824 continue;
825 }
826 let message_id = open
827 .reasoning
828 .clone()
829 .unwrap_or_else(|| format!("{}-reasoning-{}", event.id, index));
830 if is_partial {
831 translated.push(json!({
832 "type": "REASONING_MESSAGE_CHUNK",
833 "messageId": message_id,
834 "delta": thinking,
835 "timestamp": timestamp,
836 }));
837 } else {
838 let reasoning_id = format!("{}-reasoning-phase-{}", event.id, index);
839 if open.reasoning.is_none() {
840 translated.push(json!({
841 "type": "REASONING_START",
842 "messageId": reasoning_id,
843 "timestamp": timestamp,
844 }));
845 translated.push(json!({
846 "type": "REASONING_MESSAGE_START",
847 "messageId": message_id,
848 "role": "assistant",
849 "timestamp": timestamp,
850 }));
851 }
852 if !thinking.is_empty() {
853 translated.push(json!({
854 "type": "REASONING_MESSAGE_CONTENT",
855 "messageId": message_id,
856 "delta": thinking,
857 "timestamp": timestamp,
858 }));
859 }
860 translated.push(json!({
861 "type": "REASONING_MESSAGE_END",
862 "messageId": message_id,
863 "timestamp": timestamp,
864 }));
865 translated.push(json!({
866 "type": "REASONING_END",
867 "messageId": reasoning_id,
868 "timestamp": timestamp,
869 }));
870 }
871 }
872 adk_core::Part::FunctionCall { name, args, id, .. } => {
873 let tool_call_id =
874 id.clone().unwrap_or_else(|| format!("{}-tool-call-{}", event.id, index));
875 let raw_chunk_supported = is_partial && matches!(args, Value::String(_));
876 let args_delta = serialize_ag_ui_tool_call_delta(args, raw_chunk_supported);
877 if raw_chunk_supported {
878 translated.push(json!({
879 "type": "TOOL_CALL_CHUNK",
880 "toolCallId": tool_call_id,
881 "toolCallName": name,
882 "delta": args_delta,
883 "timestamp": timestamp,
884 }));
885 } else {
886 translated.push(json!({
887 "type": "TOOL_CALL_START",
888 "toolCallId": tool_call_id,
889 "toolCallName": name,
890 "timestamp": timestamp,
891 }));
892 translated.push(json!({
893 "type": "TOOL_CALL_ARGS",
894 "toolCallId": id.clone().unwrap_or_else(|| format!("{}-tool-call-{}", event.id, index)),
895 "delta": args_delta,
896 "timestamp": timestamp,
897 }));
898 translated.push(json!({
899 "type": "TOOL_CALL_END",
900 "toolCallId": id.clone().unwrap_or_else(|| format!("{}-tool-call-{}", event.id, index)),
901 "timestamp": timestamp,
902 }));
903 }
904 }
905 adk_core::Part::FunctionResponse { function_response, id, .. } => {
906 let tool_call_id =
907 id.clone().unwrap_or_else(|| format!("{}-tool-result-{}", event.id, index));
908 let response_content = serde_json::to_string(&function_response.response)
909 .unwrap_or_else(|_| function_response.response.to_string());
910 translated.push(json!({
911 "type": "TOOL_CALL_RESULT",
912 "messageId": format!("msg-{}", tool_call_id),
913 "toolCallId": tool_call_id,
914 "toolCallName": function_response.name,
915 "content": response_content,
916 "role": "tool",
917 "timestamp": timestamp,
918 }));
919 }
920 _ => {}
921 }
922 }
923
924 translated
925}
926
927async fn extract_request_context(
933 extractor: Option<&dyn RequestContextExtractor>,
934 headers: &HeaderMap,
935) -> Result<Option<RequestContext>, RuntimeError> {
936 let Some(extractor) = extractor else {
937 return Ok(None);
938 };
939
940 let mut builder = axum::http::Request::builder();
942 for (name, value) in headers {
943 builder = builder.header(name, value);
944 }
945 let (parts, _) = builder
946 .body(())
947 .map_err(|e| {
948 (StatusCode::INTERNAL_SERVER_ERROR, format!("failed to build request parts: {e}"))
949 })?
950 .into_parts();
951
952 match extractor.extract(&parts).await {
953 Ok(ctx) => Ok(Some(ctx)),
954 Err(RequestContextError::MissingAuth) => {
955 Err((StatusCode::UNAUTHORIZED, "missing authorization".to_string()))
956 }
957 Err(RequestContextError::InvalidToken(msg)) => {
958 Err((StatusCode::UNAUTHORIZED, format!("invalid token: {msg}")))
959 }
960 Err(RequestContextError::ExtractionFailed(msg)) => {
961 Err((StatusCode::INTERNAL_SERVER_ERROR, format!("auth extraction failed: {msg}")))
962 }
963 }
964}
965
966fn bridge_params_with_identity(
967 app_name: &str,
968 user_id: &str,
969 session_id: &str,
970 params: Option<Value>,
971) -> Value {
972 let mut object = params.and_then(|value| value.as_object().cloned()).unwrap_or_default();
973 object.insert("appName".to_string(), Value::String(app_name.to_string()));
974 object.insert("userId".to_string(), Value::String(user_id.to_string()));
975 object.insert("sessionId".to_string(), Value::String(session_id.to_string()));
976 Value::Object(object)
977}
978
979fn deserialize_bridge_params<T: for<'de> Deserialize<'de>>(
980 app_name: &str,
981 user_id: &str,
982 session_id: &str,
983 params: Option<Value>,
984) -> Result<T, RuntimeError> {
985 serde_json::from_value(bridge_params_with_identity(app_name, user_id, session_id, params))
986 .map_err(|error| {
987 (StatusCode::BAD_REQUEST, format!("invalid protocol-native bridge payload: {}", error))
988 })
989}
990
991fn maybe_mark_mcp_ui_initialized(
992 app_name: &str,
993 user_id: &str,
994 session_id: &str,
995 initialized_notification: Option<&Value>,
996) -> Result<(), RuntimeError> {
997 let Some(value) = initialized_notification else {
998 return Ok(());
999 };
1000 let method = value
1001 .as_object()
1002 .and_then(|object| object.get("method"))
1003 .and_then(|value| value.as_str())
1004 .unwrap_or_default();
1005 if method == "ui/notifications/initialized" {
1006 mark_mcp_ui_initialized(app_name, user_id, session_id)?;
1007 }
1008 Ok(())
1009}
1010
1011fn apply_mcp_apps_runtime_envelope(
1012 app_name: &str,
1013 user_id: &str,
1014 session_id: &str,
1015 envelope: McpAppsRuntimeEnvelope,
1016) -> Result<(), RuntimeError> {
1017 match envelope.method.as_str() {
1018 "ui/initialize" => {
1019 let params = deserialize_bridge_params::<McpUiInitializeParams>(
1020 app_name,
1021 user_id,
1022 session_id,
1023 envelope.params,
1024 )?;
1025 initialize_mcp_ui_bridge(params)?;
1026 Ok(())
1027 }
1028 "ui/message" => {
1029 let params = deserialize_bridge_params::<McpUiMessageParams>(
1030 app_name,
1031 user_id,
1032 session_id,
1033 envelope.params,
1034 )?;
1035 message_mcp_ui_bridge(params)?;
1036 Ok(())
1037 }
1038 "ui/update-model-context" => {
1039 let params = deserialize_bridge_params::<McpUiUpdateModelContextParams>(
1040 app_name,
1041 user_id,
1042 session_id,
1043 envelope.params,
1044 )?;
1045 update_mcp_ui_bridge_model_context(params)?;
1046 Ok(())
1047 }
1048 "ui/notifications/initialized" => {
1049 mark_mcp_ui_initialized(app_name, user_id, session_id)?;
1050 Ok(())
1051 }
1052 method => Err((
1053 StatusCode::BAD_REQUEST,
1054 format!("unsupported MCP Apps runtime bridge method '{}'", method),
1055 )),
1056 }
1057}
1058
1059#[derive(Default)]
1060struct AgUiMessages {
1061 text: Option<String>,
1062 reasoning: Option<String>,
1063}
1064
1065#[derive(Default)]
1066struct AgUiStream {
1067 deltas: adk_core::EventTextDeltas,
1068 messages: HashMap<(String, String), AgUiMessages>,
1069}
1070
1071impl AgUiStream {
1072 fn translate(&mut self, event: &adk_core::Event, thread_id: &str, run_id: &str) -> Vec<Value> {
1073 let key = (event.invocation_id.clone(), event.id.clone());
1074 let mut open = self.messages.remove(&key).unwrap_or_default();
1075 let mut delta = self.deltas.push(event);
1076 if !event.llm_response.partial {
1077 let parts = delta.content().map(|content| content.parts.as_slice()).unwrap_or(&[]);
1078 let mut endings = Vec::new();
1079 if open.text.is_some()
1080 && !parts.iter().any(|part| matches!(part, adk_core::Part::Text { .. }))
1081 {
1082 endings.push(adk_core::Part::Text { text: String::new() });
1083 }
1084 if open.reasoning.is_some()
1085 && !parts.iter().any(|part| matches!(part, adk_core::Part::Thinking { .. }))
1086 {
1087 endings.push(adk_core::Part::Thinking { thinking: String::new(), signature: None });
1088 }
1089 if !endings.is_empty() {
1090 delta
1091 .to_mut()
1092 .llm_response
1093 .content
1094 .get_or_insert_with(|| adk_core::Content::new("model"))
1095 .parts
1096 .extend(endings);
1097 }
1098 }
1099 let translated = translate_ag_ui_event(&delta, thread_id, run_id, &open);
1100 if event.llm_response.partial {
1101 for item in &translated {
1102 let id = item.get("messageId").and_then(Value::as_str).map(str::to_owned);
1103 match item.get("type").and_then(Value::as_str) {
1104 Some("TEXT_MESSAGE_CHUNK") => open.text = id,
1105 Some("REASONING_MESSAGE_CHUNK") => open.reasoning = id,
1106 _ => {}
1107 }
1108 }
1109 if open.text.is_some() || open.reasoning.is_some() {
1110 self.messages.insert(key, open);
1111 }
1112 }
1113 translated
1114 }
1115}
1116
1117fn build_runtime_sse_stream<S>(
1118 mut event_stream: S,
1119 profile: UiProfile,
1120 transport: UiTransportMode,
1121 session_id: String,
1122 ag_ui_input: Option<AgUiRunInput>,
1123) -> std::pin::Pin<Box<dyn Stream<Item = Result<Event, Infallible>> + Send>>
1124where
1125 S: Stream<Item = adk_core::Result<adk_core::Event>> + Send + 'static + Unpin,
1126{
1127 let selected_thread_id =
1128 ag_ui_input.as_ref().and_then(|input| input.thread_id.clone()).unwrap_or(session_id);
1129 let selected_run_input = ag_ui_input.clone();
1130 let selected_parent_run_id = ag_ui_input.as_ref().and_then(|input| input.parent_run_id.clone());
1131 let selected_initial_state = ag_ui_input.as_ref().and_then(|input| input.state.clone());
1132 let selected_messages_snapshot =
1133 ag_ui_input.as_ref().and_then(messages_snapshot_from_ag_ui_input);
1134 let selected_activity_events =
1135 ag_ui_input.as_ref().map(activity_events_from_ag_ui_input).unwrap_or_default();
1136
1137 Box::pin(async_stream::stream! {
1138 let native_ag_ui = profile == UiProfile::AgUi && transport == UiTransportMode::ProtocolNative;
1139 let mut started = false;
1140 let mut ag_ui_stream = AgUiStream::default();
1141 let mut active_run_id = ag_ui_input.as_ref().and_then(|input| input.run_id.clone());
1142
1143 while let Some(item) = event_stream.next().await {
1144 match item {
1145 Ok(event) => {
1146 if native_ag_ui {
1147 let run_id = active_run_id
1148 .get_or_insert_with(|| event.invocation_id.clone())
1149 .clone();
1150 if !started {
1151 let mut started_event = json!({
1152 "type": "RUN_STARTED",
1153 "threadId": selected_thread_id,
1154 "runId": run_id,
1155 });
1156 if let Some(parent_run_id) = selected_parent_run_id.clone()
1157 && let Some(object) = started_event.as_object_mut()
1158 {
1159 object.insert("parentRunId".to_string(), Value::String(parent_run_id));
1160 }
1161 if let Some(run_input) = selected_run_input.clone()
1162 && let Ok(value) = serde_json::to_value(run_input)
1163 && let Some(object) = started_event.as_object_mut()
1164 {
1165 object.insert("input".to_string(), value);
1166 }
1167 yield Ok(Event::default().data(started_event.to_string()));
1168
1169 if let Some(snapshot) = selected_initial_state.clone() {
1170 yield Ok(Event::default().data(json!({
1171 "type": "STATE_SNAPSHOT",
1172 "snapshot": snapshot,
1173 }).to_string()));
1174 }
1175 if let Some(messages) = selected_messages_snapshot.clone() {
1176 yield Ok(Event::default().data(json!({
1177 "type": "MESSAGES_SNAPSHOT",
1178 "messages": messages,
1179 }).to_string()));
1180 }
1181 for activity_event in selected_activity_events.clone() {
1182 yield Ok(Event::default().data(activity_event.to_string()));
1183 }
1184 started = true;
1185 }
1186
1187 for payload in ag_ui_stream.translate(&event, &selected_thread_id, &run_id) {
1188 yield Ok(Event::default().data(payload.to_string()));
1189 }
1190 } else if let Some(payload) = serialize_runtime_event(&event, profile) {
1191 yield Ok(Event::default().data(payload));
1192 }
1193 }
1194 Err(error) => {
1195 if native_ag_ui {
1196 let run_id =
1197 active_run_id.unwrap_or_else(|| format!("run-{}", Uuid::new_v4()));
1198 if !started {
1199 yield Ok(Event::default().data(json!({
1200 "type": "RUN_STARTED",
1201 "threadId": selected_thread_id,
1202 "runId": run_id,
1203 }).to_string()));
1204 }
1205 yield Ok(Event::default().data(json!({
1206 "type": "RUN_ERROR",
1207 "threadId": selected_thread_id,
1208 "runId": run_id,
1209 "message": error.to_string(),
1210 }).to_string()));
1211 } else if let Some(payload) = serialize_runtime_error(&error, profile) {
1212 yield Ok(Event::default().data(payload));
1213 }
1214 return;
1215 }
1216 }
1217 }
1218
1219 if native_ag_ui {
1220 let run_id = active_run_id.unwrap_or_else(|| format!("run-{}", Uuid::new_v4()));
1221 if !started {
1222 yield Ok(Event::default().data(json!({
1223 "type": "RUN_STARTED",
1224 "threadId": selected_thread_id,
1225 "runId": run_id,
1226 }).to_string()));
1227 }
1228 yield Ok(Event::default().data(json!({
1229 "type": "RUN_FINISHED",
1230 "threadId": selected_thread_id,
1231 "runId": run_id,
1232 }).to_string()));
1233 }
1234 })
1235}
1236
1237pub async fn run_sse(
1238 State(controller): State<RuntimeController>,
1239 Path((app_name, user_id, session_id)): Path<(String, String, String)>,
1240 headers: HeaderMap,
1241 Json(req): Json<RunRequest>,
1242) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, RuntimeError> {
1243 let ui_profile = resolve_ui_profile(&headers, infer_run_request_protocol(&req))?;
1244 let transport = resolve_ui_transport(&headers, req.ui_transport.as_deref())?;
1245 validate_transport_support(ui_profile, transport)?;
1246 let span = tracing::info_span!("run_sse", session_id = %session_id, app_name = %app_name, user_id = %user_id);
1247
1248 async move {
1249 log_profile_deprecation(ui_profile);
1250 info!(
1251 ui_protocol = %ui_profile.as_str(),
1252 ui_transport = ?transport,
1253 "resolved ui protocol profile for runtime request"
1254 );
1255
1256 let request_context = extract_request_context(
1260 controller.config.request_context_extractor.as_deref(),
1261 &headers,
1262 )
1263 .await?;
1264
1265 let effective_user_id = request_context.as_ref().map_or(user_id, |rc| rc.user_id.clone());
1271
1272 controller
1274 .config
1275 .session_service
1276 .get(adk_session::GetRequest {
1277 app_name: app_name.clone(),
1278 user_id: effective_user_id.clone(),
1279 session_id: session_id.clone(),
1280 num_recent_events: None,
1281 after: None,
1282 })
1283 .await
1284 .map_err(|_| (StatusCode::NOT_FOUND, "session not found".to_string()))?;
1285
1286 let agent = controller
1288 .config
1289 .agent_loader
1290 .load_agent(&app_name)
1291 .await
1292 .map_err(adk_err_to_runtime)?;
1293
1294 let mut runner_builder = adk_runner::Runner::builder()
1296 .app_name(app_name.clone())
1297 .agent(agent)
1298 .session_service(controller.config.session_service.clone());
1299 if let Some(ref artifact_service) = controller.config.artifact_service {
1300 runner_builder = runner_builder.artifact_service(artifact_service.clone());
1301 }
1302 if let Some(ref memory_service) = controller.config.memory_service {
1303 runner_builder = runner_builder.memory_service(memory_service.clone());
1304 }
1305 if let Some(ref compaction_config) = controller.config.compaction_config {
1306 runner_builder = runner_builder.compaction_config(compaction_config.clone());
1307 }
1308 if let Some(ref context_cache_config) = controller.config.context_cache_config {
1309 runner_builder = runner_builder.context_cache_config(context_cache_config.clone());
1310 }
1311 if let Some(ref cache_capable) = controller.config.cache_capable {
1312 runner_builder = runner_builder.cache_capable(cache_capable.clone());
1313 }
1314 if let Some(request_context) = request_context {
1315 runner_builder = runner_builder.request_context(request_context);
1316 }
1317 let runner = runner_builder.build().map_err(adk_err_to_runtime)?;
1318
1319 let content = build_content_with_attachments(&req.new_message, &req.attachments)?;
1321
1322 if !req.attachments.is_empty() {
1324 info!(attachment_count = req.attachments.len(), "processing request with attachments");
1325 }
1326
1327 let typed_user_id =
1329 UserId::new(effective_user_id).map_err(|err| adk_err_to_runtime(err.into()))?;
1330 let typed_session_id =
1331 SessionId::new(session_id.clone()).map_err(|err| adk_err_to_runtime(err.into()))?;
1332 let event_stream = runner
1333 .run(typed_user_id, typed_session_id, content)
1334 .await
1335 .map_err(adk_err_to_runtime)?;
1336
1337 let sse_stream =
1339 build_runtime_sse_stream(event_stream, ui_profile, transport, session_id.clone(), None);
1340
1341 Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
1342 }
1343 .instrument(span)
1344 .await
1345}
1346
1347pub async fn run_sse_compat(
1350 State(controller): State<RuntimeController>,
1351 headers: HeaderMap,
1352 Json(req): Json<RunSseRequest>,
1353) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, RuntimeError> {
1354 let ui_profile = resolve_ui_profile(&headers, infer_sse_request_protocol(&req))?;
1355 let transport = resolve_ui_transport(&headers, req.ui_transport.as_deref())?;
1356 validate_transport_support(ui_profile, transport)?;
1357 let app_name = req.app_name.clone();
1358 let user_id = req.user_id.clone();
1359 let session_id = req.session_id.clone();
1360 let ag_ui_input = ag_ui_input_from_request(&req);
1361 let mcp_apps_request = mcp_apps_request_from_request(&req);
1362 let mcp_apps_initialize = mcp_apps_initialize_from_request(&req);
1363
1364 info!(
1365 app_name = %app_name,
1366 user_id = %user_id,
1367 session_id = %session_id,
1368 ui_protocol = %ui_profile.as_str(),
1369 ui_transport = ?transport,
1370 "POST /run_sse request received"
1371 );
1372 log_profile_deprecation(ui_profile);
1373
1374 let request_context =
1378 extract_request_context(controller.config.request_context_extractor.as_deref(), &headers)
1379 .await?;
1380
1381 let effective_user_id = request_context.as_ref().map_or(user_id, |rc| rc.user_id.clone());
1387
1388 let resolved_new_message = req
1389 .new_message
1390 .clone()
1391 .or_else(|| ag_ui_input.as_ref().and_then(new_message_from_ag_ui_input))
1392 .ok_or_else(|| {
1393 (
1394 StatusCode::BAD_REQUEST,
1395 "newMessage is required unless protocol-native ag_ui input supplies a user message"
1396 .to_string(),
1397 )
1398 })?;
1399
1400 let content = build_content_from_parts(&resolved_new_message.parts)?;
1402
1403 let text_parts: Vec<_> =
1405 resolved_new_message.parts.iter().filter(|p| p.text.is_some()).collect();
1406 let data_parts: Vec<_> =
1407 resolved_new_message.parts.iter().filter(|p| p.inline_data.is_some()).collect();
1408 if !data_parts.is_empty() {
1409 info!(
1410 text_parts = text_parts.len(),
1411 inline_data_parts = data_parts.len(),
1412 "processing request with inline data"
1413 );
1414 }
1415
1416 let merged_state_delta = merge_runtime_state_delta(
1417 body_state_delta(req.state_delta.as_ref())?,
1418 ag_ui_input.as_ref().map(ag_ui_state_delta).unwrap_or_default(),
1419 );
1420
1421 let session_result = controller
1423 .config
1424 .session_service
1425 .get(adk_session::GetRequest {
1426 app_name: app_name.clone(),
1427 user_id: effective_user_id.clone(),
1428 session_id: session_id.clone(),
1429 num_recent_events: None,
1430 after: None,
1431 })
1432 .await;
1433
1434 if session_result.is_err() {
1436 controller
1437 .config
1438 .session_service
1439 .create(adk_session::CreateRequest {
1440 app_name: app_name.clone(),
1441 user_id: effective_user_id.clone(),
1442 session_id: Some(session_id.clone()),
1443 state: merged_state_delta.clone(),
1444 })
1445 .await
1446 .map_err(adk_err_to_runtime)?;
1447 } else {
1448 apply_state_delta_to_session(
1449 &controller.config.session_service,
1450 &app_name,
1451 &effective_user_id,
1452 &session_id,
1453 merged_state_delta.clone(),
1454 )
1455 .await?;
1456 }
1457
1458 if ui_profile == UiProfile::McpApps {
1459 if let Some(initialize) = mcp_apps_initialize {
1460 apply_mcp_apps_runtime_envelope(
1461 &app_name,
1462 &effective_user_id,
1463 &session_id,
1464 initialize,
1465 )?;
1466 }
1467 if let Some(request) = mcp_apps_request {
1468 apply_mcp_apps_runtime_envelope(&app_name, &effective_user_id, &session_id, request)?;
1469 }
1470 maybe_mark_mcp_ui_initialized(
1471 &app_name,
1472 &effective_user_id,
1473 &session_id,
1474 req.mcp_apps_initialized.as_ref(),
1475 )?;
1476 }
1477
1478 let agent =
1480 controller.config.agent_loader.load_agent(&app_name).await.map_err(adk_err_to_runtime)?;
1481
1482 let streaming_mode =
1484 if req.streaming { adk_core::StreamingMode::SSE } else { adk_core::StreamingMode::None };
1485
1486 let mut runner_builder = adk_runner::Runner::builder()
1487 .app_name(app_name)
1488 .agent(agent)
1489 .session_service(controller.config.session_service.clone())
1490 .run_config(adk_core::RunConfig::builder().streaming_mode(streaming_mode).build());
1491 if let Some(ref artifact_service) = controller.config.artifact_service {
1492 runner_builder = runner_builder.artifact_service(artifact_service.clone());
1493 }
1494 if let Some(ref memory_service) = controller.config.memory_service {
1495 runner_builder = runner_builder.memory_service(memory_service.clone());
1496 }
1497 if let Some(ref compaction_config) = controller.config.compaction_config {
1498 runner_builder = runner_builder.compaction_config(compaction_config.clone());
1499 }
1500 if let Some(ref context_cache_config) = controller.config.context_cache_config {
1501 runner_builder = runner_builder.context_cache_config(context_cache_config.clone());
1502 }
1503 if let Some(ref cache_capable) = controller.config.cache_capable {
1504 runner_builder = runner_builder.cache_capable(cache_capable.clone());
1505 }
1506 if let Some(request_context) = request_context {
1507 runner_builder = runner_builder.request_context(request_context);
1508 }
1509 let runner = runner_builder.build().map_err(adk_err_to_runtime)?;
1510
1511 let typed_user_id =
1513 UserId::new(effective_user_id).map_err(|err| adk_err_to_runtime(err.into()))?;
1514 let typed_session_id =
1515 SessionId::new(session_id.clone()).map_err(|err| adk_err_to_runtime(err.into()))?;
1516 let event_stream =
1517 runner.run(typed_user_id, typed_session_id, content).await.map_err(adk_err_to_runtime)?;
1518
1519 let sse_stream = build_runtime_sse_stream(
1521 event_stream,
1522 ui_profile,
1523 transport,
1524 session_id.clone(),
1525 ag_ui_input,
1526 );
1527
1528 Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
1529}
1530
1531pub async fn run_collect(
1538 State(controller): State<RuntimeController>,
1539 headers: HeaderMap,
1540 Json(req): Json<RunSseRequest>,
1541) -> Result<Json<Vec<adk_core::Event>>, RuntimeError> {
1542 let app_name = req.app_name.clone();
1543 let user_id = req.user_id.clone();
1544 let session_id = req.session_id.clone();
1545
1546 info!(
1547 app_name = %app_name,
1548 user_id = %user_id,
1549 session_id = %session_id,
1550 "POST /run request received"
1551 );
1552
1553 let request_context =
1557 extract_request_context(controller.config.request_context_extractor.as_deref(), &headers)
1558 .await?;
1559
1560 let effective_user_id = request_context.as_ref().map_or(user_id, |rc| rc.user_id.clone());
1566
1567 let new_message = req
1568 .new_message
1569 .as_ref()
1570 .ok_or_else(|| (StatusCode::BAD_REQUEST, "newMessage is required".to_string()))?;
1571
1572 let content = build_content_from_parts(&new_message.parts)?;
1574
1575 let state_delta = body_state_delta(req.state_delta.as_ref())?;
1576
1577 let session_result = controller
1579 .config
1580 .session_service
1581 .get(adk_session::GetRequest {
1582 app_name: app_name.clone(),
1583 user_id: effective_user_id.clone(),
1584 session_id: session_id.clone(),
1585 num_recent_events: None,
1586 after: None,
1587 })
1588 .await;
1589
1590 if session_result.is_err() {
1591 controller
1592 .config
1593 .session_service
1594 .create(adk_session::CreateRequest {
1595 app_name: app_name.clone(),
1596 user_id: effective_user_id.clone(),
1597 session_id: Some(session_id.clone()),
1598 state: state_delta.clone(),
1599 })
1600 .await
1601 .map_err(adk_err_to_runtime)?;
1602 } else {
1603 apply_state_delta_to_session(
1604 &controller.config.session_service,
1605 &app_name,
1606 &effective_user_id,
1607 &session_id,
1608 state_delta,
1609 )
1610 .await?;
1611 }
1612
1613 let agent =
1615 controller.config.agent_loader.load_agent(&app_name).await.map_err(adk_err_to_runtime)?;
1616
1617 let mut runner_builder = adk_runner::Runner::builder()
1619 .app_name(app_name)
1620 .agent(agent)
1621 .session_service(controller.config.session_service.clone())
1622 .run_config(
1623 adk_core::RunConfig::builder().streaming_mode(adk_core::StreamingMode::None).build(),
1624 );
1625 if let Some(ref artifact_service) = controller.config.artifact_service {
1626 runner_builder = runner_builder.artifact_service(artifact_service.clone());
1627 }
1628 if let Some(ref memory_service) = controller.config.memory_service {
1629 runner_builder = runner_builder.memory_service(memory_service.clone());
1630 }
1631 if let Some(ref compaction_config) = controller.config.compaction_config {
1632 runner_builder = runner_builder.compaction_config(compaction_config.clone());
1633 }
1634 if let Some(ref context_cache_config) = controller.config.context_cache_config {
1635 runner_builder = runner_builder.context_cache_config(context_cache_config.clone());
1636 }
1637 if let Some(ref cache_capable) = controller.config.cache_capable {
1638 runner_builder = runner_builder.cache_capable(cache_capable.clone());
1639 }
1640 if let Some(request_context) = request_context {
1641 runner_builder = runner_builder.request_context(request_context);
1642 }
1643 let runner = runner_builder.build().map_err(adk_err_to_runtime)?;
1644
1645 let typed_user_id =
1647 UserId::new(effective_user_id).map_err(|err| adk_err_to_runtime(err.into()))?;
1648 let typed_session_id =
1649 SessionId::new(session_id).map_err(|err| adk_err_to_runtime(err.into()))?;
1650 let mut event_stream =
1651 runner.run(typed_user_id, typed_session_id, content).await.map_err(adk_err_to_runtime)?;
1652
1653 let mut events = Vec::new();
1654 while let Some(event) = event_stream.next().await {
1655 events.push(event.map_err(adk_err_to_runtime)?);
1656 }
1657
1658 info!(event_count = events.len(), "POST /run completed");
1659
1660 Ok(Json(events))
1661}
1662
1663#[cfg(test)]
1664#[path = "runtime_stream_tests.rs"]
1665mod stream_tests;