From ea8575f462116e8c046d0065a9065315472cbdde Mon Sep 17 00:00:00 2001 From: Friel Date: Mon, 6 Apr 2026 18:57:45 +0000 Subject: [PATCH] Keep request_user_input schema stable for subagents --- .../src/tools/handlers/request_user_input.rs | 76 +++++++++++++++++++ codex-rs/tools/src/tool_config.rs | 2 +- codex-rs/tools/src/tool_config_tests.rs | 4 +- .../tools/src/tool_registry_plan_tests.rs | 2 +- 4 files changed, 80 insertions(+), 4 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 dcc9445a94..72214251ab 100644 --- a/codex-rs/core/src/tools/handlers/request_user_input.rs +++ b/codex-rs/core/src/tools/handlers/request_user_input.rs @@ -5,6 +5,7 @@ use crate::tools::context::ToolPayload; use crate::tools::handlers::parse_arguments; use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; +use codex_protocol::protocol::SessionSource; use codex_protocol::request_user_input::RequestUserInputArgs; use codex_tools::REQUEST_USER_INPUT_TOOL_NAME; use codex_tools::normalize_request_user_input_args; @@ -39,6 +40,12 @@ impl ToolHandler for RequestUserInputHandler { } }; + if matches!(turn.session_source, SessionSource::SubAgent(_)) { + return Err(FunctionCallError::RespondToModel( + "request_user_input can only be used by the root thread".to_string(), + )); + } + let mode = session.collaboration_mode().await.mode; if let Some(message) = request_user_input_unavailable_message(mode, self.default_mode_request_user_input) @@ -67,3 +74,72 @@ impl ToolHandler for RequestUserInputHandler { Ok(FunctionToolOutput::from_text(content, Some(true))) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::codex::make_session_and_context; + use crate::turn_diff_tracker::TurnDiffTracker; + use codex_protocol::ThreadId; + use codex_protocol::protocol::SubAgentSource; + use pretty_assertions::assert_eq; + use serde_json::json; + use std::sync::Arc; + use tokio::sync::Mutex; + + #[tokio::test] + async fn request_user_input_rejects_subagent_threads() { + let (session, mut turn_context) = make_session_and_context().await; + turn_context.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id: ThreadId::new(), + depth: 1, + agent_path: None, + agent_nickname: None, + agent_role: None, + }); + + let err = match (RequestUserInputHandler { + default_mode_request_user_input: true, + }) + .handle(ToolInvocation { + session: Arc::new(session), + turn: Arc::new(turn_context), + tracker: Arc::new(Mutex::new(TurnDiffTracker::default())), + call_id: "call-1".to_string(), + tool_name: REQUEST_USER_INPUT_TOOL_NAME.to_string(), + tool_namespace: None, + payload: ToolPayload::Function { + arguments: json!({ + "questions": [{ + "header": "Hdr", + "question": "Pick one", + "id": "pick_one", + "options": [ + { + "label": "A", + "description": "A" + }, + { + "label": "B", + "description": "B" + } + ] + }] + }) + .to_string(), + }, + }) + .await + { + Ok(_) => panic!("subagents should not be allowed to request user input"), + Err(err) => err, + }; + + assert_eq!( + err, + FunctionCallError::RespondToModel( + "request_user_input can only be used by the root thread".to_string(), + ) + ); + } +} diff --git a/codex-rs/tools/src/tool_config.rs b/codex-rs/tools/src/tool_config.rs index f48f1b8fcc..60ddfd55fe 100644 --- a/codex-rs/tools/src/tool_config.rs +++ b/codex-rs/tools/src/tool_config.rs @@ -141,7 +141,7 @@ impl ToolsConfig { let include_collab_tools = features.enabled(Feature::Collab); let include_multi_agent_v2 = features.enabled(Feature::MultiAgentV2); let include_agent_jobs = features.enabled(Feature::SpawnCsv); - let include_request_user_input = !matches!(session_source, SessionSource::SubAgent(_)); + let include_request_user_input = true; let include_default_mode_request_user_input = include_request_user_input && features.enabled(Feature::DefaultModeRequestUserInput); let include_search_tool = diff --git a/codex-rs/tools/src/tool_config_tests.rs b/codex-rs/tools/src/tool_config_tests.rs index 168c8e80a0..f22d24a7a3 100644 --- a/codex-rs/tools/src/tool_config_tests.rs +++ b/codex-rs/tools/src/tool_config_tests.rs @@ -131,7 +131,7 @@ fn shell_zsh_fork_prefers_shell_command_over_unified_exec() { } #[test] -fn subagents_disable_request_user_input_and_agent_jobs_workers_opt_in_by_label() { +fn subagents_keep_request_user_input_schema_and_agent_jobs_workers_opt_in_by_label() { let model_info = model_info(); let mut features = Features::with_defaults(); features.enable(Feature::SpawnCsv); @@ -149,7 +149,7 @@ fn subagents_disable_request_user_input_and_agent_jobs_workers_opt_in_by_label() windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - assert!(!tools_config.request_user_input); + assert!(tools_config.request_user_input); assert!(!tools_config.default_mode_request_user_input); assert!(tools_config.agent_jobs_tools); assert!(tools_config.agent_jobs_worker_tools); diff --git a/codex-rs/tools/src/tool_registry_plan_tests.rs b/codex-rs/tools/src/tool_registry_plan_tests.rs index fd7ab10073..b62e989bd6 100644 --- a/codex-rs/tools/src/tool_registry_plan_tests.rs +++ b/codex-rs/tools/src/tool_registry_plan_tests.rs @@ -491,7 +491,7 @@ fn test_build_specs_agent_job_worker_tools_enabled() { "report_agent_job_result", ], ); - assert_lacks_tool_name(&tools, "request_user_input"); + assert_contains_tool_names(&tools, &["request_user_input"]); } #[test]