From 6c372fbef45f730b6d8aa727b0ec8332365588db Mon Sep 17 00:00:00 2001 From: jif Date: Mon, 12 Oct 2026 12:23:13 +0000 Subject: [PATCH] Allow turns when the agent message board is unavailable (#53369) ## Why With `multi_agent_v2.disable_direct_message` enabled, an unavailable agent message board could cause tool router construction to reject the turn. ## What changed Remove the requirement that the router expose the board's `post` tool. Unavailable board calls return an unsupported-call error to the model, allowing the turn to continue. ## Testing Update scenario coverage for a disabled board feature, legacy agents, and ephemeral sessions. Assert that unavailable board calls return an error to the model, that `post`, `send_message`, and `followup_task` are not advertised, and that no board database is created. GitOrigin-RevId: cb9571459c128d84c3c96f185e70c404e0463473 --- codex-rs/core/src/tools/spec_plan.rs | 23 +---------- .../suite/scenarios_agent_message_board.rs | 39 ++++++++++++++----- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/codex-rs/core/src/tools/spec_plan.rs b/codex-rs/core/src/tools/spec_plan.rs index 00fe53e244..5027a22355 100644 --- a/codex-rs/core/src/tools/spec_plan.rs +++ b/codex-rs/core/src/tools/spec_plan.rs @@ -542,33 +542,14 @@ pub(crate) fn finalize_tool_router( .filter(|info| !info.is_empty()); let child_management_tools = required_child_management_tool_names(turn_context, model_info); - let router = ToolRouter::from_parts( + Ok(ToolRouter::from_parts( registry, model_visible_specs, tool_mode, code_mode_tool_names, tool_namespaces_info, &child_management_tools, - ); - // Internal workers can inherit MAv2 configuration without using the board. - if multi_agent_v2_enabled(turn_context) - && collab_tools_enabled(turn_context, model_info) - && turn_context.config.multi_agent_v2.disable_direct_message - && !turn_context.session_source.is_internal() - { - let post_tool = ToolName::new( - turn_context.config.multi_agent_v2.tool_namespace.clone(), - "post", - ); - if !router.exposes_tool(&post_tool) { - return Err(CodexErrorDetails::InvalidRequest( - "disable_direct_message requires an available agent message board in this session" - .to_owned(), - ) - .into()); - } - } - Ok(router) + )) } fn apply_direct_model_only_namespace_overrides( diff --git a/codex-rs/core/tests/suite/scenarios_agent_message_board.rs b/codex-rs/core/tests/suite/scenarios_agent_message_board.rs index 073538e4f9..ca587b80c8 100644 --- a/codex-rs/core/tests/suite/scenarios_agent_message_board.rs +++ b/codex-rs/core/tests/suite/scenarios_agent_message_board.rs @@ -89,16 +89,28 @@ fn configure(config: &mut codex_core::config::Config) { #[test_case::test_case(true, false, false; "legacy_agents")] #[test_case::test_case(true, true, true; "ephemeral")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn board_requires_persistent_v2_runtime( +async fn unavailable_board_calls_return_model_errors( board_enabled: bool, multi_agent_v2: bool, ephemeral: bool, ) -> anyhow::Result<()> { let server = responses::start_mock_server().await; - let mock = responses::mount_sse_once(&server, done()).await; + let mock = responses::mount_sse_sequence( + &server, + vec![ + tool( + "missing-board", + "post", + json!({"new_channel_name":"design", "text":"A decision."}), + ), + done(), + ], + ) + .await; let test = test_codex() .with_config(move |config| { configure(config); + config.multi_agent_v2.disable_direct_message = true; if !board_enabled { config .features @@ -115,15 +127,22 @@ async fn board_requires_persistent_v2_runtime( }) .build_with_auto_env(&server) .await?; - test.submit_turn("Hello.").await?; - assert!( - responses::namespace_child_tool( - &mock.single_request().body_json(), - "collaboration", - "post" - ) - .is_none() + test.submit_turn("Try posting the decision on the board.") + .await?; + let requests = mock.requests(); + assert_eq!(requests.len(), 2); + assert_eq!( + requests[1] + .function_call_output_text("missing-board") + .as_deref(), + Some("unsupported call: collaborationpost") ); + for name in ["post", "send_message", "followup_task"] { + assert!( + responses::namespace_child_tool(&requests[0].body_json(), "collaboration", name) + .is_none() + ); + } assert!( !test .config