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 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
564/// Build Content from message text and attachments
565fn 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    // Add the text part
572    content.parts.push(adk_core::Part::Text { text: text.to_string() });
573
574    // Add attachment parts
575    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
607/// Build Content from message parts (for /run_sse endpoint)
608fn 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        // Add text part if present
613        if let Some(text) = &part.text {
614            content.parts.push(adk_core::Part::Text { text: text.clone() });
615        }
616
617        // Add inline data part if present
618        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
927/// Extract [`RequestContext`] from the configured extractor, if present.
928///
929/// Constructs minimal HTTP request [`Parts`] from the provided headers so the
930/// extractor can inspect `Authorization` and other headers. Returns `None`
931/// when no extractor is configured (fall-through to existing behavior).
932async 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    // Build minimal Parts from the headers
941    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        // Extract request context from auth middleware bridge if configured.
1257        // This returns Err (401/500) when the extractor is present but auth
1258        // fails, ensuring authorization checks are never bypassed.
1259        let request_context = extract_request_context(
1260            controller.config.request_context_extractor.as_deref(),
1261            &headers,
1262        )
1263        .await?;
1264
1265        // Explicit authenticated user override: when an auth extractor is
1266        // configured and succeeds, the authenticated user_id takes precedence
1267        // over the path parameter. This prevents callers from impersonating
1268        // other users via the URL while keeping the path param as a fallback
1269        // for unauthenticated deployments (no extractor configured).
1270        let effective_user_id = request_context.as_ref().map_or(user_id, |rc| rc.user_id.clone());
1271
1272        // Validate session exists
1273        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        // Load agent
1287        let agent = controller
1288            .config
1289            .agent_loader
1290            .load_agent(&app_name)
1291            .await
1292            .map_err(adk_err_to_runtime)?;
1293
1294        // Create runner
1295        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        // Build content with attachments
1320        let content = build_content_with_attachments(&req.new_message, &req.attachments)?;
1321
1322        // Log attachment info
1323        if !req.attachments.is_empty() {
1324            info!(attachment_count = req.attachments.len(), "processing request with attachments");
1325        }
1326
1327        // Run agent
1328        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        // Convert to SSE stream
1338        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
1347/// POST /run_sse - adk-go compatible endpoint
1348/// Accepts JSON body with appName, userId, sessionId, newMessage
1349pub 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    // Extract request context from auth middleware bridge if configured.
1375    // This returns Err (401/500) when the extractor is present but auth
1376    // fails, ensuring authorization checks are never bypassed.
1377    let request_context =
1378        extract_request_context(controller.config.request_context_extractor.as_deref(), &headers)
1379            .await?;
1380
1381    // Explicit authenticated user override: when an auth extractor is
1382    // configured and succeeds, the authenticated user_id takes precedence
1383    // over the request body value. This prevents callers from impersonating
1384    // other users via the JSON payload while keeping the body param as a
1385    // fallback for unauthenticated deployments (no extractor configured).
1386    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    // Build content from message parts (includes both text and inline_data)
1401    let content = build_content_from_parts(&resolved_new_message.parts)?;
1402
1403    // Log part info
1404    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    // Validate session exists or create it
1422    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 doesn't exist, create it
1435    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    // Load agent
1479    let agent =
1480        controller.config.agent_loader.load_agent(&app_name).await.map_err(adk_err_to_runtime)?;
1481
1482    // Create runner with streaming config from request
1483    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    // Run agent with full content (text + inline data)
1512    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    // Convert to SSE stream
1520    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
1531/// POST /run - plain-JSON, non-streaming endpoint (Google ADK api_server parity)
1532///
1533/// Accepts the same JSON body as `POST /run_sse`, runs the agent to
1534/// completion, and returns every produced event as a JSON array instead of
1535/// an SSE stream. The `streaming` body field is ignored — the run always
1536/// uses `StreamingMode::None`.
1537pub 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    // Extract request context from auth middleware bridge if configured.
1554    // This returns Err (401/500) when the extractor is present but auth
1555    // fails, ensuring authorization checks are never bypassed.
1556    let request_context =
1557        extract_request_context(controller.config.request_context_extractor.as_deref(), &headers)
1558            .await?;
1559
1560    // Explicit authenticated user override: when an auth extractor is
1561    // configured and succeeds, the authenticated user_id takes precedence
1562    // over the request body value. This prevents callers from impersonating
1563    // other users via the JSON payload while keeping the body param as a
1564    // fallback for unauthenticated deployments (no extractor configured).
1565    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    // Build content from message parts (includes both text and inline_data)
1573    let content = build_content_from_parts(&new_message.parts)?;
1574
1575    let state_delta = body_state_delta(req.state_delta.as_ref())?;
1576
1577    // Validate session exists or create it (same behavior as /run_sse)
1578    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    // Load agent
1614    let agent =
1615        controller.config.agent_loader.load_agent(&app_name).await.map_err(adk_err_to_runtime)?;
1616
1617    // Create runner — always non-streaming for the plain-JSON endpoint
1618    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    // Run agent to completion, collecting every event
1646    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;