From 1029742cf7747ad2e82fe10d931744372902e2d6 Mon Sep 17 00:00:00 2001 From: guinness-oai Date: Mon, 20 Apr 2026 15:43:20 -0700 Subject: [PATCH] Add realtime silence tool (#18635) ## Summary Adds a second realtime v2 function tool, `remain_silent`, so the realtime model has an explicit non-speaking action when the collaboration mode or latest context says it should not answer aloud. This is stacked on #18597. ## Design - Advertise `remain_silent` alongside `background_agent` in realtime v2 conversational sessions. - Parse `remain_silent` function calls into a typed `RealtimeEvent::NoopRequested` event. - Have core answer that function call with an empty `function_call_output` and deliberately avoid `response.create`, so no follow-up realtime response is requested. - Keep the event hidden from app-server/TUI surfaces; it is operational plumbing, not user-visible conversation content. --- .../app-server/src/bespoke_event_handling.rs | 3 +- .../tests/suite/v2/realtime_conversation.rs | 6 +- .../endpoint/realtime_websocket/methods.rs | 60 ++++++++++--- .../realtime_websocket/methods_common.rs | 10 +-- .../endpoint/realtime_websocket/methods_v2.rs | 52 +++++++---- .../realtime_websocket/protocol_v2.rs | 28 ++++++ codex-rs/core/src/realtime_conversation.rs | 27 +++++- .../core/tests/suite/realtime_conversation.rs | 87 +++++++++++++++++++ codex-rs/protocol/src/protocol.rs | 7 ++ codex-rs/tui/src/chatwidget/realtime.rs | 1 + 10 files changed, 242 insertions(+), 39 deletions(-) diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 4701eb3c1..c001a77bf 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -510,7 +510,8 @@ pub(crate) async fn apply_bespoke_event_handling( )) .await; } - RealtimeEvent::ConversationItemDone { .. } => {} + RealtimeEvent::ConversationItemDone { .. } + | RealtimeEvent::NoopRequested(_) => {} RealtimeEvent::HandoffRequested(handoff) => { let notification = ThreadRealtimeItemAddedNotification { thread_id: conversation_id.to_string(), 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 e5676b55f..dfc3fea31 100644 --- a/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs +++ b/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs @@ -1155,7 +1155,7 @@ async fn realtime_webrtc_start_emits_sdp_notification() -> Result<()> { Some("multipart/form-data; boundary=codex-realtime-call-boundary") ); let body = String::from_utf8(request.body).context("multipart body should be utf-8")?; - let session = r#"{"tool_choice":"auto","type":"realtime","model":"gpt-realtime-1.5","instructions":"backend prompt\n\nstartup context","output_modalities":["audio"],"audio":{"input":{"format":{"type":"audio/pcm","rate":24000},"noise_reduction":{"type":"near_field"},"transcription":{"model":"gpt-4o-mini-transcribe"},"turn_detection":{"type":"server_vad","interrupt_response":true,"create_response":true,"silence_duration_ms":500}},"output":{"format":{"type":"audio/pcm","rate":24000},"voice":"marin"}},"tools":[{"type":"function","name":"background_agent","description":"Send a user request to the background agent. Use this as the default action. Do not rephrase the user's ask or rewrite it in your own words; pass along the user's own words. If the background agent is idle, this starts a new task and returns the final result to the user. If the background agent is already working on a task, this sends the request as guidance to steer that previous task. If the user asks to do something next, later, after this, or once current work finishes, call this tool so the work is actually queued instead of merely promising to do it later.","parameters":{"type":"object","properties":{"prompt":{"type":"string","description":"The user request to delegate to the background agent."}},"required":["prompt"],"additionalProperties":false}}]}"#; + let session = r#"{"tool_choice":"auto","type":"realtime","model":"gpt-realtime-1.5","instructions":"backend prompt\n\nstartup context","output_modalities":["audio"],"audio":{"input":{"format":{"type":"audio/pcm","rate":24000},"noise_reduction":{"type":"near_field"},"transcription":{"model":"gpt-4o-mini-transcribe"},"turn_detection":{"type":"server_vad","interrupt_response":true,"create_response":true,"silence_duration_ms":500}},"output":{"format":{"type":"audio/pcm","rate":24000},"voice":"marin"}},"tools":[{"type":"function","name":"background_agent","description":"Send a user request to the background agent. Use this as the default action. Do not rephrase the user's ask or rewrite it in your own words; pass along the user's own words. If the background agent is idle, this starts a new task and returns the final result to the user. If the background agent is already working on a task, this sends the request as guidance to steer that previous task. If the user asks to do something next, later, after this, or once current work finishes, call this tool so the work is actually queued instead of merely promising to do it later.","parameters":{"type":"object","properties":{"prompt":{"type":"string","description":"The user request to delegate to the background agent."}},"required":["prompt"],"additionalProperties":false}},{"type":"function","name":"remain_silent","description":"Call this when the best response is to say nothing. Use it instead of speaking after hidden system/control messages, after background agent updates in silent modes, or whenever acknowledging aloud would be distracting. This tool has no user-visible effect.","parameters":{"type":"object","properties":{},"additionalProperties":false}}]}"#; let session = normalized_json_string(session)?; assert_eq!( body, @@ -2260,6 +2260,10 @@ fn assert_v2_session_update(request: &Value) -> Result<()> { request["session"]["tools"][0]["name"].as_str(), Some("background_agent") ); + assert_eq!( + request["session"]["tools"][1]["name"].as_str(), + Some("remain_silent") + ); assert_eq!( request["session"]["audio"]["input"]["transcription"]["model"].as_str(), Some("gpt-4o-mini-transcribe") diff --git a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs index a4ddb5dee..0a8bac005 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs @@ -1,4 +1,4 @@ -use crate::endpoint::realtime_websocket::methods_common::conversation_handoff_append_message; +use crate::endpoint::realtime_websocket::methods_common::conversation_function_call_output_message; use crate::endpoint::realtime_websocket::methods_common::conversation_item_create_message; use crate::endpoint::realtime_websocket::methods_common::normalized_session_mode; use crate::endpoint::realtime_websocket::methods_common::session_update_session; @@ -230,13 +230,13 @@ impl RealtimeWebsocketConnection { self.writer.send_conversation_item_create(text).await } - pub async fn send_conversation_handoff_append( + pub async fn send_conversation_function_call_output( &self, - handoff_id: String, + call_id: String, output_text: String, ) -> Result<(), ApiError> { self.writer - .send_conversation_handoff_append(handoff_id, output_text) + .send_conversation_function_call_output(call_id, output_text) .await } @@ -290,14 +290,14 @@ impl RealtimeWebsocketWriter { .await } - pub async fn send_conversation_handoff_append( + pub async fn send_conversation_function_call_output( &self, - handoff_id: String, + call_id: String, output_text: String, ) -> Result<(), ApiError> { - self.send_json(&conversation_handoff_append_message( + self.send_json(&conversation_function_call_output_message( self.event_parser, - handoff_id, + call_id, output_text, )) .await @@ -471,6 +471,7 @@ impl RealtimeWebsocketEvents { | RealtimeEvent::ResponseCancelled(_) | RealtimeEvent::ResponseDone(_) | RealtimeEvent::ConversationItemDone { .. } + | RealtimeEvent::NoopRequested(_) | RealtimeEvent::ConversationItemAdded(_) | RealtimeEvent::Error(_) => {} } @@ -825,6 +826,7 @@ mod tests { use crate::endpoint::realtime_websocket::protocol::RealtimeTranscriptEntry; use codex_protocol::protocol::RealtimeHandoffRequested; use codex_protocol::protocol::RealtimeInputAudioSpeechStarted; + use codex_protocol::protocol::RealtimeNoopRequested; use codex_protocol::protocol::RealtimeResponseCancelled; use codex_protocol::protocol::RealtimeResponseCreated; use codex_protocol::protocol::RealtimeResponseDone; @@ -1090,6 +1092,29 @@ mod tests { ); } + #[test] + fn parse_realtime_v2_noop_tool_call_event() { + let payload = json!({ + "type": "conversation.item.done", + "item": { + "id": "item_silent", + "type": "function_call", + "name": "remain_silent", + "call_id": "call_silent", + "arguments": "{}" + } + }) + .to_string(); + + assert_eq!( + parse_realtime_event(payload.as_str(), RealtimeEventParser::RealtimeV2), + Some(RealtimeEvent::NoopRequested(RealtimeNoopRequested { + call_id: "call_silent".to_string(), + item_id: "item_silent".to_string(), + })) + ); + } + #[test] fn parse_realtime_v2_input_audio_transcription_delta_event() { let payload = json!({ @@ -1689,7 +1714,7 @@ mod tests { .await .expect("send item"); connection - .send_conversation_handoff_append( + .send_conversation_function_call_output( "handoff_1".to_string(), "hello from background agent".to_string(), ) @@ -1850,6 +1875,18 @@ mod tests { first_json["session"]["tools"][0]["parameters"]["required"], json!(["prompt"]) ); + assert_eq!( + first_json["session"]["tools"][1]["type"], + Value::String("function".to_string()) + ); + assert_eq!( + first_json["session"]["tools"][1]["name"], + Value::String("remain_silent".to_string()) + ); + assert_eq!( + first_json["session"]["tools"][1]["parameters"]["properties"], + json!({}) + ); assert_eq!( first_json["session"]["tool_choice"], Value::String("auto".to_string()) @@ -1961,7 +1998,10 @@ mod tests { .await .expect("send text item"); connection - .send_conversation_handoff_append("call_1".to_string(), "delegated result".to_string()) + .send_conversation_function_call_output( + "call_1".to_string(), + "delegated result".to_string(), + ) .await .expect("send handoff output"); diff --git a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods_common.rs b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods_common.rs index c0efcce16..1e47fb6fb 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods_common.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods_common.rs @@ -2,7 +2,7 @@ use crate::endpoint::realtime_websocket::methods_v1::conversation_handoff_append use crate::endpoint::realtime_websocket::methods_v1::conversation_item_create_message as v1_conversation_item_create_message; use crate::endpoint::realtime_websocket::methods_v1::session_update_session as v1_session_update_session; use crate::endpoint::realtime_websocket::methods_v1::websocket_intent as v1_websocket_intent; -use crate::endpoint::realtime_websocket::methods_v2::conversation_handoff_append_message as v2_conversation_handoff_append_message; +use crate::endpoint::realtime_websocket::methods_v2::conversation_function_call_output_message as v2_conversation_function_call_output_message; use crate::endpoint::realtime_websocket::methods_v2::conversation_item_create_message as v2_conversation_item_create_message; use crate::endpoint::realtime_websocket::methods_v2::session_update_session as v2_session_update_session; use crate::endpoint::realtime_websocket::methods_v2::websocket_intent as v2_websocket_intent; @@ -40,18 +40,18 @@ pub(super) fn conversation_item_create_message( } } -pub(super) fn conversation_handoff_append_message( +pub(super) fn conversation_function_call_output_message( event_parser: RealtimeEventParser, - handoff_id: String, + call_id: String, output_text: String, ) -> RealtimeOutboundMessage { match event_parser { RealtimeEventParser::V1 => v1_conversation_handoff_append_message( - handoff_id, + call_id, format!("{AGENT_FINAL_MESSAGE_PREFIX}{output_text}"), ), RealtimeEventParser::RealtimeV2 => { - v2_conversation_handoff_append_message(handoff_id, output_text) + v2_conversation_function_call_output_message(call_id, output_text) } } } diff --git a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods_v2.rs b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods_v2.rs index b1f50b02b..292067748 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods_v2.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods_v2.rs @@ -32,6 +32,8 @@ const REALTIME_V2_OUTPUT_MODALITY_TEXT: &str = "text"; const REALTIME_V2_TOOL_CHOICE: &str = "auto"; const REALTIME_V2_BACKGROUND_AGENT_TOOL_NAME: &str = "background_agent"; const REALTIME_V2_BACKGROUND_AGENT_TOOL_DESCRIPTION: &str = "Send a user request to the background agent. Use this as the default action. Do not rephrase the user's ask or rewrite it in your own words; pass along the user's own words. If the background agent is idle, this starts a new task and returns the final result to the user. If the background agent is already working on a task, this sends the request as guidance to steer that previous task. If the user asks to do something next, later, after this, or once current work finishes, call this tool so the work is actually queued instead of merely promising to do it later."; +const REALTIME_V2_SILENCE_TOOL_NAME: &str = "remain_silent"; +const REALTIME_V2_SILENCE_TOOL_DESCRIPTION: &str = "Call this when the best response is to say nothing. Use it instead of speaking after hidden system/control messages, after background agent updates in silent modes, or whenever acknowledging aloud would be distracting. This tool has no user-visible effect."; const REALTIME_V2_INPUT_TRANSCRIPTION_MODEL: &str = "gpt-4o-mini-transcribe"; pub(super) fn conversation_item_create_message(text: String) -> RealtimeOutboundMessage { @@ -47,14 +49,14 @@ pub(super) fn conversation_item_create_message(text: String) -> RealtimeOutbound } } -pub(super) fn conversation_handoff_append_message( - handoff_id: String, +pub(super) fn conversation_function_call_output_message( + call_id: String, output_text: String, ) -> RealtimeOutboundMessage { RealtimeOutboundMessage::ConversationItemCreate { item: ConversationItemPayload::FunctionCallOutput(ConversationFunctionCallOutputItem { r#type: ConversationItemType::FunctionCallOutput, - call_id: handoff_id, + call_id, output: output_text, }), } @@ -100,22 +102,34 @@ pub(super) fn session_update_session( voice, }), }, - tools: Some(vec![SessionFunctionTool { - r#type: SessionToolType::Function, - name: REALTIME_V2_BACKGROUND_AGENT_TOOL_NAME.to_string(), - description: REALTIME_V2_BACKGROUND_AGENT_TOOL_DESCRIPTION.to_string(), - parameters: json!({ - "type": "object", - "properties": { - "prompt": { - "type": "string", - "description": "The user request to delegate to the background agent." - } - }, - "required": ["prompt"], - "additionalProperties": false - }), - }]), + tools: Some(vec![ + SessionFunctionTool { + r#type: SessionToolType::Function, + name: REALTIME_V2_BACKGROUND_AGENT_TOOL_NAME.to_string(), + description: REALTIME_V2_BACKGROUND_AGENT_TOOL_DESCRIPTION.to_string(), + parameters: json!({ + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "The user request to delegate to the background agent." + } + }, + "required": ["prompt"], + "additionalProperties": false + }), + }, + SessionFunctionTool { + r#type: SessionToolType::Function, + name: REALTIME_V2_SILENCE_TOOL_NAME.to_string(), + description: REALTIME_V2_SILENCE_TOOL_DESCRIPTION.to_string(), + parameters: json!({ + "type": "object", + "properties": {}, + "additionalProperties": false + }), + }, + ]), tool_choice: Some(REALTIME_V2_TOOL_CHOICE.to_string()), }, RealtimeSessionMode::Transcription => SessionUpdateSession { diff --git a/codex-rs/codex-api/src/endpoint/realtime_websocket/protocol_v2.rs b/codex-rs/codex-api/src/endpoint/realtime_websocket/protocol_v2.rs index 9aa535757..ee0bba994 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_websocket/protocol_v2.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_websocket/protocol_v2.rs @@ -7,6 +7,7 @@ use codex_protocol::protocol::RealtimeAudioFrame; use codex_protocol::protocol::RealtimeEvent; use codex_protocol::protocol::RealtimeHandoffRequested; use codex_protocol::protocol::RealtimeInputAudioSpeechStarted; +use codex_protocol::protocol::RealtimeNoopRequested; use codex_protocol::protocol::RealtimeResponseCancelled; use codex_protocol::protocol::RealtimeResponseCreated; use codex_protocol::protocol::RealtimeResponseDone; @@ -15,6 +16,7 @@ use serde_json::Value; use tracing::debug; const BACKGROUND_AGENT_TOOL_NAME: &str = "background_agent"; +const SILENCE_TOOL_NAME: &str = "remain_silent"; const DEFAULT_AUDIO_SAMPLE_RATE: u32 = 24_000; const DEFAULT_AUDIO_CHANNELS: u16 = 1; const TOOL_ARGUMENT_KEYS: [&str; 5] = ["input_transcript", "input", "text", "prompt", "query"]; @@ -127,6 +129,9 @@ fn parse_conversation_item_done_event(parsed: &Value) -> Option { if let Some(handoff) = parse_handoff_requested_event(item) { return Some(handoff); } + if let Some(noop) = parse_noop_requested_event(item) { + return Some(noop); + } item.get("id") .and_then(Value::as_str) @@ -160,6 +165,29 @@ fn parse_handoff_requested_event(item: &JsonMap) -> Option) -> Option { + let item_type = item.get("type").and_then(Value::as_str); + let item_name = item.get("name").and_then(Value::as_str); + if item_type != Some("function_call") || item_name != Some(SILENCE_TOOL_NAME) { + return None; + } + + let call_id = item + .get("call_id") + .and_then(Value::as_str) + .or_else(|| item.get("id").and_then(Value::as_str))?; + let item_id = item + .get("id") + .and_then(Value::as_str) + .unwrap_or(call_id) + .to_string(); + + Some(RealtimeEvent::NoopRequested(RealtimeNoopRequested { + call_id: call_id.to_string(), + item_id, + })) +} + fn extract_input_transcript(arguments: &str) -> String { if arguments.is_empty() { return String::new(); diff --git a/codex-rs/core/src/realtime_conversation.rs b/codex-rs/core/src/realtime_conversation.rs index 662f7a590..0ca7c1c16 100644 --- a/codex-rs/core/src/realtime_conversation.rs +++ b/codex-rs/core/src/realtime_conversation.rs @@ -1105,7 +1105,7 @@ async fn handle_handoff_output( output_text, } => { writer - .send_conversation_handoff_append(handoff_id, output_text) + .send_conversation_function_call_output(handoff_id, output_text) .await } }, @@ -1129,7 +1129,7 @@ async fn handle_handoff_output( output_text: _, } => { if let Err(err) = writer - .send_conversation_handoff_append( + .send_conversation_function_call_output( handoff_id, REALTIME_V2_HANDOFF_COMPLETE_ACKNOWLEDGEMENT.to_string(), ) @@ -1263,7 +1263,7 @@ async fn handle_realtime_server_event( match active_handoff { Some(_) => { if let Err(err) = writer - .send_conversation_handoff_append( + .send_conversation_function_call_output( handoff.handoff_id.clone(), REALTIME_V2_STEER_ACKNOWLEDGEMENT.to_string(), ) @@ -1292,6 +1292,27 @@ async fn handle_realtime_server_event( } false } + RealtimeEvent::NoopRequested(noop) => { + *output_audio_state = None; + + match session_kind { + RealtimeSessionKind::V1 => {} + RealtimeSessionKind::V2 => { + if let Err(err) = writer + .send_conversation_function_call_output(noop.call_id.clone(), String::new()) + .await + { + let mapped_error = map_api_error(err); + warn!("failed to send realtime noop function output: {mapped_error}"); + let _ = events_tx + .send(RealtimeEvent::Error(mapped_error.to_string())) + .await; + return Err(mapped_error.into()); + } + } + } + false + } RealtimeEvent::Error(_) => true, RealtimeEvent::SessionUpdated { session_id, .. } => { info!(realtime_session_id = %session_id, "realtime session updated"); diff --git a/codex-rs/core/tests/suite/realtime_conversation.rs b/codex-rs/core/tests/suite/realtime_conversation.rs index 0d6e13056..4fb07f69b 100644 --- a/codex-rs/core/tests/suite/realtime_conversation.rs +++ b/codex-rs/core/tests/suite/realtime_conversation.rs @@ -22,6 +22,7 @@ use codex_protocol::protocol::RealtimeAudioFrame; use codex_protocol::protocol::RealtimeConversationRealtimeEvent; use codex_protocol::protocol::RealtimeConversationVersion; use codex_protocol::protocol::RealtimeEvent; +use codex_protocol::protocol::RealtimeNoopRequested; use codex_protocol::protocol::RealtimeOutputModality; use codex_protocol::protocol::RealtimeVoice; use codex_protocol::protocol::RolloutItem; @@ -2121,6 +2122,92 @@ async fn conversation_user_text_turn_is_capped_when_mirrored_to_realtime() -> Re Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn realtime_v2_noop_tool_call_returns_empty_function_output_without_response() -> Result<()> { + skip_if_no_network!(Ok(())); + + let api_server = start_mock_server().await; + let realtime_server = start_websocket_server(vec![vec![ + vec![ + json!({ + "type": "session.updated", + "session": { "id": "sess_silent", "instructions": "backend prompt" } + }), + json!({ + "type": "conversation.item.done", + "item": { + "id": "item_silent", + "type": "function_call", + "name": "remain_silent", + "call_id": "call_silent", + "arguments": "{}" + } + }), + ], + 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::V2; + } + }); + let test = builder.build(&api_server).await?; + + test.codex + .submit(Op::RealtimeConversationStart(ConversationStartParams { + output_modality: RealtimeOutputModality::Audio, + prompt: Some(Some("backend prompt".to_string())), + session_id: None, + transport: None, + voice: None, + })) + .await?; + + let _ = wait_for_event_match(&test.codex, |msg| match msg { + EventMsg::RealtimeConversationRealtime(RealtimeConversationRealtimeEvent { + payload: RealtimeEvent::NoopRequested(RealtimeNoopRequested { call_id, .. }), + }) if call_id == "call_silent" => Some(()), + _ => None, + }) + .await; + + let function_output = realtime_server + .wait_for_request(/*connection_index*/ 0, /*request_index*/ 1) + .await; + assert_eq!( + function_output.body_json(), + json!({ + "type": "conversation.item.create", + "item": { + "type": "function_call_output", + "call_id": "call_silent", + "output": "" + } + }) + ); + + let realtime_response_create = timeout(Duration::from_millis(200), async { + wait_for_matching_websocket_request( + &realtime_server, + "unexpected realtime response request for noop tool call", + |request| request.body_json()["type"].as_str() == Some("response.create"), + ) + .await + }) + .await; + assert!( + realtime_response_create.is_err(), + "noop tool calls should not request a realtime response" + ); + + realtime_server.shutdown().await; + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn conversation_mirrors_assistant_message_text_to_realtime_handoff() -> Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 475d2941c..418dc841c 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -329,6 +329,12 @@ pub struct RealtimeHandoffRequested { pub active_transcript: Vec, } +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] +pub struct RealtimeNoopRequested { + pub call_id: String, + pub item_id: String, +} + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] pub struct RealtimeInputAudioSpeechStarted { pub item_id: Option, @@ -369,6 +375,7 @@ pub enum RealtimeEvent { item_id: String, }, HandoffRequested(RealtimeHandoffRequested), + NoopRequested(RealtimeNoopRequested), Error(String), } diff --git a/codex-rs/tui/src/chatwidget/realtime.rs b/codex-rs/tui/src/chatwidget/realtime.rs index f09e3e9b3..bfeaff2ea 100644 --- a/codex-rs/tui/src/chatwidget/realtime.rs +++ b/codex-rs/tui/src/chatwidget/realtime.rs @@ -338,6 +338,7 @@ impl ChatWidget { RealtimeEvent::ConversationItemAdded(_item) => {} RealtimeEvent::ConversationItemDone { .. } => {} RealtimeEvent::HandoffRequested(_) => {} + RealtimeEvent::NoopRequested(_) => {} RealtimeEvent::Error(message) => { self.fail_realtime_conversation(format!("Realtime voice error: {message}")); }