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 3b9ab31772..8f94aec386 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 10b684720f..c1ab9a6bac 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -3606,6 +3606,7 @@ mod tests { request_id: RequestId::Integer(9), params: v2::ThreadRealtimeStartParams { client_managed_handoffs: Some(true), + delegation_ack_filler: Some(false), flush_transcript_tail_on_session_end: Some(true), codex_responses_as_items: None, codex_response_item_prefix: None, @@ -3648,6 +3649,7 @@ mod tests { "params": { "threadId": "thr_123", "clientManagedHandoffs": true, + "delegationAckFiller": false, "flushTranscriptTailOnSessionEnd": true, "codexResponsesAsItems": null, "codexResponseItemPrefix": null, @@ -3690,6 +3692,7 @@ mod tests { request_id: RequestId::Integer(9), params: v2::ThreadRealtimeStartParams { client_managed_handoffs: None, + delegation_ack_filler: None, flush_transcript_tail_on_session_end: None, codex_responses_as_items: None, codex_response_item_prefix: None, @@ -3716,6 +3719,7 @@ mod tests { "params": { "threadId": "thr_123", "clientManagedHandoffs": null, + "delegationAckFiller": null, "flushTranscriptTailOnSessionEnd": null, "codexResponsesAsItems": null, "codexResponseItemPrefix": null, @@ -3740,6 +3744,7 @@ mod tests { request_id: RequestId::Integer(9), params: v2::ThreadRealtimeStartParams { client_managed_handoffs: None, + delegation_ack_filler: None, flush_transcript_tail_on_session_end: None, codex_responses_as_items: None, codex_response_item_prefix: None, @@ -3766,6 +3771,7 @@ mod tests { "params": { "threadId": "thr_123", "clientManagedHandoffs": null, + "delegationAckFiller": null, "flushTranscriptTailOnSessionEnd": null, "codexResponsesAsItems": null, "codexResponseItemPrefix": null, @@ -3990,6 +3996,7 @@ mod tests { request_id: RequestId::Integer(1), params: v2::ThreadRealtimeStartParams { client_managed_handoffs: None, + delegation_ack_filler: None, flush_transcript_tail_on_session_end: None, codex_responses_as_items: None, codex_response_item_prefix: 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 1b7ae70043..a53f001838 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs @@ -72,6 +72,10 @@ pub struct ThreadRealtimeStartParams { /// them automatically. Defaults to false. #[ts(optional = nullable)] pub client_managed_handoffs: Option, + /// Controls whether a realtime V3 delegation produces an acknowledgement filler. + /// Omitted values preserve the Realtime API's default behavior. + #[ts(optional = nullable)] + pub delegation_ack_filler: Option, /// Routes any transcript tail remaining at session end through Codex. Defaults to false. /// TODO: Remove this rollout knob once transcript-tail flushing is always enabled. #[ts(optional = nullable)] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 6091b76323..9fe748dfee 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -4731,7 +4731,6 @@ fn realtime_start_omitted_initial_items_remain_none() { assert_eq!(params.initial_items, None); } - #[test] fn realtime_start_deserializes_client_handoff_channel_prefixes() { let params = serde_json::from_value::(json!({ diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 109852c551..c6822d8cd8 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -180,7 +180,7 @@ Example with notification opt-out: - `thread/inject_items` — append raw Responses API items to a loaded thread’s model-visible history without starting a user turn; returns `{}` on success. - `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Review and manual compaction turns reject `turn/steer`. - `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`. -- `thread/realtime/start` — start a thread-scoped realtime session (experimental); pass `outputModality: "text"` or `outputModality: "audio"` to choose model output, optionally pass `model` and `version` to override configured realtime selection for this session only, pass `includeStartupContext: false` to omit Codex's generated startup context, and optionally pass `initialItems` to seed V3 with complete role-bearing text messages at session creation. Pass `realtimeStartInstructions` and `realtimeEndInstructions` to control the developer instructions given to the backing Codex model when this session starts and ends. Version `"v1"` uses legacy Bidi `conversation.handoff.*`, `"v2"` uses the Realtime Voice API, and `"v3"` preserves V1 Codex Voice behavior while using Frameless Bidi `delegation.*`. For V3 automatic Codex text, `codexResponseHandoffMode` accepts `"thinking"` (the default; all output uses channel-less thinking appends), `"commentary"` (all output uses the commentary channel), or `"bemTags"` (the raw BEM envelope selects the API channel: BEM `analysis` and `commentary` use `commentary`, while BEM `final` and unparsable output use `speakable`). The BEM envelope remains in the appended text for the frontend model to interpret. V1 and V2 ignore this setting. V3 handoffs do not prepend the legacy `"Agent Final Message"` label. Pass `clientManagedHandoffs: true` to disable automatic Codex response delivery so only the client's explicit append calls produce handoffs. Pass `codexResponsesAsItems: true` to send automatic Codex responses as realtime conversation items instead, and optionally pass `codexResponseItemPrefix` to prepend experiment instructions to those items. Returns `{}` and streams `thread/realtime/*` notifications. Omit `transport` for the websocket transport, or pass `{ "type": "webrtc", "sdp": "..." }` to create a Bidi WebRTC session from a browser-generated SDP offer; the remote answer SDP is emitted as `thread/realtime/sdp`. Conversation `version: "v2"` requests remain unsupported for WebRTC. +- `thread/realtime/start` — start a thread-scoped realtime session (experimental); pass `outputModality: "text"` or `outputModality: "audio"` to choose model output, optionally pass `model` and `version` to override configured realtime selection for this session only, pass `includeStartupContext: false` to omit Codex's generated startup context, and optionally pass `initialItems` to seed V3 with complete role-bearing text messages at session creation. Pass `realtimeStartInstructions` and `realtimeEndInstructions` to control the developer instructions given to the backing Codex model when this session starts and ends. Version `"v1"` uses legacy Bidi `conversation.handoff.*`, `"v2"` uses the Realtime Voice API, and `"v3"` preserves V1 Codex Voice behavior while using Frameless Bidi `delegation.*`. For V3 automatic Codex text, `codexResponseHandoffMode` accepts `"thinking"` (the default; all output uses channel-less thinking appends), `"commentary"` (all output uses the commentary channel), or `"bemTags"` (the raw BEM envelope selects the API channel: BEM `analysis` and `commentary` use `commentary`, while BEM `final` and unparsable output use `speakable`). The BEM envelope remains in the appended text for the frontend model to interpret. V1 and V2 ignore this setting. For V3, pass `delegationAckFiller: false` to suppress the Realtime API's delegation acknowledgement filler or `true` to restore it; omitting the field preserves the Realtime API's default. V1 and V2 ignore `delegationAckFiller`. V3 handoffs do not prepend the legacy `"Agent Final Message"` label. Pass `clientManagedHandoffs: true` to disable automatic Codex response delivery so only the client's explicit append calls produce handoffs. Pass `codexResponsesAsItems: true` to send automatic Codex responses as realtime conversation items instead, and optionally pass `codexResponseItemPrefix` to prepend experiment instructions to those items. Returns `{}` and streams `thread/realtime/*` notifications. Omit `transport` for the websocket transport, or pass `{ "type": "webrtc", "sdp": "..." }` to create a Bidi WebRTC session from a browser-generated SDP offer; the remote answer SDP is emitted as `thread/realtime/sdp`. Conversation `version: "v2"` requests remain unsupported for WebRTC. - `thread/realtime/appendAudio` — append an input audio chunk to the active realtime session (experimental); returns `{}`. - `thread/realtime/appendText` — append text input to the active realtime session with a required `role` of `user`, `developer`, or `assistant` (experimental); returns `{}`. Older clients that omit `role` default to `user`. - `thread/realtime/appendSpeech` — append text that the realtime model should speak to the user (experimental); returns `{}`. @@ -1023,6 +1023,10 @@ Requests are limited to 128 items, 8,192 estimated text tokens per item, and Omitting `initialItems`, or passing an empty list, preserves the previous session payload and startup behavior. V1 and V2 reject non-empty `initialItems`. +For V3, pass `delegationAckFiller: false` to suppress the Realtime API's +delegation acknowledgement filler during WebRTC session creation, or pass `true` +to restore the legacy acknowledgement. Omitting `delegationAckFiller` preserves +the Realtime API's default. V1 and V2 ignore this setting. Pass `clientManagedHandoffs: true` to suppress automatic Codex response handoffs and items. The client can then choose which updates to deliver with `thread/realtime/appendText` or `thread/realtime/appendSpeech`. 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 2ac576cac7..09c54f9035 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -1075,6 +1075,7 @@ impl TurnRequestProcessor { thread.as_ref(), Op::RealtimeConversationStart(ConversationStartParams { client_managed_handoffs: params.client_managed_handoffs.unwrap_or(false), + delegation_ack_filler: params.delegation_ack_filler, flush_transcript_tail_on_session_end: params .flush_transcript_tail_on_session_end .unwrap_or(false), 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 c408ac0d6a..07efb08a20 100644 --- a/codex-rs/app-server/tests/suite/v2/experimental_api.rs +++ b/codex-rs/app-server/tests/suite/v2/experimental_api.rs @@ -90,6 +90,7 @@ async fn realtime_conversation_start_requires_experimental_api_capability() -> R let request_id = mcp .send_thread_realtime_start_request(ThreadRealtimeStartParams { client_managed_handoffs: None, + delegation_ack_filler: None, flush_transcript_tail_on_session_end: None, codex_responses_as_items: None, codex_response_item_prefix: None, @@ -223,6 +224,7 @@ async fn realtime_webrtc_start_requires_experimental_api_capability() -> Result< let request_id = mcp .send_thread_realtime_start_request(ThreadRealtimeStartParams { client_managed_handoffs: None, + delegation_ack_filler: None, flush_transcript_tail_on_session_end: None, codex_responses_as_items: None, codex_response_item_prefix: 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 0d2c91bfc2..3361ca7955 100644 --- a/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs +++ b/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs @@ -329,6 +329,7 @@ impl RealtimeE2eHarness { /*client_managed_handoffs*/ None, /*codex_responses_as_items*/ None, /*codex_response_handoff_mode*/ None, + /*delegation_ack_filler*/ None, RealtimeConversationVersion::V1, ) .await @@ -343,6 +344,7 @@ impl RealtimeE2eHarness { /*client_managed_handoffs*/ None, /*codex_responses_as_items*/ Some(true), /*codex_response_handoff_mode*/ None, + /*delegation_ack_filler*/ None, RealtimeConversationVersion::V1, ) .await @@ -354,6 +356,7 @@ impl RealtimeE2eHarness { client_managed_handoffs: Option, codex_responses_as_items: Option, codex_response_handoff_mode: Option, + delegation_ack_filler: Option, version: RealtimeConversationVersion, ) -> Result { // Starts realtime through the public JSON-RPC method, then waits for the same client-visible @@ -362,6 +365,7 @@ impl RealtimeE2eHarness { .mcp .send_thread_realtime_start_request(ThreadRealtimeStartParams { client_managed_handoffs, + delegation_ack_filler, flush_transcript_tail_on_session_end: None, thread_id: self.thread_id.clone(), codex_response_item_prefix: codex_responses_as_items @@ -423,6 +427,7 @@ impl RealtimeE2eHarness { .send_thread_realtime_start_request(ThreadRealtimeStartParams { thread_id: self.thread_id.clone(), client_managed_handoffs: None, + delegation_ack_filler: None, flush_transcript_tail_on_session_end: None, codex_response_item_prefix: codex_responses_as_items .unwrap_or(false) @@ -461,6 +466,7 @@ impl RealtimeE2eHarness { .send_thread_realtime_start_request(ThreadRealtimeStartParams { thread_id: self.thread_id.clone(), client_managed_handoffs: None, + delegation_ack_filler: None, flush_transcript_tail_on_session_end: None, codex_response_item_prefix: None, codex_response_handoff_mode, @@ -730,6 +736,7 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> { let start_request_id = mcp .send_thread_realtime_start_request(ThreadRealtimeStartParams { client_managed_handoffs: None, + delegation_ack_filler: None, flush_transcript_tail_on_session_end: None, codex_responses_as_items: None, codex_response_item_prefix: None, @@ -1011,6 +1018,7 @@ async fn realtime_start_can_skip_startup_context() -> Result<()> { let start_request_id = mcp .send_thread_realtime_start_request(ThreadRealtimeStartParams { client_managed_handoffs: None, + delegation_ack_filler: None, flush_transcript_tail_on_session_end: None, codex_responses_as_items: None, codex_response_item_prefix: None, @@ -1108,6 +1116,7 @@ async fn realtime_text_output_modality_requests_text_output_and_final_transcript let start_request_id = mcp .send_thread_realtime_start_request(ThreadRealtimeStartParams { client_managed_handoffs: None, + delegation_ack_filler: None, flush_transcript_tail_on_session_end: None, codex_responses_as_items: None, codex_response_item_prefix: None, @@ -1286,6 +1295,7 @@ async fn realtime_conversation_stop_emits_closed_notification() -> Result<()> { let start_request_id = mcp .send_thread_realtime_start_request(ThreadRealtimeStartParams { client_managed_handoffs: None, + delegation_ack_filler: None, flush_transcript_tail_on_session_end: None, codex_responses_as_items: None, codex_response_item_prefix: None, @@ -1359,6 +1369,7 @@ async fn realtime_mode_uses_client_instructions_on_entry_and_exit() -> Result<() .send_thread_realtime_start_request(ThreadRealtimeStartParams { thread_id: harness.thread_id.clone(), client_managed_handoffs: None, + delegation_ack_filler: None, flush_transcript_tail_on_session_end: None, codex_responses_as_items: None, codex_response_item_prefix: None, @@ -1493,6 +1504,7 @@ async fn realtime_webrtc_start_emits_sdp_notification() -> Result<()> { let start_request_id = mcp .send_thread_realtime_start_request(ThreadRealtimeStartParams { client_managed_handoffs: None, + delegation_ack_filler: None, flush_transcript_tail_on_session_end: None, codex_responses_as_items: None, codex_response_item_prefix: None, @@ -1687,6 +1699,7 @@ async fn webrtc_v3_start_posts_live_session_and_joins_without_session_update() - /*client_managed_handoffs*/ None, /*codex_responses_as_items*/ None, /*codex_response_handoff_mode*/ None, + /*delegation_ack_filler*/ Some(false), RealtimeConversationVersion::V3, ) .await?; @@ -1708,7 +1721,7 @@ async fn webrtc_v3_start_posts_live_session_and_joins_without_session_update() - assert_call_create_multipart( harness.call_capture.single_request(), "v=offer\r\n", - r#"{"audio":{"output":{"voice":"cove"}},"delegation":{"type":"client"},"instructions":"backend prompt\n\nstartup context","model":"gpt-live-1-boulder-alpha"}"#, + r#"{"audio":{"output":{"voice":"cove"}},"delegation":{"ack_filler":false,"type":"client"},"instructions":"backend prompt\n\nstartup context","model":"gpt-live-1-boulder-alpha"}"#, "/v1/live", )?; assert!( @@ -1812,6 +1825,7 @@ async fn webrtc_v1_client_managed_handoffs_disable_automatic_output() -> Result< /*client_managed_handoffs*/ Some(true), /*codex_responses_as_items*/ None, /*codex_response_handoff_mode*/ None, + /*delegation_ack_filler*/ None, RealtimeConversationVersion::V1, ) .await?; @@ -1902,6 +1916,7 @@ async fn webrtc_v1_ignores_codex_response_handoff_mode() -> Result<()> { /*client_managed_handoffs*/ None, /*codex_responses_as_items*/ None, /*codex_response_handoff_mode*/ Some(CodexResponseHandoffMode::BemTags), + /*delegation_ack_filler*/ None, RealtimeConversationVersion::V1, ) .await?; @@ -3130,6 +3145,7 @@ async fn realtime_webrtc_start_surfaces_backend_error() -> Result<()> { let start_request_id = mcp .send_thread_realtime_start_request(ThreadRealtimeStartParams { client_managed_handoffs: None, + delegation_ack_filler: None, flush_transcript_tail_on_session_end: None, codex_responses_as_items: None, codex_response_item_prefix: None, @@ -3195,6 +3211,7 @@ async fn realtime_conversation_requires_feature_flag() -> Result<()> { let start_request_id = mcp .send_thread_realtime_start_request(ThreadRealtimeStartParams { client_managed_handoffs: None, + delegation_ack_filler: None, flush_transcript_tail_on_session_end: None, codex_responses_as_items: None, codex_response_item_prefix: None, diff --git a/codex-rs/codex-api/src/endpoint/realtime_call.rs b/codex-rs/codex-api/src/endpoint/realtime_call.rs index e005fd4ad0..b43e02d9b0 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_call.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_call.rs @@ -389,6 +389,7 @@ mod tests { RealtimeSessionConfig { instructions: "hi".to_string(), initial_items: Vec::new(), + delegation_ack_filler: None, model: Some("gpt-realtime".to_string()), session_id: Some(session_id.to_string()), event_parser: RealtimeEventParser::V1, 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 8c18f7515a..ebd79efd57 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs @@ -384,6 +384,7 @@ impl RealtimeWebsocketWriter { session_mode: RealtimeSessionMode, output_modality: RealtimeOutputModality, voice: RealtimeVoice, + delegation_ack_filler: Option, ) -> Result<(), ApiError> { let session_mode = normalized_session_mode(self.event_parser, session_mode); let message = session_update_message( @@ -393,6 +394,7 @@ impl RealtimeWebsocketWriter { session_mode, output_modality, voice, + delegation_ack_filler, ); self.send_json(&message).await } @@ -868,6 +870,7 @@ impl RealtimeWebsocketClient { config.session_mode, config.output_modality, config.voice, + config.delegation_ack_filler, ) .await?; } @@ -2019,6 +2022,7 @@ mod tests { RealtimeSessionConfig { instructions: "backend prompt".to_string(), initial_items: Vec::new(), + delegation_ack_filler: None, model: Some("realtime-test-model".to_string()), session_id: Some("conv_1".to_string()), event_parser: RealtimeEventParser::V1, @@ -2344,6 +2348,7 @@ mod tests { RealtimeSessionConfig { instructions: "backend prompt".to_string(), initial_items: Vec::new(), + delegation_ack_filler: None, model: Some("realtime-test-model".to_string()), session_id: Some("conv_1".to_string()), event_parser: RealtimeEventParser::RealtimeV2, @@ -2470,6 +2475,7 @@ mod tests { RealtimeSessionConfig { instructions: "backend prompt".to_string(), initial_items: Vec::new(), + delegation_ack_filler: None, model: Some("realtime-test-model".to_string()), session_id: Some("conv_1".to_string()), event_parser: RealtimeEventParser::RealtimeV2, @@ -2575,6 +2581,7 @@ mod tests { RealtimeSessionConfig { instructions: "backend prompt".to_string(), initial_items: Vec::new(), + delegation_ack_filler: None, model: Some("realtime-test-model".to_string()), session_id: Some("conv_1".to_string()), event_parser: RealtimeEventParser::V1, @@ -2666,6 +2673,7 @@ mod tests { RealtimeSessionConfig { instructions: "backend prompt".to_string(), initial_items: Vec::new(), + delegation_ack_filler: None, model: Some("realtime-test-model".to_string()), session_id: Some("conv_1".to_string()), event_parser: RealtimeEventParser::V1, 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 c2cad2aac4..e5482e0134 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 @@ -118,15 +118,19 @@ pub(super) fn session_update_message( session_mode: RealtimeSessionMode, output_modality: RealtimeOutputModality, voice: RealtimeVoice, + delegation_ack_filler: Option, ) -> RealtimeOutboundMessage { let session_mode = normalized_session_mode(wire_adapter, session_mode); match wire_adapter { RealtimeWireAdapter::V1 => RealtimeOutboundMessage::SessionUpdate { session: v1_session_update_session(instructions, voice), }, - RealtimeWireAdapter::FramelessBidi => { - frameless_session_update_message(instructions, initial_items, voice) - } + RealtimeWireAdapter::FramelessBidi => frameless_session_update_message( + instructions, + initial_items, + voice, + delegation_ack_filler, + ), RealtimeWireAdapter::RealtimeV2 => RealtimeOutboundMessage::SessionUpdate { session: v2_session_update_session(instructions, session_mode, output_modality, voice), }, @@ -157,6 +161,7 @@ pub fn session_update_session_json(config: RealtimeSessionConfig) -> JsonResult< config.instructions, config.initial_items, config.voice, + config.delegation_ack_filler, )), } } diff --git a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods_common_tests.rs b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods_common_tests.rs index 63a2672216..f23b24d77f 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods_common_tests.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods_common_tests.rs @@ -1,13 +1,51 @@ use super::conversation_function_call_output_message; use super::conversation_handoff_append_message; +use super::session_update_message; use super::standalone_handoff_message; use crate::endpoint::realtime_websocket::protocol::RealtimeContextAppendChannel; +use crate::endpoint::realtime_websocket::protocol::RealtimeOutputModality; +use crate::endpoint::realtime_websocket::protocol::RealtimeSessionMode; +use crate::endpoint::realtime_websocket::protocol::RealtimeVoice; use crate::endpoint::realtime_websocket::protocol::RealtimeWireAdapter; use pretty_assertions::assert_eq; use serde_json::Value; use serde_json::json; use serde_json::to_value; +#[test] +fn frameless_session_update_encodes_explicit_delegation_ack_filler() { + for delegation_ack_filler in [false, true] { + let message = session_update_message( + RealtimeWireAdapter::FramelessBidi, + "instructions".to_string(), + Vec::new(), + RealtimeSessionMode::Conversational, + RealtimeOutputModality::Audio, + RealtimeVoice::Marin, + Some(delegation_ack_filler), + ); + + assert_eq!( + to_value(message).expect("frameless session update should serialize"), + json!({ + "type": "session.update", + "session": { + "instructions": "instructions", + "audio": { + "output": { + "voice": "marin", + }, + }, + "delegation": { + "type": "client", + "ack_filler": delegation_ack_filler, + }, + }, + }) + ); + } +} + #[test] fn context_append_channel_only_encodes_for_frameless_handoff_output() { let legacy = conversation_handoff_append_message( diff --git a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods_frameless_bidi.rs b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods_frameless_bidi.rs index d92907fb14..246878febc 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods_frameless_bidi.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods_frameless_bidi.rs @@ -36,9 +36,16 @@ pub(super) fn session_update_message( instructions: String, initial_items: Vec, voice: RealtimeVoice, + delegation_ack_filler: Option, ) -> RealtimeOutboundMessage { RealtimeOutboundMessage::FramelessSessionUpdate { - session: session_json(/*model*/ None, instructions, initial_items, voice), + session: session_json( + /*model*/ None, + instructions, + initial_items, + voice, + delegation_ack_filler, + ), } } @@ -47,6 +54,7 @@ pub(super) fn session_json( instructions: String, initial_items: Vec, voice: RealtimeVoice, + delegation_ack_filler: Option, ) -> Value { let mut session = json!({ "instructions": instructions, @@ -62,6 +70,9 @@ pub(super) fn session_json( if let Some(model) = model { session["model"] = Value::String(model); } + if let Some(delegation_ack_filler) = delegation_ack_filler { + session["delegation"]["ack_filler"] = Value::Bool(delegation_ack_filler); + } if !initial_items.is_empty() { session["initial_items"] = Value::Array( initial_items diff --git a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods_frameless_bidi_tests.rs b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods_frameless_bidi_tests.rs index c6d1412318..d187e2edc7 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods_frameless_bidi_tests.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods_frameless_bidi_tests.rs @@ -27,6 +27,7 @@ fn session_json_omits_initial_items_when_empty() { "instructions".to_string(), Vec::new(), RealtimeVoice::Marin, + /*delegation_ack_filler*/ None, ); assert_eq!( @@ -66,6 +67,7 @@ fn session_json_encodes_role_bearing_initial_items() { }, ], RealtimeVoice::Marin, + /*delegation_ack_filler*/ None, ); assert_eq!( diff --git a/codex-rs/codex-api/src/endpoint/realtime_websocket/protocol.rs b/codex-rs/codex-api/src/endpoint/realtime_websocket/protocol.rs index a4b0037d1b..b7fc073dfa 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_websocket/protocol.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_websocket/protocol.rs @@ -38,6 +38,7 @@ pub enum RealtimeContextAppendChannel { pub struct RealtimeSessionConfig { pub instructions: String, pub initial_items: Vec, + pub delegation_ack_filler: Option, pub model: Option, pub session_id: Option, pub event_parser: RealtimeEventParser, diff --git a/codex-rs/codex-api/tests/realtime_websocket_e2e.rs b/codex-rs/codex-api/tests/realtime_websocket_e2e.rs index c3018fa929..1fd1c59878 100644 --- a/codex-rs/codex-api/tests/realtime_websocket_e2e.rs +++ b/codex-rs/codex-api/tests/realtime_websocket_e2e.rs @@ -145,6 +145,7 @@ async fn realtime_ws_e2e_session_create_and_event_flow() { RealtimeSessionConfig { instructions: "backend prompt".to_string(), initial_items: Vec::new(), + delegation_ack_filler: None, model: Some("realtime-test-model".to_string()), session_id: Some("conv_123".to_string()), event_parser: RealtimeEventParser::V1, @@ -251,6 +252,7 @@ async fn realtime_ws_connect_webrtc_sideband_retries_join_until_server_is_availa RealtimeSessionConfig { instructions: "backend prompt".to_string(), initial_items: Vec::new(), + delegation_ack_filler: None, model: Some("realtime-test-model".to_string()), session_id: Some("conv_123".to_string()), event_parser: RealtimeEventParser::RealtimeV2, @@ -324,6 +326,7 @@ async fn realtime_ws_e2e_send_while_next_event_waits() { RealtimeSessionConfig { instructions: "backend prompt".to_string(), initial_items: Vec::new(), + delegation_ack_filler: None, model: Some("realtime-test-model".to_string()), session_id: Some("conv_123".to_string()), event_parser: RealtimeEventParser::V1, @@ -393,6 +396,7 @@ async fn realtime_ws_e2e_disconnected_emitted_once() { RealtimeSessionConfig { instructions: "backend prompt".to_string(), initial_items: Vec::new(), + delegation_ack_filler: None, model: Some("realtime-test-model".to_string()), session_id: Some("conv_123".to_string()), event_parser: RealtimeEventParser::V1, @@ -458,6 +462,7 @@ async fn realtime_ws_e2e_ignores_unknown_text_events() { RealtimeSessionConfig { instructions: "backend prompt".to_string(), initial_items: Vec::new(), + delegation_ack_filler: None, model: Some("realtime-test-model".to_string()), session_id: Some("conv_123".to_string()), event_parser: RealtimeEventParser::V1, @@ -566,6 +571,7 @@ async fn realtime_ws_e2e_realtime_v2_parser_emits_handoff_requested() { RealtimeSessionConfig { instructions: "backend prompt".to_string(), initial_items: Vec::new(), + delegation_ack_filler: None, model: Some("realtime-test-model".to_string()), session_id: Some("conv_123".to_string()), event_parser: RealtimeEventParser::RealtimeV2, diff --git a/codex-rs/core/src/realtime_conversation.rs b/codex-rs/core/src/realtime_conversation.rs index b56da61bbe..325bd34394 100644 --- a/codex-rs/core/src/realtime_conversation.rs +++ b/codex-rs/core/src/realtime_conversation.rs @@ -1357,6 +1357,7 @@ pub(crate) async fn build_realtime_session_config( Ok(RealtimeSessionConfig { instructions: prompt, initial_items: params.initial_items.clone(), + delegation_ack_filler: params.delegation_ack_filler, model, session_id: Some( params diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index c9e64f78a8..00efbb24c9 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -243,6 +243,7 @@ async fn start_realtime_conversation(codex: &codex_core::CodexThread) -> Result< 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, diff --git a/codex-rs/core/tests/suite/realtime_conversation.rs b/codex-rs/core/tests/suite/realtime_conversation.rs index c9f991e63a..20f1868f87 100644 --- a/codex-rs/core/tests/suite/realtime_conversation.rs +++ b/codex-rs/core/tests/suite/realtime_conversation.rs @@ -286,6 +286,7 @@ async fn conversation_start_audio_text_close_round_trip() -> Result<()> { 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, @@ -438,6 +439,7 @@ async fn conversation_start_defaults_to_v2_and_gpt_realtime_1_5() -> Result<()> 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, @@ -531,6 +533,7 @@ async fn conversation_webrtc_frameless_chatgpt_sends_codex_headers_to_backend() 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, @@ -657,6 +660,7 @@ async fn conversation_webrtc_start_posts_generated_session() -> Result<()> { 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, @@ -850,6 +854,7 @@ async fn conversation_webrtc_start_uses_avas_query() -> Result<()> { 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, @@ -954,6 +959,7 @@ async fn conversation_webrtc_default_v1_ignores_configured_v2_voice() -> Result< 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, @@ -1020,6 +1026,7 @@ async fn conversation_webrtc_default_v1_rejects_explicit_v2_voice() -> Result<() 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, @@ -1096,6 +1103,7 @@ async fn conversation_webrtc_start_uses_configured_call_base_url_for_avas() -> R 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, @@ -1209,6 +1217,7 @@ async fn conversation_webrtc_close_while_sideband_connecting_drops_pending_join( 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, @@ -1315,6 +1324,7 @@ async fn conversation_webrtc_sideband_connect_failure_closes_with_error() -> Res 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, @@ -1414,6 +1424,7 @@ async fn conversation_start_uses_openai_env_key_fallback_with_chatgpt_auth() -> 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, @@ -1511,6 +1522,7 @@ async fn assert_transport_close_tail_flush( test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { client_managed_handoffs: false, + delegation_ack_filler: None, flush_transcript_tail_on_session_end, codex_responses_as_items: false, codex_response_item_prefix: None, @@ -1633,6 +1645,7 @@ async fn conversation_start_preflight_failure_emits_realtime_error_only() -> Res 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, @@ -1690,6 +1703,7 @@ async fn conversation_start_connect_failure_emits_realtime_error_only() -> Resul 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, @@ -1795,6 +1809,7 @@ async fn conversation_second_start_replaces_runtime() -> Result<()> { 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, @@ -1831,6 +1846,7 @@ async fn conversation_second_start_replaces_runtime() -> Result<()> { 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, @@ -1938,6 +1954,7 @@ async fn conversation_uses_experimental_realtime_ws_base_url_override() -> Resul 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, @@ -2013,6 +2030,7 @@ async fn conversation_uses_default_realtime_backend_prompt() -> Result<()> { 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, @@ -2096,6 +2114,7 @@ async fn conversation_uses_empty_instructions_for_null_or_empty_prompt() -> Resu 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, @@ -2172,6 +2191,7 @@ async fn conversation_uses_explicit_start_voice() -> Result<()> { 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, @@ -2240,6 +2260,7 @@ async fn conversation_uses_configured_realtime_voice() -> Result<()> { 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, @@ -2296,6 +2317,7 @@ async fn conversation_rejects_voice_for_wrong_realtime_version() -> Result<()> { 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, @@ -2353,6 +2375,7 @@ async fn conversation_uses_experimental_realtime_ws_backend_prompt_override() -> 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, @@ -2436,6 +2459,7 @@ async fn conversation_uses_experimental_realtime_ws_startup_context_override() - 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, @@ -2513,6 +2537,7 @@ async fn conversation_disables_realtime_startup_context_with_empty_override() -> 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, @@ -2583,6 +2608,7 @@ async fn conversation_start_injects_startup_context_from_thread_history() -> Res 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, @@ -2708,6 +2734,7 @@ async fn conversation_startup_context_current_thread_selects_many_turns_by_budge 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, @@ -2826,6 +2853,7 @@ async fn conversation_startup_context_falls_back_to_workspace_map() -> Result<() 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, @@ -2896,6 +2924,7 @@ async fn conversation_startup_context_is_truncated_and_sent_once_per_start() -> 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, @@ -2987,6 +3016,7 @@ async fn conversation_user_text_turn_is_not_sent_to_realtime() -> Result<()> { 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, @@ -3094,6 +3124,7 @@ async fn realtime_v2_noop_tool_call_returns_empty_function_output_without_respon 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, @@ -3203,6 +3234,7 @@ async fn conversation_mirrors_assistant_message_text_to_realtime_handoff() -> Re 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, @@ -3373,6 +3405,7 @@ async fn conversation_flushes_assistant_deltas_every_200ms_for_v3_handoff() -> R 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, @@ -3551,6 +3584,7 @@ async fn conversation_handoff_persists_across_item_done_until_turn_complete() -> 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, @@ -3713,6 +3747,7 @@ async fn inbound_handoff_request_starts_turn() -> Result<()> { 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, @@ -3833,6 +3868,7 @@ async fn inbound_handoff_request_uses_active_transcript() -> Result<()> { 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, @@ -3946,6 +3982,7 @@ async fn inbound_handoff_request_sends_transcript_delta_after_each_handoff() -> 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, @@ -4078,6 +4115,7 @@ async fn conversation_close_routes_only_remaining_transcript_tail_once() -> Resu test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { client_managed_handoffs: false, + delegation_ack_filler: None, flush_transcript_tail_on_session_end: true, codex_responses_as_items: false, codex_response_item_prefix: None, @@ -4171,6 +4209,7 @@ async fn inbound_conversation_item_does_not_start_turn_and_still_forwards_audio( 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, @@ -4304,6 +4343,7 @@ async fn delegated_turn_user_role_echo_does_not_redelegate_and_still_forwards_au 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, @@ -4467,6 +4507,7 @@ async fn inbound_handoff_request_does_not_block_realtime_event_forwarding() -> R 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, @@ -4619,6 +4660,7 @@ async fn inbound_handoff_request_steers_active_turn() -> Result<()> { 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, @@ -4782,6 +4824,7 @@ async fn inbound_handoff_request_starts_turn_and_does_not_block_realtime_audio() 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, diff --git a/codex-rs/core/tests/suite/realtime_initial_items.rs b/codex-rs/core/tests/suite/realtime_initial_items.rs index 4fc037eae0..06de0356b1 100644 --- a/codex-rs/core/tests/suite/realtime_initial_items.rs +++ b/codex-rs/core/tests/suite/realtime_initial_items.rs @@ -199,6 +199,7 @@ async fn assert_start_error(params: ConversationStartParams, expected_error: &st fn start_params(version: RealtimeConversationVersion) -> ConversationStartParams { 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, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 724630c017..3155da6743 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -200,6 +200,9 @@ pub struct W3cTraceContext { pub struct ConversationStartParams { /// Whether Codex response handoffs are managed through explicit client append calls. pub client_managed_handoffs: bool, + /// Whether a realtime V3 delegation produces an acknowledgement filler. + /// `None` preserves the Realtime API's default behavior. + pub delegation_ack_filler: Option, /// Whether to route any remaining transcript tail through Codex when the session ends. /// TODO: Remove this rollout knob once transcript-tail flushing is always enabled. pub flush_transcript_tail_on_session_end: bool,