From 20465d30aed42753da78a9d03b43cda799f356be Mon Sep 17 00:00:00 2001 From: Shijie Rao Date: Fri, 17 Jul 2026 13:56:38 +0000 Subject: [PATCH] Suppress empty multi-agent mode messages (#33862) ## What changed - Treat an empty custom `multi_agent_mode_hint_text` as having no instructions to emit. - Avoid adding an empty multi-agent mode message both on the initial turn and when the configured hint changes after resuming a session. ## Testing - Update the empty-hint integration test to assert that no mode message is sent. - Add resume coverage that verifies changing a custom hint to an empty string emits no update. GitOrigin-RevId: 8033292115936fac2cf22f85141278595804ee61 --- .../context/multi_agent_mode_instructions.rs | 11 +++- codex-rs/core/src/context_manager/updates.rs | 6 +- codex-rs/core/src/session/mod.rs | 8 +-- codex-rs/core/tests/suite/multi_agent_mode.rs | 59 ++++++++++++++++++- 4 files changed, 74 insertions(+), 10 deletions(-) diff --git a/codex-rs/core/src/context/multi_agent_mode_instructions.rs b/codex-rs/core/src/context/multi_agent_mode_instructions.rs index 9773875d8953..635b65cb545e 100644 --- a/codex-rs/core/src/context/multi_agent_mode_instructions.rs +++ b/codex-rs/core/src/context/multi_agent_mode_instructions.rs @@ -12,8 +12,15 @@ pub(crate) struct MultiAgentModeInstructions { } impl MultiAgentModeInstructions { - pub(crate) fn new(multi_agent_mode: MultiAgentMode) -> Self { - Self { multi_agent_mode } + pub(crate) fn from_mode(multi_agent_mode: MultiAgentMode) -> Option { + if matches!( + &multi_agent_mode, + MultiAgentMode::Custom(hint_text) if hint_text.is_empty() + ) { + return None; + } + + Some(Self { multi_agent_mode }) } } diff --git a/codex-rs/core/src/context_manager/updates.rs b/codex-rs/core/src/context_manager/updates.rs index 70a8ba16967a..69373f39f8da 100644 --- a/codex-rs/core/src/context_manager/updates.rs +++ b/codex-rs/core/src/context_manager/updates.rs @@ -95,9 +95,11 @@ fn build_multi_agent_mode_update_item( } match effective_multi_agent_mode { - Some(multi_agent_mode) => Some(MultiAgentModeInstructions::new(multi_agent_mode).render()), + Some(multi_agent_mode) => MultiAgentModeInstructions::from_mode(multi_agent_mode) + .map(|instructions| instructions.render()), None if previous.multi_agent_mode == Some(MultiAgentMode::Proactive) => { - Some(MultiAgentModeInstructions::new(MultiAgentMode::ExplicitRequestOnly).render()) + MultiAgentModeInstructions::from_mode(MultiAgentMode::ExplicitRequestOnly) + .map(|instructions| instructions.render()) } None => None, } diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index da0992cb3d83..79cedbffbde5 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -3471,10 +3471,10 @@ impl Session { { items.push(usage_hint_message); } - if let Some(multi_agent_mode) = multi_agents::effective_multi_agent_mode(turn_context) { - items.push(ContextualUserFragment::into( - MultiAgentModeInstructions::new(multi_agent_mode), - )); + if let Some(multi_agent_mode) = multi_agents::effective_multi_agent_mode(turn_context) + && let Some(instructions) = MultiAgentModeInstructions::from_mode(multi_agent_mode) + { + items.push(ContextualUserFragment::into(instructions)); } if let Some(contextual_user_message) = crate::context_manager::updates::build_contextual_user_message(contextual_user_sections) diff --git a/codex-rs/core/tests/suite/multi_agent_mode.rs b/codex-rs/core/tests/suite/multi_agent_mode.rs index 85ce4091446c..cb59873d6fae 100644 --- a/codex-rs/core/tests/suite/multi_agent_mode.rs +++ b/codex-rs/core/tests/suite/multi_agent_mode.rs @@ -193,7 +193,7 @@ async fn configured_mode_hint_uses_custom_mode_across_reasoning_efforts() -> Res } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn empty_configured_mode_hint_suppresses_builtin_text() -> Result<()> { +async fn empty_configured_mode_hint_emits_no_mode_message() -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; @@ -220,7 +220,62 @@ async fn empty_configured_mode_hint_suppresses_builtin_text() -> Result<()> { count_containing(&texts, NO_SPAWN_TEXT), count_containing(&texts, PROACTIVE_TEXT), ), - (1, 0, 0) + (0, 0, 0) + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn changing_configured_mode_hint_to_empty_emits_no_update() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + let responses = mount_sse_sequence( + &server, + (1..=2) + .map(|index| { + sse(vec![ + ev_response_created(&format!("resp-{index}")), + ev_completed(&format!("resp-{index}")), + ]) + }) + .collect(), + ) + .await; + let initial = test_codex() + .with_config(configure_custom_mode_hint) + .build(&server) + .await?; + let home = initial.home.clone(); + let rollout_path = initial + .session_configured + .rollout_path + .clone() + .expect("rollout path"); + + submit_turn(&initial.codex, "before resume", /*effort*/ None).await?; + drop(initial); + + let mut resume_builder = test_codex().with_config(|config| { + configure_multi_agent_v2(config); + config.multi_agent_v2.multi_agent_mode_hint_text = Some(String::new()); + }); + let resumed = resume_builder.resume(&server, home, rollout_path).await?; + submit_turn(&resumed.codex, "after resume", /*effort*/ None).await?; + + let requests = responses.requests(); + let first_input = requests[0].input(); + let first_texts = developer_texts(&first_input); + let resumed_input = requests[1].input(); + let resumed_texts = developer_texts(&resumed_input); + assert_eq!( + ( + count_containing(&first_texts, MULTI_AGENT_MODE_OPEN_TAG), + count_containing(&resumed_texts, MULTI_AGENT_MODE_OPEN_TAG), + count_containing(&resumed_texts, CUSTOM_MODE_HINT_TEXT), + ), + (1, 1, 1) ); Ok(())