Skip to main content

adk_server/rest/controllers/
runtime.rs

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/// Attachment structure for the legacy /run endpoint
46#[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/// Request format for /run_sse (adk-go compatible)
68#[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
203/// Convert an `AdkError` into a `RuntimeError` using the structured error envelope.
204///
205/// Uses `AdkError::http_status_code()` for the HTTP status and
206/// `AdkError::to_problem_json()` for the response body. The problem JSON
207/// includes `retry_after_ms` when the error carries retry guidance.
208fn 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
557/// Build Content from message text and attachments
558fn 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    // Add the text part
565    content.parts.push(adk_core::Part::Text { text: text.to_string() });
566
567    // Add attachment parts
568    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
600/// Build Content from message parts (for /run_sse endpoint)
601fn 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        // Add text part if present
606        if let Some(text) = &part.text {
607            content.parts.push(adk_core::Part::Text { text: text.clone() });
608        }
609
610        // Add inline data part if present
611        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
877/// Extract [`RequestContext`] from the configured extractor, if present.
878///
879/// Constructs minimal HTTP request [`Parts`] from the provided headers so the
880/// extractor can inspect `Authorization` and other headers. Returns `None`
881/// when no extractor is configured (fall-through to existing behavior).
882async 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    // Build minimal Parts from the headers
891    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        // Extract request context from auth middleware bridge if configured.
1153        // This returns Err (401/500) when the extractor is present but auth
1154        // fails, ensuring authorization checks are never bypassed.
1155        let request_context = extract_request_context(
1156            controller.config.request_context_extractor.as_deref(),
1157            &headers,
1158        )
1159        .await?;
1160
1161        // Explicit authenticated user override: when an auth extractor is
1162        // configured and succeeds, the authenticated user_id takes precedence
1163        // over the path parameter. This prevents callers from impersonating
1164        // other users via the URL while keeping the path param as a fallback
1165        // for unauthenticated deployments (no extractor configured).
1166        let effective_user_id = request_context.as_ref().map_or(user_id, |rc| rc.user_id.clone());
1167
1168        // Validate session exists
1169        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        // Load agent
1183        let agent = controller
1184            .config
1185            .agent_loader
1186            .load_agent(&app_name)
1187            .await
1188            .map_err(adk_err_to_runtime)?;
1189
1190        // Create runner
1191        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        // Build content with attachments
1216        let content = build_content_with_attachments(&req.new_message, &req.attachments)?;
1217
1218        // Log attachment info
1219        if !req.attachments.is_empty() {
1220            info!(attachment_count = req.attachments.len(), "processing request with attachments");
1221        }
1222
1223        // Run agent
1224        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        // Convert to SSE stream
1234        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
1243/// POST /run_sse - adk-go compatible endpoint
1244/// Accepts JSON body with appName, userId, sessionId, newMessage
1245pub 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    // Extract request context from auth middleware bridge if configured.
1271    // This returns Err (401/500) when the extractor is present but auth
1272    // fails, ensuring authorization checks are never bypassed.
1273    let request_context =
1274        extract_request_context(controller.config.request_context_extractor.as_deref(), &headers)
1275            .await?;
1276
1277    // Explicit authenticated user override: when an auth extractor is
1278    // configured and succeeds, the authenticated user_id takes precedence
1279    // over the request body value. This prevents callers from impersonating
1280    // other users via the JSON payload while keeping the body param as a
1281    // fallback for unauthenticated deployments (no extractor configured).
1282    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    // Build content from message parts (includes both text and inline_data)
1297    let content = build_content_from_parts(&resolved_new_message.parts)?;
1298
1299    // Log part info
1300    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    // Validate session exists or create it
1318    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 doesn't exist, create it
1331    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    // Load agent
1375    let agent =
1376        controller.config.agent_loader.load_agent(&app_name).await.map_err(adk_err_to_runtime)?;
1377
1378    // Create runner with streaming config from request
1379    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    // Run agent with full content (text + inline data)
1408    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    // Convert to SSE stream
1416    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}