From 7a19b14229b9ce70a734884ea73425d2c67ddd25 Mon Sep 17 00:00:00 2001 From: Shijie Rao Date: Thu, 11 Jun 2026 23:23:44 -0700 Subject: [PATCH] Keep request_user_input direct-model only (#27316) ## Why `request_user_input` has direct blocking semantics when invoked by the model. When it is exposed as a nested code-mode tool, the call has to flow through code-mode waiting and continuation behavior instead, which is not the behavior we want for this user-input request surface. ## What changed - Mark `request_user_input` with `ToolExposure::DirectModelOnly` when registering the core utility tool. - Keep `request_user_input` direct-model visible, including in code-mode-only planning. - Add focused `spec_plan_tests` coverage that verifies `request_user_input` remains visible and registered as direct-model-only, while it is omitted from the nested code-mode tool description. No active goal suppression or runtime unavailability behavior is included in this PR. ## Validation - No new build/test run for this housekeeping pass, per maintainer request. - Earlier targeted run, confirmed from session context: `just test -p codex-core request_user_input` passed. --- codex-rs/core/src/tools/spec_plan.rs | 9 ++++-- codex-rs/core/src/tools/spec_plan_tests.rs | 30 +++++++++++++++++++ codex-rs/core/tests/suite/code_mode.rs | 2 ++ .../tests/suite/model_runtime_selectors.rs | 1 + 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/codex-rs/core/src/tools/spec_plan.rs b/codex-rs/core/src/tools/spec_plan.rs index 79c86b94d..76c1e4389 100644 --- a/codex-rs/core/src/tools/spec_plan.rs +++ b/codex-rs/core/src/tools/spec_plan.rs @@ -643,9 +643,12 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut planned_tools.add(PlanHandler); if turn_context.config.experimental_request_user_input_enabled { - planned_tools.add(RequestUserInputHandler { - available_modes: request_user_input_available_modes(features), - }); + planned_tools.add_with_exposure( + RequestUserInputHandler { + available_modes: request_user_input_available_modes(features), + }, + ToolExposure::DirectModelOnly, + ); } if features.enabled(Feature::RequestPermissionsTool) { diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index 2f7f23947..3f9832c85 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -431,6 +431,10 @@ async fn request_user_input_tool_respects_experimental_config_gate() { let enabled = probe(|_| {}).await; enabled.assert_visible_contains(&["request_user_input"]); enabled.assert_registered_contains(&["request_user_input"]); + assert_eq!( + enabled.exposure("request_user_input"), + ToolExposure::DirectModelOnly + ); let disabled = probe(|turn| { update_config(turn, |config| { @@ -442,6 +446,30 @@ async fn request_user_input_tool_respects_experimental_config_gate() { disabled.assert_registered_lacks(&["request_user_input"]); } +#[tokio::test] +async fn request_user_input_stays_direct_in_code_mode_only() { + let plan = probe(|turn| { + set_features(turn, &[Feature::CodeMode, Feature::CodeModeOnly]); + }) + .await; + + plan.assert_visible_contains(&[ + "request_user_input", + codex_code_mode::PUBLIC_TOOL_NAME, + codex_code_mode::WAIT_TOOL_NAME, + ]); + plan.assert_registered_contains(&["request_user_input"]); + assert_eq!( + plan.exposure("request_user_input"), + ToolExposure::DirectModelOnly + ); + + let ToolSpec::Freeform(exec) = plan.visible_spec(codex_code_mode::PUBLIC_TOOL_NAME) else { + panic!("expected code mode exec tool"); + }; + assert!(!exec.description.contains("request_user_input")); +} + #[tokio::test] async fn shell_family_registers_visible_unified_exec_and_hidden_legacy_shell() { let plan = probe(|turn| { @@ -1224,6 +1252,7 @@ async fn code_mode_only_can_expose_namespaced_multi_agent_v2_as_normal_tools() { vec![ "exec", "wait", + "request_user_input", "agents", // Hosted Responses tools. "web_search", @@ -1309,6 +1338,7 @@ async fn hosted_tools_follow_provider_auth_model_and_config_gates() { // Code-mode entrypoints. codex_code_mode::PUBLIC_TOOL_NAME, codex_code_mode::WAIT_TOOL_NAME, + "request_user_input", // Multi-agent v2 tools. "spawn_agent", "send_message", diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index 0f954bac7..a25b5bd4d 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -502,6 +502,7 @@ async fn code_mode_only_restricts_prompt_tools() -> Result<()> { vec![ "exec".to_string(), "wait".to_string(), + "request_user_input".to_string(), "web_search".to_string() ] ); @@ -588,6 +589,7 @@ if (!tool) { vec![ "exec".to_string(), "wait".to_string(), + "request_user_input".to_string(), "web_search".to_string(), "image_generation".to_string() ] diff --git a/codex-rs/core/tests/suite/model_runtime_selectors.rs b/codex-rs/core/tests/suite/model_runtime_selectors.rs index 38c95394e..3ee758372 100644 --- a/codex-rs/core/tests/suite/model_runtime_selectors.rs +++ b/codex-rs/core/tests/suite/model_runtime_selectors.rs @@ -173,6 +173,7 @@ async fn remote_tool_mode_selector_overrides_feature_flags() -> Result<()> { // Code-mode entrypoints. codex_code_mode::PUBLIC_TOOL_NAME.to_string(), codex_code_mode::WAIT_TOOL_NAME.to_string(), + "request_user_input".to_string(), // Hosted Responses tools. "web_search".to_string(), "image_generation".to_string(),