diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 1b4234566f..799269c88e 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 43a1a0071a..8ddf160c71 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -4009,6 +4009,7 @@ mod tests { codex_responses_as_items: None, codex_response_item_prefix: None, codex_response_handoff_mode: Some(CodexResponseHandoffMode::BemTags), + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: Some(std::collections::BTreeMap::from([ ("analysis".to_string(), vec!["[THINKING]".to_string()]), ( @@ -4095,6 +4096,7 @@ mod tests { codex_responses_as_items: None, codex_response_item_prefix: None, codex_response_handoff_mode: None, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, thread_id: "thr_123".to_string(), model: None, @@ -4147,6 +4149,7 @@ mod tests { codex_responses_as_items: None, codex_response_item_prefix: None, codex_response_handoff_mode: None, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, thread_id: "thr_123".to_string(), model: None, @@ -4399,6 +4402,7 @@ mod tests { codex_responses_as_items: None, codex_response_item_prefix: None, codex_response_handoff_mode: None, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, thread_id: "thr_123".to_string(), model: None, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs b/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs index 4bba26856c..fa79c1fd87 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs @@ -220,6 +220,9 @@ pub struct ThreadRealtimeStartParams { /// default to `thinking`. Realtime V1 and V2 ignore this setting. #[ts(optional = nullable)] pub codex_response_handoff_mode: Option, + /// Relays public reasoning summaries as quiet context for realtime V3 delegations. Defaults to false. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub backend_reasoning_status: bool, /// Overrides BEM channel prefixes by `analysis`, `commentary`, or `final`. /// Omitted channels retain their default uppercase bracketed prefixes. #[ts(optional = nullable)] diff --git a/codex-rs/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs index df3b049d81..243f4b122c 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -1238,6 +1238,7 @@ impl TurnRequestProcessor { codex_responses_as_items: params.codex_responses_as_items.unwrap_or(false), codex_response_item_prefix: params.codex_response_item_prefix, codex_response_handoff_mode: params.codex_response_handoff_mode.unwrap_or_default(), + backend_reasoning_status: params.backend_reasoning_status, codex_response_handoff_channel_prefixes: params .codex_response_handoff_channel_prefixes, model: params.model, diff --git a/codex-rs/app-server/tests/suite/v2/experimental_api.rs b/codex-rs/app-server/tests/suite/v2/experimental_api.rs index b849e6d665..1c119d04ba 100644 --- a/codex-rs/app-server/tests/suite/v2/experimental_api.rs +++ b/codex-rs/app-server/tests/suite/v2/experimental_api.rs @@ -99,6 +99,7 @@ async fn realtime_conversation_start_requires_experimental_api_capability() -> R codex_responses_as_items: None, codex_response_item_prefix: None, codex_response_handoff_mode: None, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, thread_id: "thr_123".to_string(), model: None, @@ -240,6 +241,7 @@ async fn realtime_webrtc_start_requires_experimental_api_capability() -> Result< codex_responses_as_items: None, codex_response_item_prefix: None, codex_response_handoff_mode: None, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, thread_id: "thr_123".to_string(), model: None, diff --git a/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs b/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs index 112422da70..9ab13cfb5f 100644 --- a/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs +++ b/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs @@ -393,6 +393,7 @@ impl RealtimeE2eHarness { .unwrap_or(false) .then(|| RESPONSE_ITEM_PREFIX.to_string()), codex_response_handoff_mode, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, codex_responses_as_items, model: None, @@ -454,6 +455,7 @@ impl RealtimeE2eHarness { .unwrap_or(false) .then(|| RESPONSE_ITEM_PREFIX.to_string()), codex_response_handoff_mode: None, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, codex_responses_as_items, model: None, @@ -491,6 +493,7 @@ impl RealtimeE2eHarness { flush_transcript_tail_on_session_end: None, codex_response_item_prefix: None, codex_response_handoff_mode, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes, codex_responses_as_items: None, model: None, @@ -709,6 +712,7 @@ async fn realtime_conversation_streams_timeline_items() -> Result<()> { codex_responses_as_items: None, codex_response_item_prefix: None, codex_response_handoff_mode: None, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -905,6 +909,7 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> { codex_responses_as_items: None, codex_response_item_prefix: None, codex_response_handoff_mode: None, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, thread_id: thread_start.thread.id.clone(), model: Some("realtime-treatment-model".to_string()), @@ -1474,6 +1479,7 @@ async fn realtime_start_can_skip_startup_context() -> Result<()> { codex_responses_as_items: None, codex_response_item_prefix: None, codex_response_handoff_mode: None, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, thread_id: thread_start.thread.id.clone(), model: None, @@ -1571,6 +1577,7 @@ async fn realtime_text_output_modality_requests_text_output_and_final_transcript codex_responses_as_items: None, codex_response_item_prefix: None, codex_response_handoff_mode: None, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, thread_id: thread_start.thread.id.clone(), model: None, @@ -1748,6 +1755,7 @@ async fn realtime_conversation_stop_emits_closed_notification() -> Result<()> { codex_responses_as_items: None, codex_response_item_prefix: None, codex_response_handoff_mode: None, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, thread_id: thread_start.thread.id.clone(), model: None, @@ -1822,6 +1830,7 @@ async fn realtime_mode_uses_client_instructions_on_entry_and_exit() -> Result<() codex_responses_as_items: None, codex_response_item_prefix: None, codex_response_handoff_mode: None, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -2106,6 +2115,7 @@ async fn realtime_webrtc_start_emits_sdp_notification() -> Result<()> { codex_responses_as_items: None, codex_response_item_prefix: None, codex_response_handoff_mode: None, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, thread_id: thread_id.clone(), model: None, @@ -2324,6 +2334,7 @@ async fn existing_call_attaches_without_reinitializing_the_client_session( flush_transcript_tail_on_session_end: None, codex_response_item_prefix: None, codex_response_handoff_mode: None, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, codex_responses_as_items: None, model: None, @@ -2412,6 +2423,7 @@ async fn existing_call_rejects_client_owned_session_configuration(option: &str) flush_transcript_tail_on_session_end: None, codex_response_item_prefix: None, codex_response_handoff_mode: None, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, codex_responses_as_items: None, model: None, @@ -3931,6 +3943,7 @@ async fn realtime_webrtc_start_surfaces_backend_error() -> Result<()> { codex_responses_as_items: None, codex_response_item_prefix: None, codex_response_handoff_mode: None, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, thread_id: thread_start.thread.id, model: None, diff --git a/codex-rs/core/src/realtime_conversation.rs b/codex-rs/core/src/realtime_conversation.rs index 29ddc53466..eebea1fc75 100644 --- a/codex-rs/core/src/realtime_conversation.rs +++ b/codex-rs/core/src/realtime_conversation.rs @@ -100,6 +100,7 @@ const OUTPUT_EVENTS_QUEUE_CAPACITY: usize = 256; const REALTIME_THREAD_SOURCE_MAX_BYTES: usize = 256; const REALTIME_STARTUP_CONTEXT_TOKEN_BUDGET: usize = 5_300; const REALTIME_ASSISTANT_OUTPUT_TOKEN_BUDGET: usize = 1_000; +const REALTIME_REASONING_STATUS_MAX_BYTES: usize = 256; const REALTIME_INITIAL_ITEMS_MAX_COUNT: usize = 128; const REALTIME_INITIAL_ITEMS_MAX_TOKENS: usize = 8_192; const REALTIME_MODE_INSTRUCTIONS_MAX_TOKENS: usize = 8_192; @@ -189,6 +190,7 @@ struct RealtimeHandoffState { codex_responses_as_items: bool, codex_response_item_prefix: Option, codex_response_handoff_mode: CodexResponseHandoffMode, + backend_reasoning_status: bool, codex_response_handoff_channel_prefixes: Arc>>, session_kind: RealtimeSessionKind, event_parser: RealtimeEventParser, @@ -361,6 +363,10 @@ fn take_last_bytes_at_char_boundary(text: &str, max_bytes: usize) -> &str { #[derive(Clone, Debug, PartialEq, Eq)] enum RealtimeOutbound { + QuietReasoningStatus { + handoff_id: String, + text: String, + }, StandaloneHandoff { text: String, phase: Option, @@ -516,6 +522,7 @@ struct RealtimeStart { codex_responses_as_items: bool, codex_response_item_prefix: Option, codex_response_handoff_mode: CodexResponseHandoffMode, + backend_reasoning_status: bool, codex_response_handoff_channel_prefixes: Option>>, realtime_call_api_provider: Option, session_config: RealtimeSessionConfig, @@ -610,6 +617,7 @@ impl RealtimeConversationManager { codex_responses_as_items, codex_response_item_prefix, codex_response_handoff_mode, + backend_reasoning_status, codex_response_handoff_channel_prefixes, realtime_call_api_provider, session_config, @@ -644,6 +652,7 @@ impl RealtimeConversationManager { codex_responses_as_items, codex_response_item_prefix, codex_response_handoff_mode, + backend_reasoning_status, codex_response_handoff_channel_prefixes: Arc::new( codex_response_handoff_channel_prefixes.unwrap_or_default(), ), @@ -840,6 +849,34 @@ impl RealtimeConversationManager { Ok(()) } + pub(crate) async fn send_reasoning_status(&self, text: &str) -> CodexResult<()> { + let handoff = self + .state + .lock() + .await + .as_ref() + .map(|state| state.handoff.clone()); + let Some(handoff) = handoff.filter(|handoff| { + handoff.backend_reasoning_status + && !handoff.client_managed_handoffs + && handoff.event_parser == RealtimeEventParser::FramelessBidi + }) else { + return Ok(()); + }; + let Some(handoff_id) = handoff.stream.lock().await.active_handoff.clone() else { + return Ok(()); + }; + let text = take_bytes_at_char_boundary(text, REALTIME_REASONING_STATUS_MAX_BYTES); + handoff + .output_tx + .send(RealtimeOutbound::QuietReasoningStatus { + handoff_id, + text: format!("[STATUS] {text}"), + }) + .await + .map_err(|_| CodexErr::InvalidRequest("conversation is not running".to_string())) + } + pub(crate) async fn handoff_out( &self, output_text: String, @@ -1215,6 +1252,7 @@ struct PreparedRealtimeConversationStart { codex_responses_as_items: bool, codex_response_item_prefix: Option, codex_response_handoff_mode: CodexResponseHandoffMode, + backend_reasoning_status: bool, codex_response_handoff_channel_prefixes: Option>>, realtime_start_instructions: Option, realtime_end_instructions: Option, @@ -1357,6 +1395,7 @@ async fn prepare_realtime_start( codex_responses_as_items: params.codex_responses_as_items, codex_response_item_prefix: params.codex_response_item_prefix, codex_response_handoff_mode: params.codex_response_handoff_mode, + backend_reasoning_status: params.backend_reasoning_status, codex_response_handoff_channel_prefixes: params.codex_response_handoff_channel_prefixes, realtime_start_instructions: params.realtime_start_instructions, realtime_end_instructions: params.realtime_end_instructions, @@ -1580,6 +1619,7 @@ async fn handle_start_inner( codex_responses_as_items, codex_response_item_prefix, codex_response_handoff_mode, + backend_reasoning_status, codex_response_handoff_channel_prefixes, realtime_start_instructions, realtime_end_instructions, @@ -1609,6 +1649,7 @@ async fn handle_start_inner( codex_responses_as_items, codex_response_item_prefix, codex_response_handoff_mode, + backend_reasoning_status, codex_response_handoff_channel_prefixes, realtime_call_api_provider, session_config, @@ -2190,6 +2231,7 @@ async fn handle_handoff_output( let handoff_output = handoff_output.context("handoff output channel closed")?; let result = match event_parser { RealtimeEventParser::V1 => match handoff_output { + RealtimeOutbound::QuietReasoningStatus { .. } => Ok(()), RealtimeOutbound::StandaloneHandoff { text, phase: _ } => { writer .send_standalone_handoff(STANDALONE_HANDOFF_ID.to_string(), text) @@ -2231,6 +2273,18 @@ async fn handle_handoff_output( RealtimeOutbound::HandoffCompleteAck { .. } => Ok(()), }, RealtimeEventParser::FramelessBidi => match handoff_output { + RealtimeOutbound::QuietReasoningStatus { handoff_id, text } => { + if handoff_state.stream.lock().await.active_handoff.as_deref() != Some(&handoff_id) + { + return Ok(()); + } + // An omitted channel defaults to speakable on this wire protocol. + writer + .clone() + .with_context_append_channel(RealtimeContextAppendChannel::Commentary) + .send_conversation_handoff_append(handoff_id, text) + .await + } RealtimeOutbound::StandaloneHandoff { text, phase } => { v3_output_writer( writer, @@ -2298,6 +2352,7 @@ async fn handle_handoff_output( RealtimeOutbound::HandoffCompleteAck { .. } => Ok(()), }, RealtimeEventParser::RealtimeV2 => match handoff_output { + RealtimeOutbound::QuietReasoningStatus { .. } => Ok(()), RealtimeOutbound::StandaloneHandoff { text, phase: _ } => { if let Err(err) = writer .send_conversation_item_create(text, ConversationTextRole::User) diff --git a/codex-rs/core/src/realtime_conversation_tests.rs b/codex-rs/core/src/realtime_conversation_tests.rs index 70075592d1..e7e8fc03dc 100644 --- a/codex-rs/core/src/realtime_conversation_tests.rs +++ b/codex-rs/core/src/realtime_conversation_tests.rs @@ -214,6 +214,7 @@ async fn clears_active_handoff_explicitly() { client_managed_handoffs: false, codex_responses_as_items: false, codex_response_item_prefix: None, + backend_reasoning_status: false, codex_response_handoff_mode: CodexResponseHandoffMode::Thinking, codex_response_handoff_channel_prefixes: Arc::new(BTreeMap::new()), session_kind: RealtimeSessionKind::V1, diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 52ef829394..961f36fe1d 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -278,6 +278,7 @@ use self::session::SessionSettingsCommit; pub(crate) use self::session::SessionSettingsUpdate; #[cfg(test)] use self::turn::AssistantMessageStreamParsers; +use self::turn::RealtimeEventText; use self::turn::agent_message_text; #[cfg(test)] use self::turn::collect_explicit_app_ids_from_skill_items; @@ -2491,10 +2492,16 @@ impl Session { } _ => {} } - let Some((text, phase)) = realtime_text_for_event(msg) else { - return; + let result = match realtime_text_for_event(msg) { + Some(RealtimeEventText::Handoff(text, phase)) => { + self.conversation.handoff_out(text, phase).await + } + Some(RealtimeEventText::QuietReasoning(text)) => { + self.conversation.send_reasoning_status(&text).await + } + None => return, }; - if let Err(err) = self.conversation.handoff_out(text, phase).await { + if let Err(err) = result { debug!("failed to mirror event text to realtime conversation: {err}"); } } diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index a69f26ca9a..8f96220f62 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -2000,7 +2000,13 @@ pub(super) fn agent_message_text(item: &codex_protocol::items::AgentMessageItem) .collect() } -pub(super) fn realtime_text_for_event(msg: &EventMsg) -> Option<(String, Option)> { +#[derive(Debug, PartialEq)] +pub(super) enum RealtimeEventText { + Handoff(String, Option), + QuietReasoning(String), +} + +pub(super) fn realtime_text_for_event(msg: &EventMsg) -> Option { match msg { EventMsg::ElicitationRequest(request) if matches!( @@ -2008,11 +2014,27 @@ pub(super) fn realtime_text_for_event(msg: &EventMsg) -> Option<(String, Option< codex_protocol::approvals::ElicitationRequest::UserVerification { .. } ) => { - Some((UserVerificationNotice.render(), None)) + Some(RealtimeEventText::Handoff( + UserVerificationNotice.render(), + None, + )) } - EventMsg::AgentMessage(event) => Some((event.message.clone(), event.phase.clone())), + EventMsg::AgentMessage(event) => Some(RealtimeEventText::Handoff( + event.message.clone(), + event.phase.clone(), + )), EventMsg::ItemCompleted(event) => match &event.item { - TurnItem::AgentMessage(item) => Some((agent_message_text(item), item.phase.clone())), + TurnItem::AgentMessage(item) => Some(RealtimeEventText::Handoff( + agent_message_text(item), + item.phase.clone(), + )), + TurnItem::Reasoning(item) => item + .summary_text + .iter() + .rev() + .map(|summary| summary.trim()) + .find(|summary| !summary.is_empty()) + .map(|summary| RealtimeEventText::QuietReasoning(summary.to_owned())), _ => None, }, EventMsg::ExecApprovalRequest(_) @@ -2030,7 +2052,7 @@ pub(super) fn realtime_text_for_event(msg: &EventMsg) -> Option<(String, Option< }; serde_json::to_string(msg) .ok() - .map(|request| (format!("{message}\n\n{request}"), None)) + .map(|request| RealtimeEventText::Handoff(format!("{message}\n\n{request}"), None)) } EventMsg::Error(_) | EventMsg::Warning(_) diff --git a/codex-rs/core/src/session/turn_tests.rs b/codex-rs/core/src/session/turn_tests.rs index 40faa0bd78..f60ea8be7d 100644 --- a/codex-rs/core/src/session/turn_tests.rs +++ b/codex-rs/core/src/session/turn_tests.rs @@ -103,7 +103,7 @@ fn realtime_user_verification_notice_excludes_request_payload() { }); assert_eq!( realtime_text_for_event(&event), - Some(( + Some(RealtimeEventText::Handoff( "User verification is required. Please respond in the app.".to_string(), None, )), diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index f8a82bd394..28821bb3bb 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -104,6 +104,7 @@ async fn start_realtime_conversation(codex: &codex_core::CodexThread) -> Result< codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, diff --git a/codex-rs/core/tests/suite/realtime_conversation.rs b/codex-rs/core/tests/suite/realtime_conversation.rs index b891fcfd64..e4c0ba1295 100644 --- a/codex-rs/core/tests/suite/realtime_conversation.rs +++ b/codex-rs/core/tests/suite/realtime_conversation.rs @@ -316,6 +316,7 @@ async fn conversation_start_audio_text_close_round_trip() -> Result<()> { codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -497,6 +498,7 @@ async fn conversation_records_history_without_an_event_observer( codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -607,6 +609,7 @@ async fn conversation_start_defaults_to_v2_and_gpt_realtime_1_5() -> Result<()> codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -702,6 +705,7 @@ async fn conversation_websocket_transports_send_codex_headers_without_creating_a codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -872,6 +876,7 @@ async fn conversation_webrtc_frameless_chatgpt_sends_codex_headers_to_backend( codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: model.map(str::to_string), output_modality: RealtimeOutputModality::Audio, @@ -1033,6 +1038,7 @@ async fn conversation_webrtc_start_posts_generated_session() -> Result<()> { codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: Some("session-override-model".to_string()), output_modality: RealtimeOutputModality::Audio, @@ -1397,6 +1403,7 @@ async fn conversation_webrtc_live_reconnects_sideband_after_unclean_disconnect( codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -1552,6 +1559,7 @@ async fn conversation_webrtc_start_uses_avas_query() -> Result<()> { codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -1657,6 +1665,7 @@ async fn conversation_webrtc_default_v1_ignores_configured_v2_voice() -> Result< codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -1724,6 +1733,7 @@ async fn conversation_webrtc_default_v1_rejects_explicit_v2_voice() -> Result<() codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -1801,6 +1811,7 @@ async fn conversation_webrtc_start_uses_configured_call_base_url_for_avas() -> R codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -1915,6 +1926,7 @@ async fn conversation_webrtc_close_while_sideband_connecting_drops_pending_join( codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -2022,6 +2034,7 @@ async fn conversation_webrtc_sideband_connect_failure_closes_with_error() -> Res codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -2122,6 +2135,7 @@ async fn conversation_start_uses_openai_env_key_fallback_with_chatgpt_auth() -> codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -2220,6 +2234,7 @@ async fn assert_transport_close_tail_flush( codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -2343,6 +2358,7 @@ async fn conversation_start_preflight_failure_emits_realtime_error_only() -> Res codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -2401,6 +2417,7 @@ async fn conversation_start_connect_failure_emits_realtime_error_only() -> Resul codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -2507,6 +2524,7 @@ async fn conversation_second_start_replaces_runtime() -> Result<()> { codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -2544,6 +2562,7 @@ async fn conversation_second_start_replaces_runtime() -> Result<()> { codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -2652,6 +2671,7 @@ async fn conversation_uses_experimental_realtime_ws_base_url_override() -> Resul codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -2728,6 +2748,7 @@ async fn conversation_uses_default_realtime_backend_prompt() -> Result<()> { codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -2812,6 +2833,7 @@ async fn conversation_uses_empty_instructions_for_null_or_empty_prompt() -> Resu codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -2889,6 +2911,7 @@ async fn conversation_uses_explicit_start_voice() -> Result<()> { codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -2958,6 +2981,7 @@ async fn conversation_uses_configured_realtime_voice() -> Result<()> { codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -3015,6 +3039,7 @@ async fn conversation_rejects_voice_for_wrong_realtime_version() -> Result<()> { codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -3073,6 +3098,7 @@ async fn conversation_uses_experimental_realtime_ws_backend_prompt_override() -> codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -3157,6 +3183,7 @@ async fn conversation_uses_experimental_realtime_ws_startup_context_override() - codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -3235,6 +3262,7 @@ async fn conversation_disables_realtime_startup_context_with_empty_override() -> codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -3306,6 +3334,7 @@ async fn conversation_start_injects_startup_context_from_thread_history() -> Res codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -3433,6 +3462,7 @@ async fn conversation_startup_context_current_thread_selects_many_turns_by_budge codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -3552,6 +3582,7 @@ async fn conversation_startup_context_falls_back_to_workspace_map() -> Result<() codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -3623,6 +3654,7 @@ async fn conversation_startup_context_is_truncated_and_sent_once_per_start() -> codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -3727,6 +3759,7 @@ async fn conversation_user_text_turn_is_not_sent_to_realtime(ephemeral: bool) -> codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -3867,6 +3900,7 @@ async fn realtime_v2_noop_tool_call_returns_empty_function_output_without_respon codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -3977,6 +4011,7 @@ async fn conversation_mirrors_assistant_message_text_to_realtime_handoff() -> Re codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -4051,6 +4086,137 @@ async fn conversation_mirrors_assistant_message_text_to_realtime_handoff() -> Re Ok(()) } +#[test_case(true; "enabled")] +#[test_case(false; "disabled")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn conversation_relays_only_public_reasoning_as_quiet_status(enabled: bool) -> Result<()> { + skip_if_no_network!(Ok(())); + let (item_tx, item_rx) = oneshot::channel(); + let (response_tx, response_rx) = oneshot::channel(); + let chunks = vec![ + StreamingSseChunk { + gate: None, + body: responses::sse(vec![ + responses::ev_response_created("summary_response"), + responses::ev_reasoning_item_added("reasoning", &[""]), + responses::ev_reasoning_text_delta("Private raw reasoning."), + responses::ev_reasoning_summary_text_delta("Partial summary."), + ]), + }, + StreamingSseChunk { + gate: Some(item_rx), + body: sse_event(responses::ev_reasoning_item( + "reasoning", + &["Older public summary", "Checking availability.", " "], + &["Private raw reasoning."], + )), + }, + StreamingSseChunk { + gate: Some(response_rx), + body: sse_event(responses::ev_completed("summary_response")), + }, + ]; + let (api_server, completions) = start_streaming_sse_server(vec![chunks]).await; + let realtime_server = start_websocket_server(vec![vec![vec![ + json!({"type":"session.started", "session":{"id":"summary_session"}}), + json!({"type":"delegation.created", "item":{"id":"summary_delegation", "type":"delegation", "target":"client", "content":[{"type":"input_text","text":"check availability"}]}}), + ], vec![]]]).await; + let mut builder = test_codex().with_config({ + let realtime_base_url = realtime_server.uri().to_string(); + move |config| { + config.experimental_realtime_ws_base_url = Some(realtime_base_url); + config.realtime.version = RealtimeWsVersion::V3; + config.model_reasoning_summary = + Some(codex_protocol::config_types::ReasoningSummary::None); + } + }); + let test = builder.build_with_streaming_server(&api_server).await?; + test.codex + .submit(Op::RealtimeConversationStart(ConversationStartParams { + client_managed_handoffs: false, + delegation_ack_filler: None, + flush_transcript_tail_on_session_end: false, + codex_responses_as_items: false, + codex_response_item_prefix: None, + // The generic default omits its wire channel; quiet status must override it. + codex_response_handoff_mode: + codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: enabled, + codex_response_handoff_channel_prefixes: None, + model: None, + output_modality: RealtimeOutputModality::Audio, + include_startup_context: false, + initial_items: Vec::new(), + realtime_start_instructions: None, + realtime_end_instructions: None, + prompt: None, + realtime_session_id: None, + transport: None, + version: None, + voice: None, + })) + .await?; + wait_for_event(&test.codex, |event| { + matches!(event, + EventMsg::ReasoningContentDelta(delta) if delta.delta == "Partial summary." + ) + }) + .await; + let next_request = || { + wait_for_websocket_request( + &realtime_server, + /*connection_index*/ 0, + /*request_index*/ 1, + ) + }; + assert!( + timeout(Duration::from_millis(150), next_request()) + .await + .is_err() + ); + let requests = api_server.requests().await; + let model_request: Value = serde_json::from_slice(&requests[0])?; + assert_eq!(model_request["reasoning"].get("summary"), None); + let _ = item_tx.send(()); + wait_for_event(&test.codex, |event| matches!(event, + EventMsg::ItemCompleted(item) if matches!(&item.item, codex_protocol::items::TurnItem::Reasoning(reasoning) if reasoning.id == "reasoning") + )).await; + if enabled { + assert_eq!( + next_request().await?.body_json(), + json!({ + "type":"delegation.context.append", "delegation_item_id":"summary_delegation", "channel":"commentary", + "content":[{"type":"input_text","text":"[STATUS] Checking availability."}] + }) + ); + } else { + assert!( + timeout(Duration::from_millis(150), next_request()) + .await + .is_err() + ); + } + let _ = response_tx.send(()); + completions + .into_iter() + .next() + .expect("one response") + .await?; + wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; + let requests = realtime_server.single_connection(); + assert_eq!(requests.len(), if enabled { 2 } else { 1 }); + assert!(requests.iter().all(|request| { + let body = request.body_json(); + body["type"] != "response.create" && !body.to_string().contains("Private raw reasoning") + })); + realtime_server.shutdown().await; + api_server.shutdown().await; + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn conversation_flushes_assistant_deltas_every_200ms_for_v3_handoff() -> Result<()> { skip_if_no_network!(Ok(())); @@ -4148,6 +4314,7 @@ async fn conversation_flushes_assistant_deltas_every_200ms_for_v3_handoff() -> R codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::BemTags, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: Some(BTreeMap::from([ ( "commentary".to_string(), @@ -4327,6 +4494,7 @@ async fn conversation_handoff_persists_across_item_done_until_turn_complete() -> codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::BemTags, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -4492,6 +4660,7 @@ async fn inbound_handoff_request_starts_turn_and_promotes_its_artifact() -> Resu codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -4656,6 +4825,7 @@ async fn inbound_handoff_request_uses_active_transcript() -> Result<()> { codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -4770,6 +4940,7 @@ async fn inbound_handoff_request_sends_transcript_delta_after_each_handoff() -> codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -4903,6 +5074,7 @@ async fn conversation_close_routes_only_remaining_transcript_tail_once() -> Resu codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -4997,6 +5169,7 @@ async fn inbound_conversation_item_does_not_start_turn_and_still_forwards_audio( codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -5131,6 +5304,7 @@ async fn delegated_turn_user_role_echo_does_not_redelegate_and_still_forwards_au codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -5295,6 +5469,7 @@ async fn inbound_handoff_request_does_not_block_realtime_event_forwarding() -> R codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -5448,6 +5623,7 @@ async fn inbound_handoff_request_steers_active_turn() -> Result<()> { codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, @@ -5606,6 +5782,7 @@ async fn inbound_handoff_request_starts_turn_and_does_not_block_realtime_audio() codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, diff --git a/codex-rs/core/tests/suite/realtime_initial_items.rs b/codex-rs/core/tests/suite/realtime_initial_items.rs index 06de0356b1..6122efdd79 100644 --- a/codex-rs/core/tests/suite/realtime_initial_items.rs +++ b/codex-rs/core/tests/suite/realtime_initial_items.rs @@ -204,6 +204,7 @@ fn start_params(version: RealtimeConversationVersion) -> ConversationStartParams codex_responses_as_items: false, codex_response_item_prefix: None, codex_response_handoff_mode: CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, diff --git a/codex-rs/core/tests/suite/realtime_misalignment.rs b/codex-rs/core/tests/suite/realtime_misalignment.rs index 109e61b78c..d823e87540 100644 --- a/codex-rs/core/tests/suite/realtime_misalignment.rs +++ b/codex-rs/core/tests/suite/realtime_misalignment.rs @@ -97,6 +97,7 @@ async fn misalignment_retires_late_voice_handoff_before_it_starts_a_turn() -> Re codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, diff --git a/codex-rs/core/tests/suite/realtime_sideband_endpoint.rs b/codex-rs/core/tests/suite/realtime_sideband_endpoint.rs index b94be5d07f..39227ce21d 100644 --- a/codex-rs/core/tests/suite/realtime_sideband_endpoint.rs +++ b/codex-rs/core/tests/suite/realtime_sideband_endpoint.rs @@ -66,6 +66,7 @@ async fn existing_call_uses_selected_endpoint_and_runtime_auth( codex_responses_as_items: false, codex_response_item_prefix: None, codex_response_handoff_mode: CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, diff --git a/codex-rs/core/tests/suite/realtime_system_proxy.rs b/codex-rs/core/tests/suite/realtime_system_proxy.rs index a20eb7cde2..7c7da31727 100644 --- a/codex-rs/core/tests/suite/realtime_system_proxy.rs +++ b/codex-rs/core/tests/suite/realtime_system_proxy.rs @@ -140,6 +140,7 @@ async fn webrtc_sideband_honors_configured_system_proxy() -> Result<()> { codex_responses_as_items: false, codex_response_item_prefix: None, codex_response_handoff_mode: CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, diff --git a/codex-rs/core/tests/suite/turn_input_submission.rs b/codex-rs/core/tests/suite/turn_input_submission.rs index 03583036ba..393cf8a536 100644 --- a/codex-rs/core/tests/suite/turn_input_submission.rs +++ b/codex-rs/core/tests/suite/turn_input_submission.rs @@ -269,6 +269,7 @@ async fn host_drain_closes_realtime_after_handoff_error() -> anyhow::Result<()> codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model: None, output_modality: codex_protocol::protocol::RealtimeOutputModality::Audio, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 3b44039af6..614c201c70 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -215,6 +215,8 @@ pub struct ConversationStartParams { /// Selects how automatic Codex handoffs are routed in Frameless Bidi sessions. /// Realtime V1 and V2 ignore this setting. pub codex_response_handoff_mode: CodexResponseHandoffMode, + /// Relays public reasoning summaries as quiet context for realtime V3 delegations. + pub backend_reasoning_status: bool, /// Optional client-selected BEM prefixes keyed by `analysis`, `commentary`, and `final`. pub codex_response_handoff_channel_prefixes: Option>>, /// Overrides the configured realtime model for this session only. diff --git a/codex-rs/tui/src/app_server_session/realtime.rs b/codex-rs/tui/src/app_server_session/realtime.rs index 3614301e89..1e8483e103 100644 --- a/codex-rs/tui/src/app_server_session/realtime.rs +++ b/codex-rs/tui/src/app_server_session/realtime.rs @@ -37,6 +37,7 @@ impl AppServerSession { codex_responses_as_items: None, codex_response_item_prefix: None, codex_response_handoff_mode: None, + backend_reasoning_status: false, codex_response_handoff_channel_prefixes: None, model, output_modality: RealtimeOutputModality::Audio,