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.
This commit is contained in:
Shijie Rao
2026-06-11 23:23:44 -07:00
committed by GitHub
Unverified
parent a94deb5fa7
commit 7a19b14229
4 changed files with 39 additions and 3 deletions
+6 -3
View File
@@ -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) {
@@ -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",
+2
View File
@@ -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()
]
@@ -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(),