diff --git a/codex-rs/core/src/realtime_conversation.rs b/codex-rs/core/src/realtime_conversation.rs index 8842c41c9..e614be106 100644 --- a/codex-rs/core/src/realtime_conversation.rs +++ b/codex-rs/core/src/realtime_conversation.rs @@ -373,7 +373,15 @@ pub(crate) async fn handle_audio( } fn realtime_text_from_handoff_request(handoff: &RealtimeHandoffRequested) -> Option { - (!handoff.input_transcript.is_empty()).then(|| handoff.input_transcript.clone()) + let messages = handoff + .messages + .iter() + .map(|message| message.text.as_str()) + .collect::>() + .join("\n"); + (!messages.is_empty()).then_some(messages).or_else(|| { + (!handoff.input_transcript.is_empty()).then(|| handoff.input_transcript.clone()) + }) } fn realtime_api_key( @@ -579,19 +587,39 @@ mod tests { use pretty_assertions::assert_eq; #[test] - fn extracts_text_from_handoff_request_input_transcript() { + fn extracts_text_from_handoff_request_messages() { let handoff = RealtimeHandoffRequested { handoff_id: "handoff_1".to_string(), item_id: "item_1".to_string(), - input_transcript: "hello".to_string(), - messages: vec![RealtimeHandoffMessage { - role: "user".to_string(), - text: "hello".to_string(), - }], + input_transcript: "ignored".to_string(), + messages: vec![ + RealtimeHandoffMessage { + role: "user".to_string(), + text: "hello".to_string(), + }, + RealtimeHandoffMessage { + role: "assistant".to_string(), + text: "hi there".to_string(), + }, + ], }; assert_eq!( realtime_text_from_handoff_request(&handoff), - Some("hello".to_string()) + Some("hello\nhi there".to_string()) + ); + } + + #[test] + fn extracts_text_from_handoff_request_input_transcript_if_messages_missing() { + let handoff = RealtimeHandoffRequested { + handoff_id: "handoff_1".to_string(), + item_id: "item_1".to_string(), + input_transcript: "ignored".to_string(), + messages: vec![], + }; + assert_eq!( + realtime_text_from_handoff_request(&handoff), + Some("ignored".to_string()) ); } diff --git a/codex-rs/core/tests/suite/realtime_conversation.rs b/codex-rs/core/tests/suite/realtime_conversation.rs index 32500299c..ff4e745dd 100644 --- a/codex-rs/core/tests/suite/realtime_conversation.rs +++ b/codex-rs/core/tests/suite/realtime_conversation.rs @@ -918,6 +918,80 @@ async fn inbound_handoff_request_starts_turn() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn inbound_handoff_request_uses_all_messages() -> Result<()> { + skip_if_no_network!(Ok(())); + + let api_server = start_mock_server().await; + let response_mock = responses::mount_sse_once( + &api_server, + responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_assistant_message("msg-1", "ok"), + responses::ev_completed("resp-1"), + ]), + ) + .await; + + let realtime_server = start_websocket_server(vec![vec![vec![ + json!({ + "type": "session.updated", + "session": { "id": "sess_inbound_multi", "instructions": "backend prompt" } + }), + json!({ + "type": "conversation.handoff.requested", + "handoff_id": "handoff_inbound_multi", + "item_id": "item_inbound_multi", + "input_transcript": "ignored", + "messages": [ + { "role": "assistant", "text": "assistant context" }, + { "role": "user", "text": "delegated query" }, + { "role": "assistant", "text": "assist confirm" }, + ] + }), + ]]]) + .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); + } + }); + let test = builder.build(&api_server).await?; + + test.codex + .submit(Op::RealtimeConversationStart(ConversationStartParams { + prompt: "backend prompt".to_string(), + session_id: None, + })) + .await?; + + let _ = wait_for_event_match(&test.codex, |msg| match msg { + EventMsg::RealtimeConversationRealtime(RealtimeConversationRealtimeEvent { + payload: RealtimeEvent::SessionUpdated { session_id, .. }, + }) => Some(session_id.clone()), + _ => None, + }) + .await; + + wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; + + let request = response_mock.single_request(); + let user_texts = request.message_input_texts("user"); + assert!( + user_texts + .iter() + .any(|text| text == "assistant context\ndelegated query\nassist confirm") + ); + + realtime_server.shutdown().await; + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn inbound_conversation_item_does_not_start_turn_and_still_forwards_audio() -> Result<()> { skip_if_no_network!(Ok(()));