From 8f8a0f55ceda03680b28bf92a99ca2393793b895 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Wed, 11 Mar 2026 11:14:51 -0700 Subject: [PATCH] spawn prompt (#14362) # External (non-OpenAI) Pull Request Requirements Before opening this Pull Request, please read the dedicated "Contributing" markdown file or your PR may be closed: https://github.com/openai/codex/blob/main/docs/contributing.md If your PR conforms to our contribution guidelines, replace this text with a detailed and high quality description of your changes. Include a link to a bug report or enhancement request. --- codex-rs/core/src/codex.rs | 12 ++ codex-rs/core/src/codex_tests.rs | 2 + codex-rs/core/src/tools/spec.rs | 120 +++++++++++- codex-rs/core/tests/suite/mod.rs | 1 + .../tests/suite/spawn_agent_description.rs | 185 ++++++++++++++++++ 5 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 codex-rs/core/tests/suite/spawn_agent_description.rs diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index e4232c0c9..d46ec5996 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -32,6 +32,7 @@ use crate::features::maybe_push_unstable_features_warning; #[cfg(test)] use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig; use crate::models_manager::manager::ModelsManager; +use crate::models_manager::manager::RefreshStrategy; use crate::parse_command::parse_command; use crate::parse_turn_item; use crate::realtime_conversation::RealtimeConversationManager; @@ -776,6 +777,9 @@ impl TurnContext { let features = self.features.clone(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &models_manager + .list_models(RefreshStrategy::OnlineIfUncached) + .await, features: &features, web_search_mode: self.tools_config.web_search_mode, session_source: self.session_source.clone(), @@ -1163,6 +1167,7 @@ impl Session { session_configuration: &SessionConfiguration, per_turn_config: Config, model_info: ModelInfo, + models_manager: &ModelsManager, network: Option, sub_id: String, js_repl: Arc, @@ -1184,6 +1189,7 @@ impl Session { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &models_manager.try_list_models().unwrap_or_default(), features: &per_turn_config.features, web_search_mode: Some(per_turn_config.web_search_mode.value()), session_source: session_source.clone(), @@ -2310,6 +2316,7 @@ impl Session { &session_configuration, per_turn_config, model_info, + &self.services.models_manager, self.services .network_proxy .as_ref() @@ -5147,6 +5154,11 @@ async fn spawn_review_thread( let review_web_search_mode = WebSearchMode::Disabled; let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &review_model_info, + available_models: &sess + .services + .models_manager + .list_models(RefreshStrategy::OnlineIfUncached) + .await, features: &review_features, web_search_mode: Some(review_web_search_mode), session_source: parent_turn_context.session_source.clone(), diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 2d627671d..6d3270dcd 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -2250,6 +2250,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { &session_configuration, per_turn_config, model_info, + &models_manager, None, "turn_id".to_string(), Arc::clone(&js_repl), @@ -2810,6 +2811,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( &session_configuration, per_turn_config, model_info, + &models_manager, None, "turn_id".to_string(), Arc::clone(&js_repl), diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 1b287a216..cf9eaa32f 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -29,6 +29,7 @@ use codex_protocol::openai_models::ApplyPatchToolType; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::ModelInfo; +use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::WebSearchToolType; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; @@ -90,6 +91,7 @@ pub enum UnifiedExecBackendConfig { #[derive(Debug, Clone)] pub(crate) struct ToolsConfig { + pub available_models: Vec, pub shell_type: ConfigShellToolType, shell_command_backend: ShellCommandBackendConfig, pub unified_exec_backend: UnifiedExecBackendConfig, @@ -117,6 +119,7 @@ pub(crate) struct ToolsConfig { pub(crate) struct ToolsConfigParams<'a> { pub(crate) model_info: &'a ModelInfo, + pub(crate) available_models: &'a Vec, pub(crate) features: &'a Features, pub(crate) web_search_mode: Option, pub(crate) session_source: SessionSource, @@ -126,6 +129,7 @@ impl ToolsConfig { pub fn new(params: &ToolsConfigParams) -> Self { let ToolsConfigParams { model_info, + available_models: available_models_ref, features, web_search_mode, session_source, @@ -195,6 +199,7 @@ impl ToolsConfig { ); Self { + available_models: available_models_ref.to_vec(), shell_type, shell_command_backend, unified_exec_backend, @@ -765,6 +770,7 @@ fn create_collab_input_items_schema() -> JsonSchema { } fn create_spawn_agent_tool(config: &ToolsConfig) -> ToolSpec { + let available_models_description = spawn_agent_models_description(&config.available_models); let properties = BTreeMap::from([ ( "message".to_string(), @@ -815,8 +821,11 @@ fn create_spawn_agent_tool(config: &ToolsConfig) -> ToolSpec { ToolSpec::Function(ResponsesApiTool { name: "spawn_agent".to_string(), - description: r#"Spawn a sub-agent for a well-scoped task. Returns the agent id (and user-facing nickname when available) to use to communicate with this agent. This spawn_agent tool provides you access to smaller but more efficient sub-agents. A mini model can solve many tasks faster than the main model. You should follow the rules and guidelines below to use this tool. + description: format!( + r#" + Only use `spawn_agent` if and only if the user explicitly asked for sub-agents or parallel agent work. Spawn a sub-agent for a well-scoped task. Returns the agent id (and user-facing nickname when available) to use to communicate with this agent. This spawn_agent tool provides you access to smaller but more efficient sub-agents. A mini model can solve many tasks faster than the main model. You should follow the rules and guidelines below to use this tool. +{available_models_description} ### When to delegate vs. do the subtask yourself - First, quickly analyze the overall user task and form a succinct high-level plan. Identify which tasks are immediate blockers on the critical path, and which tasks are sidecar tasks that are needed but can run in parallel without blocking the next local step. As part of that plan, explicitly decide what immediate task you should do locally right now. Do this planning step before delegating to agents so you do not hand off the immediate blocking task to a submodel and then waste time waiting on it. - Use the smaller subagent when a subtask is easy enough for it to handle and can run in parallel with your local work. Prefer delegating concrete, bounded sidecar tasks that materially advance the main task without blocking your immediate next local step. @@ -845,7 +854,7 @@ fn create_spawn_agent_tool(config: &ToolsConfig) -> ToolSpec { - Split implementation into disjoint codebase slices and spawn multiple agents for them in parallel when the write scopes do not overlap. - Delegate verification only when it can run in parallel with ongoing implementation and is likely to catch a concrete risk before final integration. - The key is to find opportunities to spawn multiple independent subtasks in parallel within the same round, while ensuring each subtask is well-defined, self-contained, and materially advances the main task."# - .to_string(), + ), strict: false, parameters: JsonSchema::Object { properties, @@ -856,6 +865,35 @@ fn create_spawn_agent_tool(config: &ToolsConfig) -> ToolSpec { }) } +fn spawn_agent_models_description(models: &[ModelPreset]) -> String { + let visible_models: Vec<&ModelPreset> = + models.iter().filter(|model| model.show_in_picker).collect(); + if visible_models.is_empty() { + return "No picker-visible models are currently loaded.".to_string(); + } + + visible_models + .into_iter() + .map(|model| { + let efforts = model + .supported_reasoning_efforts + .iter() + .map(|preset| format!("{} ({})", preset.effort, preset.description)) + .collect::>() + .join(", "); + format!( + "- {} (`{}`): {} Default reasoning effort: {}. Supported reasoning efforts: {}.", + model.display_name, + model.model, + model.description, + model.default_reasoning_effort, + efforts + ) + }) + .collect::>() + .join("\n") +} + fn create_spawn_agents_on_csv_tool() -> ToolSpec { let mut properties = BTreeMap::new(); properties.insert( @@ -2734,8 +2772,10 @@ mod tests { let model_info = model_info_from_models_json("gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); + let available_models = Vec::new(); let config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Live), session_source: SessionSource::Cli, @@ -2805,8 +2845,10 @@ mod tests { ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::Collab); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -2827,8 +2869,10 @@ mod tests { let mut features = Features::with_defaults(); features.enable(Feature::SpawnCsv); features.normalize_dependencies(); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -2855,8 +2899,10 @@ mod tests { ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::Artifact); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -2874,8 +2920,10 @@ mod tests { features.enable(Feature::SpawnCsv); features.normalize_dependencies(); features.enable(Feature::Sqlite); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::SubAgent(SubAgentSource::Other( @@ -2904,8 +2952,10 @@ mod tests { let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -2918,8 +2968,10 @@ mod tests { ); features.enable(Feature::DefaultModeRequestUserInput); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -2940,8 +2992,10 @@ mod tests { let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let features = Features::with_defaults(); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -2951,8 +3005,10 @@ mod tests { let mut features = Features::with_defaults(); features.enable(Feature::RequestPermissionsTool); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -2972,8 +3028,10 @@ mod tests { ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::RequestPermissions); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -2990,8 +3048,10 @@ mod tests { ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.disable(Feature::MemoryTool); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -3010,8 +3070,10 @@ mod tests { ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let features = Features::with_defaults(); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -3036,8 +3098,10 @@ mod tests { let mut features = Features::with_defaults(); features.enable(Feature::JsRepl); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -3058,8 +3122,10 @@ mod tests { let mut image_generation_features = default_features.clone(); image_generation_features.enable(Feature::ImageGeneration); + let available_models = Vec::new(); let default_tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &supported_model_info, + available_models: &available_models, features: &default_features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -3074,6 +3140,7 @@ mod tests { let supported_tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &supported_model_info, + available_models: &available_models, features: &image_generation_features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -3091,6 +3158,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &unsupported_model_info, + available_models: &available_models, features: &image_generation_features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -3127,8 +3195,10 @@ mod tests { ) { let _config = test_config(); let model_info = model_info_from_models_json(model_slug); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features, web_search_mode, session_source: SessionSource::Cli, @@ -3161,8 +3231,10 @@ mod tests { ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let features = Features::with_defaults(); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -3189,8 +3261,10 @@ mod tests { ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let features = Features::with_defaults(); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Live), session_source: SessionSource::Cli, @@ -3230,8 +3304,10 @@ mod tests { search_context_size: Some(codex_protocol::config_types::WebSearchContextSize::High), }; + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Live), session_source: SessionSource::Cli, @@ -3264,8 +3340,10 @@ mod tests { model_info.web_search_tool_type = WebSearchToolType::TextAndImage; let features = Features::with_defaults(); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Live), session_source: SessionSource::Cli, @@ -3296,8 +3374,10 @@ mod tests { let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let features = Features::with_defaults(); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -3319,8 +3399,10 @@ mod tests { let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let features = Features::with_defaults(); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -3510,8 +3592,10 @@ mod tests { let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Live), session_source: SessionSource::Cli, @@ -3534,8 +3618,10 @@ mod tests { features.enable(Feature::UnifiedExec); features.enable(Feature::ShellZshFork); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Live), session_source: SessionSource::Cli, @@ -3560,8 +3646,10 @@ mod tests { ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -3586,8 +3674,10 @@ mod tests { "list_dir".to_string(), ]; let features = Features::with_defaults(); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -3618,8 +3708,10 @@ mod tests { let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Live), session_source: SessionSource::Cli, @@ -3706,8 +3798,10 @@ mod tests { let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -3752,8 +3846,10 @@ mod tests { ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::Apps); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -3837,8 +3933,10 @@ mod tests { )])); let features = Features::with_defaults(); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -3848,8 +3946,10 @@ mod tests { let mut features = Features::with_defaults(); features.enable(Feature::Apps); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -3865,8 +3965,10 @@ mod tests { ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::Apps); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -3890,8 +3992,10 @@ mod tests { ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -3946,8 +4050,10 @@ mod tests { ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -3999,8 +4105,10 @@ mod tests { let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); features.enable(Feature::ApplyPatchFreeform); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -4054,8 +4162,10 @@ mod tests { ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -4261,8 +4371,10 @@ Examples of valid command strings: ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -4368,8 +4480,10 @@ Examples of valid command strings: let mut features = Features::with_defaults(); features.enable(Feature::CodeMode); features.enable(Feature::UnifiedExec); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, @@ -4396,8 +4510,10 @@ Examples of valid command strings: let mut features = Features::with_defaults(); features.enable(Feature::CodeMode); features.enable(Feature::UnifiedExec); + let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, + available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs index 0695fcb19..5ec63d952 100644 --- a/codex-rs/core/tests/suite/mod.rs +++ b/codex-rs/core/tests/suite/mod.rs @@ -121,6 +121,7 @@ mod shell_serialization; mod shell_snapshot; mod skill_approval; mod skills; +mod spawn_agent_description; mod sqlite_state; mod stream_error_allows_next_turn; mod stream_no_completed; diff --git a/codex-rs/core/tests/suite/spawn_agent_description.rs b/codex-rs/core/tests/suite/spawn_agent_description.rs new file mode 100644 index 000000000..ad822805a --- /dev/null +++ b/codex-rs/core/tests/suite/spawn_agent_description.rs @@ -0,0 +1,185 @@ +#![cfg(not(target_os = "windows"))] +#![allow(clippy::unwrap_used, clippy::expect_used)] + +use anyhow::Result; +use codex_core::CodexAuth; +use codex_core::features::Feature; +use codex_core::models_manager::manager::ModelsManager; +use codex_core::models_manager::manager::RefreshStrategy; +use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::openai_models::ConfigShellToolType; +use codex_protocol::openai_models::ModelInfo; +use codex_protocol::openai_models::ModelVisibility; +use codex_protocol::openai_models::ModelsResponse; +use codex_protocol::openai_models::ReasoningEffort; +use codex_protocol::openai_models::ReasoningEffortPreset; +use codex_protocol::openai_models::TruncationPolicyConfig; +use codex_protocol::openai_models::default_input_modalities; +use core_test_support::responses::ev_completed; +use core_test_support::responses::ev_response_created; +use core_test_support::responses::mount_models_once; +use core_test_support::responses::mount_sse_once; +use core_test_support::responses::sse; +use core_test_support::responses::start_mock_server; +use core_test_support::test_codex::test_codex; +use serde_json::Value; +use std::sync::Arc; +use std::time::Duration; +use std::time::Instant; +use tokio::time::sleep; + +const SPAWN_AGENT_TOOL_NAME: &str = "spawn_agent"; + +fn spawn_agent_description(body: &Value) -> Option { + body.get("tools") + .and_then(Value::as_array) + .and_then(|tools| { + tools.iter().find_map(|tool| { + if tool.get("name").and_then(Value::as_str) == Some(SPAWN_AGENT_TOOL_NAME) { + tool.get("description") + .and_then(Value::as_str) + .map(str::to_string) + } else { + None + } + }) + }) +} + +fn test_model_info( + slug: &str, + display_name: &str, + description: &str, + visibility: ModelVisibility, + default_reasoning_level: ReasoningEffort, + supported_reasoning_levels: Vec, +) -> ModelInfo { + ModelInfo { + slug: slug.to_string(), + display_name: display_name.to_string(), + description: Some(description.to_string()), + default_reasoning_level: Some(default_reasoning_level), + supported_reasoning_levels, + shell_type: ConfigShellToolType::ShellCommand, + visibility, + supported_in_api: true, + input_modalities: default_input_modalities(), + prefer_websockets: false, + used_fallback_model_metadata: false, + priority: 1, + upgrade: None, + base_instructions: "base instructions".to_string(), + model_messages: None, + supports_reasoning_summaries: false, + default_reasoning_summary: ReasoningSummary::Auto, + support_verbosity: false, + default_verbosity: None, + availability_nux: None, + apply_patch_tool_type: None, + web_search_tool_type: Default::default(), + truncation_policy: TruncationPolicyConfig::bytes(10_000), + supports_parallel_tool_calls: false, + supports_image_detail_original: false, + context_window: Some(272_000), + auto_compact_token_limit: None, + effective_context_window_percent: 95, + experimental_supported_tools: Vec::new(), + } +} + +async fn wait_for_model_available(manager: &Arc, slug: &str) { + let deadline = Instant::now() + Duration::from_secs(2); + loop { + let available_models = manager.list_models(RefreshStrategy::Online).await; + if available_models.iter().any(|model| model.model == slug) { + return; + } + if Instant::now() >= deadline { + panic!("timed out waiting for remote model {slug} to appear"); + } + sleep(Duration::from_millis(25)).await; + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn spawn_agent_description_lists_visible_models_and_reasoning_efforts() -> Result<()> { + let server = start_mock_server().await; + mount_models_once( + &server, + ModelsResponse { + models: vec![ + test_model_info( + "visible-model", + "Visible Model", + "Fast and capable", + ModelVisibility::List, + ReasoningEffort::Medium, + vec![ + ReasoningEffortPreset { + effort: ReasoningEffort::Low, + description: "Quick scan".to_string(), + }, + ReasoningEffortPreset { + effort: ReasoningEffort::High, + description: "Deep dive".to_string(), + }, + ], + ), + test_model_info( + "hidden-model", + "Hidden Model", + "Should not be shown", + ModelVisibility::Hide, + ReasoningEffort::Low, + vec![ReasoningEffortPreset { + effort: ReasoningEffort::Low, + description: "Not visible".to_string(), + }], + ), + ], + }, + ) + .await; + let resp_mock = mount_sse_once( + &server, + sse(vec![ev_response_created("resp1"), ev_completed("resp1")]), + ) + .await; + + let mut builder = test_codex() + .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) + .with_model("visible-model") + .with_config(|config| { + config + .features + .enable(Feature::Collab) + .expect("test config should allow feature update"); + }); + let test = builder.build(&server).await?; + wait_for_model_available(&test.thread_manager.get_models_manager(), "visible-model").await; + + test.submit_turn("hello").await?; + + let body = resp_mock.single_request().body_json(); + let description = + spawn_agent_description(&body).expect("spawn_agent description should be present"); + + assert!( + description.contains("- Visible Model (`visible-model`): Fast and capable"), + "expected visible model summary in spawn_agent description: {description:?}" + ); + assert!( + description.contains("Default reasoning effort: medium."), + "expected default reasoning effort in spawn_agent description: {description:?}" + ); + assert!( + description.contains("low (Quick scan), high (Deep dive)."), + "expected reasoning efforts in spawn_agent description: {description:?}" + ); + assert!( + !description.contains("Hidden Model"), + "hidden picker model should be omitted from spawn_agent description: {description:?}" + ); + + Ok(()) +}