From 1b5a16f05eb9e7c19ae6390bac9d7c817c860678 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 1 Apr 2026 21:18:45 -0700 Subject: [PATCH] Extract request_user_input normalization into codex-tools (#16503) ## Why This is another incremental step in the `codex-core` -> `codex-tools` migration called out in `AGENTS.md`: keep pure tool-definition and wire-shaping logic out of `codex-core` so the core crate can stay focused on runtime orchestration. `request_user_input` already had its spec and mode-availability helpers in `codex-tools` after #16471. The remaining argument validation and normalization still lived in the core runtime handler, which left that tool split across the two crates. ## What Changed - Export `REQUEST_USER_INPUT_TOOL_NAME` and `normalize_request_user_input_args()` from `codex-rs/tools/src/request_user_input_tool.rs`. - Use that `codex-tools` surface from `codex-rs/core/src/tools/spec.rs` and `codex-rs/core/src/tools/handlers/request_user_input.rs`. - Keep the core handler responsible for payload parsing, session dispatch, cancellation handling, and response serialization. This is intended to be a straight refactor with no behavior change. ## Verification - `cargo test -p codex-tools` - `cargo test -p codex-core request_user_input` --- .../src/tools/handlers/request_user_input.rs | 32 +++++++------------ codex-rs/core/src/tools/spec.rs | 3 +- codex-rs/tools/src/lib.rs | 2 ++ codex-rs/tools/src/request_user_input_tool.rs | 23 ++++++++++++- 4 files changed, 38 insertions(+), 22 deletions(-) diff --git a/codex-rs/core/src/tools/handlers/request_user_input.rs b/codex-rs/core/src/tools/handlers/request_user_input.rs index a5d43d574..f12defd0e 100644 --- a/codex-rs/core/src/tools/handlers/request_user_input.rs +++ b/codex-rs/core/src/tools/handlers/request_user_input.rs @@ -7,6 +7,8 @@ use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; use async_trait::async_trait; use codex_protocol::request_user_input::RequestUserInputArgs; +use codex_tools::REQUEST_USER_INPUT_TOOL_NAME; +use codex_tools::normalize_request_user_input_args; use codex_tools::request_user_input_unavailable_message; pub struct RequestUserInputHandler { @@ -33,9 +35,9 @@ impl ToolHandler for RequestUserInputHandler { let arguments = match payload { ToolPayload::Function { arguments } => arguments, _ => { - return Err(FunctionCallError::RespondToModel( - "request_user_input handler received unsupported payload".to_string(), - )); + return Err(FunctionCallError::RespondToModel(format!( + "{REQUEST_USER_INPUT_TOOL_NAME} handler received unsupported payload" + ))); } }; @@ -46,31 +48,21 @@ impl ToolHandler for RequestUserInputHandler { return Err(FunctionCallError::RespondToModel(message)); } - let mut args: RequestUserInputArgs = parse_arguments(&arguments)?; - let missing_options = args - .questions - .iter() - .any(|question| question.options.as_ref().is_none_or(Vec::is_empty)); - if missing_options { - return Err(FunctionCallError::RespondToModel( - "request_user_input requires non-empty options for every question".to_string(), - )); - } - for question in &mut args.questions { - question.is_other = true; - } + let args: RequestUserInputArgs = parse_arguments(&arguments)?; + let args = + normalize_request_user_input_args(args).map_err(FunctionCallError::RespondToModel)?; let response = session .request_user_input(turn.as_ref(), call_id, args) .await .ok_or_else(|| { - FunctionCallError::RespondToModel( - "request_user_input was cancelled before receiving a response".to_string(), - ) + FunctionCallError::RespondToModel(format!( + "{REQUEST_USER_INPUT_TOOL_NAME} was cancelled before receiving a response" + )) })?; let content = serde_json::to_string(&response).map_err(|err| { FunctionCallError::Fatal(format!( - "failed to serialize request_user_input response: {err}" + "failed to serialize {REQUEST_USER_INPUT_TOOL_NAME} response: {err}" )) })?; diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index a484749c7..74f46c2a9 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -15,6 +15,7 @@ use codex_protocol::openai_models::ApplyPatchToolType; use codex_protocol::openai_models::ConfigShellToolType; use codex_tools::CommandToolOptions; use codex_tools::DiscoverableTool; +use codex_tools::REQUEST_USER_INPUT_TOOL_NAME; use codex_tools::ShellToolOptions; use codex_tools::SpawnAgentToolOptions; use codex_tools::TOOL_SEARCH_DEFAULT_LIMIT; @@ -344,7 +345,7 @@ pub(crate) fn build_specs_with_discoverable_tools( /*supports_parallel_tool_calls*/ false, config.code_mode_enabled, ); - builder.register_handler("request_user_input", request_user_input_handler); + builder.register_handler(REQUEST_USER_INPUT_TOOL_NAME, request_user_input_handler); } if config.request_permissions_tool_enabled { diff --git a/codex-rs/tools/src/lib.rs b/codex-rs/tools/src/lib.rs index cb10ba27b..2e58fa152 100644 --- a/codex-rs/tools/src/lib.rs +++ b/codex-rs/tools/src/lib.rs @@ -67,7 +67,9 @@ pub use mcp_resource_tool::create_read_mcp_resource_tool; pub use mcp_tool::mcp_call_tool_result_output_schema; pub use mcp_tool::parse_mcp_tool; pub use plan_tool::create_update_plan_tool; +pub use request_user_input_tool::REQUEST_USER_INPUT_TOOL_NAME; pub use request_user_input_tool::create_request_user_input_tool; +pub use request_user_input_tool::normalize_request_user_input_args; pub use request_user_input_tool::request_user_input_tool_description; pub use request_user_input_tool::request_user_input_unavailable_message; pub use responses_api::FreeformTool; diff --git a/codex-rs/tools/src/request_user_input_tool.rs b/codex-rs/tools/src/request_user_input_tool.rs index dd4f4f995..77c536738 100644 --- a/codex-rs/tools/src/request_user_input_tool.rs +++ b/codex-rs/tools/src/request_user_input_tool.rs @@ -3,8 +3,11 @@ use crate::ResponsesApiTool; use crate::ToolSpec; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::TUI_VISIBLE_COLLABORATION_MODES; +use codex_protocol::request_user_input::RequestUserInputArgs; use std::collections::BTreeMap; +pub const REQUEST_USER_INPUT_TOOL_NAME: &str = "request_user_input"; + pub fn create_request_user_input_tool(description: String) -> ToolSpec { let option_props = BTreeMap::from([ ( @@ -78,7 +81,7 @@ pub fn create_request_user_input_tool(description: String) -> ToolSpec { let properties = BTreeMap::from([("questions".to_string(), questions_schema)]); ToolSpec::Function(ResponsesApiTool { - name: "request_user_input".to_string(), + name: REQUEST_USER_INPUT_TOOL_NAME.to_string(), description, strict: false, defer_loading: None, @@ -105,6 +108,24 @@ pub fn request_user_input_unavailable_message( } } +pub fn normalize_request_user_input_args( + mut args: RequestUserInputArgs, +) -> Result { + let missing_options = args + .questions + .iter() + .any(|question| question.options.as_ref().is_none_or(Vec::is_empty)); + if missing_options { + return Err("request_user_input requires non-empty options for every question".to_string()); + } + + for question in &mut args.questions { + question.is_other = true; + } + + Ok(args) +} + pub fn request_user_input_tool_description(default_mode_request_user_input: bool) -> String { let allowed_modes = format_allowed_modes(default_mode_request_user_input); format!(