From b3fc1375fdcae181be9b92577088647793d6a440 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Feb 2026 11:57:41 -0800 Subject: [PATCH 01/16] Enable request_user_input in default mode --- codex-rs/app-server/tests/suite/v2/turn_start.rs | 2 +- .../core/src/tools/handlers/request_user_input.rs | 10 +++++----- .../core/templates/collaboration_mode/default.md | 2 +- codex-rs/core/tests/suite/request_user_input.rs | 12 ++---------- codex-rs/protocol/src/config_types.rs | 2 +- 5 files changed, 10 insertions(+), 18 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index eaa017d6ae80..d7e098c2f8e7 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -412,7 +412,7 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> { let payload = request.body_json(); assert_eq!(payload["model"].as_str(), Some("mock-model-collab")); let payload_text = payload.to_string(); - assert!(payload_text.contains("The `request_user_input` tool is unavailable in Default mode.")); + assert!(payload_text.contains("The `request_user_input` tool is available in Default mode.")); Ok(()) } 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 134294d4287f..06ce5d6f7852 100644 --- a/codex-rs/core/src/tools/handlers/request_user_input.rs +++ b/codex-rs/core/src/tools/handlers/request_user_input.rs @@ -117,9 +117,9 @@ mod tests { use pretty_assertions::assert_eq; #[test] - fn request_user_input_mode_availability_is_plan_only() { + fn request_user_input_mode_availability_includes_default_mode() { assert!(ModeKind::Plan.allows_request_user_input()); - assert!(!ModeKind::Default.allows_request_user_input()); + assert!(ModeKind::Default.allows_request_user_input()); assert!(!ModeKind::Execute.allows_request_user_input()); assert!(!ModeKind::PairProgramming.allows_request_user_input()); } @@ -129,7 +129,7 @@ mod tests { assert_eq!(request_user_input_unavailable_message(ModeKind::Plan), None); assert_eq!( request_user_input_unavailable_message(ModeKind::Default), - Some("request_user_input is unavailable in Default mode".to_string()) + None ); assert_eq!( request_user_input_unavailable_message(ModeKind::Execute), @@ -142,10 +142,10 @@ mod tests { } #[test] - fn request_user_input_tool_description_mentions_plan_only() { + fn request_user_input_tool_description_mentions_default_and_plan() { assert_eq!( request_user_input_tool_description(), - "Request user input for one to three short questions and wait for the response. This tool is only available in Plan mode.".to_string() + "Request user input for one to three short questions and wait for the response. This tool is only available in Default or Plan mode.".to_string() ); } } diff --git a/codex-rs/core/templates/collaboration_mode/default.md b/codex-rs/core/templates/collaboration_mode/default.md index 4efd963ba0c3..3206387a6a0e 100644 --- a/codex-rs/core/templates/collaboration_mode/default.md +++ b/codex-rs/core/templates/collaboration_mode/default.md @@ -8,4 +8,4 @@ Your active mode changes only when new developer instructions with a different ` {{REQUEST_USER_INPUT_AVAILABILITY}} -If a decision is necessary and cannot be discovered from local context, ask the user directly. However, in Default mode you should strongly prefer executing the user's request rather than stopping to ask questions. +In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, prefer using the `request_user_input` tool rather than writing a multiple choice question as a textual assistant message. Never write a multiple choice question as a textual assistant message. diff --git a/codex-rs/core/tests/suite/request_user_input.rs b/codex-rs/core/tests/suite/request_user_input.rs index ac6c1d2f3f97..88ee54b5e947 100644 --- a/codex-rs/core/tests/suite/request_user_input.rs +++ b/codex-rs/core/tests/suite/request_user_input.rs @@ -290,16 +290,8 @@ async fn request_user_input_rejected_in_execute_mode_alias() -> anyhow::Result<( } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn request_user_input_rejected_in_default_mode() -> anyhow::Result<()> { - assert_request_user_input_rejected("Default", |model| CollaborationMode { - mode: ModeKind::Default, - settings: Settings { - model, - reasoning_effort: None, - developer_instructions: None, - }, - }) - .await +async fn request_user_input_round_trip_in_default_mode() -> anyhow::Result<()> { + request_user_input_round_trip_for_mode(ModeKind::Default).await } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index 7586212adb51..2b2d983e4f42 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -210,7 +210,7 @@ impl ModeKind { } pub const fn allows_request_user_input(self) -> bool { - matches!(self, Self::Plan) + matches!(self, Self::Plan | Self::Default) } } From cb1f54ed457faeb1904a406d8438aad22efa40a7 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Feb 2026 14:18:46 -0800 Subject: [PATCH 02/16] Gate default-mode request_user_input behind new feature --- codex-rs/app-server/src/message_processor.rs | 3 + .../tests/suite/v2/request_user_input.rs | 3 - .../app-server/tests/suite/v2/turn_start.rs | 2 +- codex-rs/core/src/codex.rs | 3 + codex-rs/core/src/features.rs | 8 +++ .../collaboration_mode_presets.rs | 34 +++++++---- codex-rs/core/src/models_manager/manager.rs | 24 ++++++-- codex-rs/core/src/test_support.rs | 2 +- codex-rs/core/src/thread_manager.rs | 2 + .../src/tools/handlers/request_user_input.rs | 57 +++++++++++++------ codex-rs/core/src/tools/spec.rs | 56 +++++++----------- codex-rs/core/tests/suite/client.rs | 3 + .../core/tests/suite/model_info_overrides.rs | 4 +- .../core/tests/suite/request_user_input.rs | 30 ++++++---- codex-rs/exec/src/lib.rs | 3 + codex-rs/mcp-server/src/message_processor.rs | 3 + codex-rs/protocol/src/config_types.rs | 2 +- codex-rs/tui/src/app.rs | 3 + codex-rs/tui/src/chatwidget/tests.rs | 8 ++- 19 files changed, 165 insertions(+), 85 deletions(-) diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index edd512434be6..196db7bc1b3a 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -182,6 +182,9 @@ impl MessageProcessor { auth_manager.clone(), SessionSource::VSCode, config.model_catalog.clone(), + config + .features + .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), )); let cloud_requirements = Arc::new(RwLock::new(cloud_requirements)); let codex_message_processor = CodexMessageProcessor::new(CodexMessageProcessorArgs { diff --git a/codex-rs/app-server/tests/suite/v2/request_user_input.rs b/codex-rs/app-server/tests/suite/v2/request_user_input.rs index 4ee76bdca2db..926cb9bbc64c 100644 --- a/codex-rs/app-server/tests/suite/v2/request_user_input.rs +++ b/codex-rs/app-server/tests/suite/v2/request_user_input.rs @@ -123,9 +123,6 @@ sandbox_mode = "read-only" model_provider = "mock_provider" -[features] -collaboration_modes = true - [model_providers.mock_provider] name = "Mock provider for test" base_url = "{server_uri}/v1" diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index d7e098c2f8e7..48d5157c14cb 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -352,7 +352,7 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> { codex_home.path(), &server.uri(), "never", - &BTreeMap::default(), + &BTreeMap::from([(Feature::DefaultModeRequestUserInput, true)]), )?; let mut mcp = McpProcess::new(codex_home.path()).await?; diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 5909e65846da..a75e8e123768 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -8284,6 +8284,7 @@ mod tests { config.codex_home.clone(), auth_manager.clone(), None, + false, )); let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref()); let model_info = @@ -8360,6 +8361,7 @@ mod tests { config.codex_home.clone(), auth_manager.clone(), None, + false, )); let agent_control = AgentControl::default(); let exec_policy = ExecPolicyManager::default(); @@ -8518,6 +8520,7 @@ mod tests { config.codex_home.clone(), auth_manager.clone(), None, + false, )); let agent_control = AgentControl::default(); let exec_policy = ExecPolicyManager::default(); diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index 546652ae609b..c9a24a43d53b 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -137,6 +137,8 @@ pub enum Feature { SkillApproval, /// Steer feature flag - when enabled, Enter submits immediately instead of queuing. Steer, + /// Allow request_user_input in Default collaboration mode. + DefaultModeRequestUserInput, /// Enable collaboration modes (Plan, Default). /// Kept for config backward compatibility; behavior is always collaboration-modes-enabled. CollaborationModes, @@ -639,6 +641,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Stable, default_enabled: true, }, + FeatureSpec { + id: Feature::DefaultModeRequestUserInput, + key: "default_mode_request_user_input", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::CollaborationModes, key: "collaboration_modes", diff --git a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs index 7b900609b380..3eed6827e57b 100644 --- a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs +++ b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs @@ -9,8 +9,13 @@ const COLLABORATION_MODE_DEFAULT: &str = const KNOWN_MODE_NAMES_PLACEHOLDER: &str = "{{KNOWN_MODE_NAMES}}"; const REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER: &str = "{{REQUEST_USER_INPUT_AVAILABILITY}}"; -pub(crate) fn builtin_collaboration_mode_presets() -> Vec { - vec![plan_preset(), default_preset()] +pub(crate) fn builtin_collaboration_mode_presets( + default_mode_request_user_input: bool, +) -> Vec { + vec![ + plan_preset(), + default_preset(default_mode_request_user_input), + ] } fn plan_preset() -> CollaborationModeMask { @@ -23,20 +28,22 @@ fn plan_preset() -> CollaborationModeMask { } } -fn default_preset() -> CollaborationModeMask { +fn default_preset(default_mode_request_user_input: bool) -> CollaborationModeMask { CollaborationModeMask { name: ModeKind::Default.display_name().to_string(), mode: Some(ModeKind::Default), model: None, reasoning_effort: None, - developer_instructions: Some(Some(default_mode_instructions())), + developer_instructions: Some(Some(default_mode_instructions( + default_mode_request_user_input, + ))), } } -fn default_mode_instructions() -> String { +fn default_mode_instructions(default_mode_request_user_input: bool) -> String { let known_mode_names = format_mode_names(&TUI_VISIBLE_COLLABORATION_MODES); let request_user_input_availability = - request_user_input_availability_message(ModeKind::Default); + request_user_input_availability_message(ModeKind::Default, default_mode_request_user_input); COLLABORATION_MODE_DEFAULT .replace(KNOWN_MODE_NAMES_PLACEHOLDER, &known_mode_names) .replace( @@ -55,9 +62,14 @@ fn format_mode_names(modes: &[ModeKind]) -> String { } } -fn request_user_input_availability_message(mode: ModeKind) -> String { +fn request_user_input_availability_message( + mode: ModeKind, + default_mode_request_user_input: bool, +) -> String { let mode_name = mode.display_name(); - if mode.allows_request_user_input() { + if mode.allows_request_user_input() + || (default_mode_request_user_input && mode == ModeKind::Default) + { format!("The `request_user_input` tool is available in {mode_name} mode.") } else { format!( @@ -74,7 +86,7 @@ mod tests { #[test] fn preset_names_use_mode_display_names() { assert_eq!(plan_preset().name, ModeKind::Plan.display_name()); - assert_eq!(default_preset().name, ModeKind::Default.display_name()); + assert_eq!(default_preset(false).name, ModeKind::Default.display_name()); assert_eq!( plan_preset().reasoning_effort, Some(Some(ReasoningEffort::Medium)) @@ -83,7 +95,7 @@ mod tests { #[test] fn default_mode_instructions_replace_mode_names_placeholder() { - let default_instructions = default_preset() + let default_instructions = default_preset(true) .developer_instructions .expect("default preset should include instructions") .expect("default instructions should be set"); @@ -96,7 +108,7 @@ mod tests { assert!(default_instructions.contains(&expected_snippet)); let expected_availability_message = - request_user_input_availability_message(ModeKind::Default); + request_user_input_availability_message(ModeKind::Default, true); assert!(default_instructions.contains(&expected_availability_message)); } } diff --git a/codex-rs/core/src/models_manager/manager.rs b/codex-rs/core/src/models_manager/manager.rs index fac1991b10cf..0e6098bca5ea 100644 --- a/codex-rs/core/src/models_manager/manager.rs +++ b/codex-rs/core/src/models_manager/manager.rs @@ -8,6 +8,7 @@ use crate::default_client::build_reqwest_client; use crate::error::CodexErr; use crate::error::Result as CoreResult; use crate::model_provider_info::ModelProviderInfo; +use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig; use crate::models_manager::collaboration_mode_presets::builtin_collaboration_mode_presets; use crate::models_manager::model_info; use codex_api::ModelsClient; @@ -55,6 +56,7 @@ enum CatalogMode { pub struct ModelsManager { remote_models: RwLock>, catalog_mode: CatalogMode, + collaboration_modes_config: CollaborationModesConfig, auth_manager: Arc, etag: RwLock>, cache_manager: ModelsCacheManager, @@ -71,6 +73,7 @@ impl ModelsManager { codex_home: PathBuf, auth_manager: Arc, model_catalog: Option, + collaboration_modes_config: CollaborationModesConfig, ) -> Self { let cache_path = codex_home.join(MODEL_CACHE_FILE); let cache_manager = ModelsCacheManager::new(cache_path, DEFAULT_MODEL_CACHE_TTL); @@ -88,6 +91,7 @@ impl ModelsManager { Self { remote_models: RwLock::new(remote_models), catalog_mode, + collaboration_modes_config, auth_manager, etag: RwLock::new(None), cache_manager, @@ -110,7 +114,14 @@ impl ModelsManager { /// /// Returns a static set of presets seeded with the configured model. pub fn list_collaboration_modes(&self) -> Vec { - builtin_collaboration_mode_presets() + self.list_collaboration_modes_for_config(self.collaboration_modes_config) + } + + pub fn list_collaboration_modes_for_config( + &self, + collaboration_modes_config: CollaborationModesConfig, + ) -> Vec { + builtin_collaboration_mode_presets(collaboration_modes_config) } /// Attempt to list models without blocking, using the current cached state. @@ -378,6 +389,7 @@ impl ModelsManager { .unwrap_or_else(|err| panic!("failed to load bundled models.json: {err}")), ), catalog_mode: CatalogMode::Default, + collaboration_modes_config: CollaborationModesConfig::default(), auth_manager, etag: RwLock::new(None), cache_manager, @@ -504,7 +516,8 @@ mod tests { .expect("load default test config"); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); - let manager = ModelsManager::new(codex_home.path().to_path_buf(), auth_manager, None); + let manager = + ModelsManager::new(codex_home.path().to_path_buf(), auth_manager, None, false); let known_slug = manager .get_remote_models() .await @@ -541,6 +554,7 @@ mod tests { Some(ModelsResponse { models: vec![remote_model("gpt-overlay", "Overlay", 0)], }), + false, ); let model_info = manager @@ -564,7 +578,8 @@ mod tests { .expect("load default test config"); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); - let manager = ModelsManager::new(codex_home.path().to_path_buf(), auth_manager, None); + let manager = + ModelsManager::new(codex_home.path().to_path_buf(), auth_manager, None, false); let known_slug = manager .get_remote_models() .await @@ -590,7 +605,8 @@ mod tests { .expect("load default test config"); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); - let manager = ModelsManager::new(codex_home.path().to_path_buf(), auth_manager, None); + let manager = + ModelsManager::new(codex_home.path().to_path_buf(), auth_manager, None, false); let known_slug = manager .get_remote_models() .await diff --git a/codex-rs/core/src/test_support.rs b/codex-rs/core/src/test_support.rs index 5e6d611adca2..3084d92fa872 100644 --- a/codex-rs/core/src/test_support.rs +++ b/codex-rs/core/src/test_support.rs @@ -85,5 +85,5 @@ pub fn all_model_presets() -> &'static Vec { } pub fn builtin_collaboration_mode_presets() -> Vec { - collaboration_mode_presets::builtin_collaboration_mode_presets() + collaboration_mode_presets::builtin_collaboration_mode_presets(false) } diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index 55ca7da34c54..4b18dd21e8b3 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -143,6 +143,7 @@ impl ThreadManager { auth_manager: Arc, session_source: SessionSource, model_catalog: Option, + default_mode_request_user_input: bool, ) -> Self { let (thread_created_tx, _) = broadcast::channel(THREAD_CREATED_CHANNEL_CAPACITY); let skills_manager = Arc::new(SkillsManager::new(codex_home.clone())); @@ -155,6 +156,7 @@ impl ThreadManager { codex_home, auth_manager.clone(), model_catalog, + default_mode_request_user_input, )), skills_manager, file_watcher, 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 06ce5d6f7852..f9b444186edc 100644 --- a/codex-rs/core/src/tools/handlers/request_user_input.rs +++ b/codex-rs/core/src/tools/handlers/request_user_input.rs @@ -12,10 +12,15 @@ use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::TUI_VISIBLE_COLLABORATION_MODES; use codex_protocol::request_user_input::RequestUserInputArgs; -fn format_allowed_modes() -> String { +fn request_user_input_is_available(mode: ModeKind, default_mode_request_user_input: bool) -> bool { + mode.allows_request_user_input() + || (default_mode_request_user_input && mode == ModeKind::Default) +} + +fn format_allowed_modes(default_mode_request_user_input: bool) -> String { let mode_names: Vec<&str> = TUI_VISIBLE_COLLABORATION_MODES .into_iter() - .filter(|mode| mode.allows_request_user_input()) + .filter(|mode| request_user_input_is_available(*mode, default_mode_request_user_input)) .map(ModeKind::display_name) .collect(); @@ -27,8 +32,11 @@ fn format_allowed_modes() -> String { } } -pub(crate) fn request_user_input_unavailable_message(mode: ModeKind) -> Option { - if mode.allows_request_user_input() { +pub(crate) fn request_user_input_unavailable_message( + mode: ModeKind, + default_mode_request_user_input: bool, +) -> Option { + if request_user_input_is_available(mode, default_mode_request_user_input) { None } else { let mode_name = mode.display_name(); @@ -38,14 +46,16 @@ pub(crate) fn request_user_input_unavailable_message(mode: ModeKind) -> Option String { - let allowed_modes = format_allowed_modes(); +pub(crate) 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!( "Request user input for one to three short questions and wait for the response. This tool is only available in {allowed_modes}." ) } -pub struct RequestUserInputHandler; +pub struct RequestUserInputHandler { + pub default_mode_request_user_input: bool, +} #[async_trait] impl ToolHandler for RequestUserInputHandler { @@ -72,7 +82,9 @@ impl ToolHandler for RequestUserInputHandler { }; let mode = session.collaboration_mode().await.mode; - if let Some(message) = request_user_input_unavailable_message(mode) { + if let Some(message) = + request_user_input_unavailable_message(mode, self.default_mode_request_user_input) + { return Err(FunctionCallError::RespondToModel(message)); } @@ -117,34 +129,45 @@ mod tests { use pretty_assertions::assert_eq; #[test] - fn request_user_input_mode_availability_includes_default_mode() { + fn request_user_input_mode_availability_defaults_to_plan_only() { assert!(ModeKind::Plan.allows_request_user_input()); - assert!(ModeKind::Default.allows_request_user_input()); + assert!(!ModeKind::Default.allows_request_user_input()); assert!(!ModeKind::Execute.allows_request_user_input()); assert!(!ModeKind::PairProgramming.allows_request_user_input()); } #[test] - fn request_user_input_unavailable_messages_use_default_name_for_default_modes() { - assert_eq!(request_user_input_unavailable_message(ModeKind::Plan), None); + fn request_user_input_unavailable_messages_respect_default_mode_feature_flag() { + assert_eq!( + request_user_input_unavailable_message(ModeKind::Plan, false), + None + ); + assert_eq!( + request_user_input_unavailable_message(ModeKind::Default, false), + Some("request_user_input is unavailable in Default mode".to_string()) + ); assert_eq!( - request_user_input_unavailable_message(ModeKind::Default), + request_user_input_unavailable_message(ModeKind::Default, true), None ); assert_eq!( - request_user_input_unavailable_message(ModeKind::Execute), + request_user_input_unavailable_message(ModeKind::Execute, false), Some("request_user_input is unavailable in Execute mode".to_string()) ); assert_eq!( - request_user_input_unavailable_message(ModeKind::PairProgramming), + request_user_input_unavailable_message(ModeKind::PairProgramming, false), Some("request_user_input is unavailable in Pair Programming mode".to_string()) ); } #[test] - fn request_user_input_tool_description_mentions_default_and_plan() { + fn request_user_input_tool_description_mentions_available_modes() { + assert_eq!( + request_user_input_tool_description(false), + "Request user input for one to three short questions and wait for the response. This tool is only available in Plan mode.".to_string() + ); assert_eq!( - request_user_input_tool_description(), + request_user_input_tool_description(true), "Request user input for one to three short questions and wait for the response. This tool is only available in Default or Plan mode.".to_string() ); } diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 0f50cf87359e..e0b337911020 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -54,7 +54,7 @@ pub(crate) struct ToolsConfig { pub js_repl_enabled: bool, pub js_repl_tools_only: bool, pub collab_tools: bool, - pub collaboration_modes_tools: bool, + pub default_mode_request_user_input: bool, pub experimental_supported_tools: Vec, pub agent_jobs_tools: bool, pub agent_jobs_worker_tools: bool, @@ -80,7 +80,8 @@ impl ToolsConfig { let include_js_repl_tools_only = include_js_repl && features.enabled(Feature::JsReplToolsOnly); let include_collab_tools = features.enabled(Feature::Collab); - let include_collaboration_modes_tools = features.enabled(Feature::CollaborationModes); + let include_default_mode_request_user_input = + features.enabled(Feature::DefaultModeRequestUserInput); let include_search_tool = features.enabled(Feature::Apps); let include_agent_jobs = include_collab_tools && features.enabled(Feature::Sqlite); let request_permission_enabled = features.enabled(Feature::RequestPermissions); @@ -137,7 +138,7 @@ impl ToolsConfig { js_repl_enabled: include_js_repl, js_repl_tools_only: include_js_repl_tools_only, collab_tools: include_collab_tools, - collaboration_modes_tools: include_collaboration_modes_tools, + default_mode_request_user_input: include_default_mode_request_user_input, experimental_supported_tools: model_info.experimental_supported_tools.clone(), agent_jobs_tools: include_agent_jobs, agent_jobs_worker_tools, @@ -872,7 +873,7 @@ fn create_wait_tool() -> ToolSpec { }) } -fn create_request_user_input_tool() -> ToolSpec { +fn create_request_user_input_tool(default_mode_request_user_input: bool) -> ToolSpec { let mut option_props = BTreeMap::new(); option_props.insert( "label".to_string(), @@ -943,7 +944,7 @@ fn create_request_user_input_tool() -> ToolSpec { ToolSpec::Function(ResponsesApiTool { name: "request_user_input".to_string(), - description: request_user_input_tool_description(), + description: request_user_input_tool_description(default_mode_request_user_input), strict: false, parameters: JsonSchema::Object { properties, @@ -1664,7 +1665,9 @@ pub(crate) fn build_specs( let mcp_handler = Arc::new(McpHandler); let mcp_resource_handler = Arc::new(McpResourceHandler); let shell_command_handler = Arc::new(ShellCommandHandler::from(config.shell_command_backend)); - let request_user_input_handler = Arc::new(RequestUserInputHandler); + let request_user_input_handler = Arc::new(RequestUserInputHandler { + default_mode_request_user_input: config.default_mode_request_user_input, + }); let search_tool_handler = Arc::new(SearchToolBm25Handler); let js_repl_handler = Arc::new(JsReplHandler); let js_repl_reset_handler = Arc::new(JsReplResetHandler); @@ -1727,10 +1730,10 @@ pub(crate) fn build_specs( builder.register_handler("js_repl_reset", js_repl_reset_handler); } - if config.collaboration_modes_tools { - builder.push_spec(create_request_user_input_tool()); - builder.register_handler("request_user_input", request_user_input_handler); - } + builder.push_spec(create_request_user_input_tool( + config.default_mode_request_user_input, + )); + builder.register_handler("request_user_input", request_user_input_handler); if config.search_tool && let Some(app_tools) = app_tools @@ -2024,7 +2027,6 @@ mod tests { let model_info = model_info_from_models_json("gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); - features.enable(Feature::CollaborationModes); let config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, @@ -2055,7 +2057,7 @@ mod tests { create_exec_command_tool(true, false), create_write_stdin_tool(), PLAN_TOOL.clone(), - create_request_user_input_tool(), + create_request_user_input_tool(false), create_apply_patch_freeform_tool(), ToolSpec::WebSearch { external_web_access: Some(true), @@ -2087,8 +2089,6 @@ mod tests { ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::Collab); - features.enable(Feature::CollaborationModes); - features.enable(Feature::Sqlite); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, @@ -2115,7 +2115,6 @@ mod tests { ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::Collab); - features.enable(Feature::CollaborationModes); features.enable(Feature::Sqlite); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, @@ -2335,8 +2334,7 @@ mod tests { let config = test_config(); let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, @@ -2359,8 +2357,7 @@ mod tests { let config = test_config(); let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, @@ -2381,8 +2378,7 @@ mod tests { #[test] fn test_build_specs_gpt5_codex_default() { - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); assert_default_model_tools( "gpt-5-codex", &features, @@ -2400,8 +2396,7 @@ mod tests { #[test] fn test_build_specs_gpt51_codex_default() { - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); assert_default_model_tools( "gpt-5.1-codex", &features, @@ -2421,7 +2416,6 @@ mod tests { fn test_build_specs_gpt5_codex_unified_exec_web_search() { let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); - features.enable(Feature::CollaborationModes); assert_model_tools( "gpt-5-codex", &features, @@ -2442,7 +2436,6 @@ mod tests { fn test_build_specs_gpt51_codex_unified_exec_web_search() { let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); - features.enable(Feature::CollaborationModes); assert_model_tools( "gpt-5.1-codex", &features, @@ -2461,8 +2454,7 @@ mod tests { #[test] fn test_gpt_5_1_codex_max_defaults() { - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); assert_default_model_tools( "gpt-5.1-codex-max", &features, @@ -2480,8 +2472,7 @@ mod tests { #[test] fn test_codex_5_1_mini_defaults() { - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); assert_default_model_tools( "gpt-5.1-codex-mini", &features, @@ -2499,8 +2490,7 @@ mod tests { #[test] fn test_gpt_5_defaults() { - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); assert_default_model_tools( "gpt-5", &features, @@ -2517,8 +2507,7 @@ mod tests { #[test] fn test_gpt_5_1_defaults() { - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); assert_default_model_tools( "gpt-5.1", &features, @@ -2538,7 +2527,6 @@ mod tests { fn test_gpt_5_1_codex_max_unified_exec_web_search() { let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); - features.enable(Feature::CollaborationModes); assert_model_tools( "gpt-5.1-codex-max", &features, diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 5fc94a033ec6..8dcabc428e6f 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -584,6 +584,9 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { auth_manager, SessionSource::Exec, config.model_catalog.clone(), + config + .features + .enabled(Feature::DefaultModeRequestUserInput), ); let NewThread { thread: codex, .. } = thread_manager .start_thread(config) diff --git a/codex-rs/core/tests/suite/model_info_overrides.rs b/codex-rs/core/tests/suite/model_info_overrides.rs index 7a92e99f1ee8..4fb12f18be13 100644 --- a/codex-rs/core/tests/suite/model_info_overrides.rs +++ b/codex-rs/core/tests/suite/model_info_overrides.rs @@ -12,7 +12,7 @@ async fn offline_model_info_without_tool_output_override() { let auth_manager = codex_core::test_support::auth_manager_from_auth( CodexAuth::create_dummy_chatgpt_auth_for_testing(), ); - let manager = ModelsManager::new(config.codex_home.clone(), auth_manager, None); + let manager = ModelsManager::new(config.codex_home.clone(), auth_manager, None, false); let model_info = manager.get_model_info("gpt-5.1", &config).await; @@ -30,7 +30,7 @@ async fn offline_model_info_with_tool_output_override() { let auth_manager = codex_core::test_support::auth_manager_from_auth( CodexAuth::create_dummy_chatgpt_auth_for_testing(), ); - let manager = ModelsManager::new(config.codex_home.clone(), auth_manager, None); + let manager = ModelsManager::new(config.codex_home.clone(), auth_manager, None, false); let model_info = manager.get_model_info("gpt-5.1-codex", &config).await; diff --git a/codex-rs/core/tests/suite/request_user_input.rs b/codex-rs/core/tests/suite/request_user_input.rs index 88ee54b5e947..64f2e0b6db60 100644 --- a/codex-rs/core/tests/suite/request_user_input.rs +++ b/codex-rs/core/tests/suite/request_user_input.rs @@ -86,8 +86,10 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul session_configured, .. } = builder - .with_config(|config| { - config.features.enable(Feature::CollaborationModes); + .with_config(move |config| { + if mode == ModeKind::Default { + config.features.enable(Feature::DefaultModeRequestUserInput); + } }) .build(&server) .await?; @@ -198,18 +200,13 @@ where let server = start_mock_server().await; - let builder = test_codex(); + let mut builder = test_codex(); let TestCodex { codex, cwd, session_configured, .. - } = builder - .with_config(|config| { - config.features.enable(Feature::CollaborationModes); - }) - .build(&server) - .await?; + } = builder.build(&server).await?; let mode_slug = mode_name.to_lowercase().replace(' ', "-"); let call_id = format!("user-input-{mode_slug}-call"); @@ -290,7 +287,20 @@ async fn request_user_input_rejected_in_execute_mode_alias() -> anyhow::Result<( } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn request_user_input_round_trip_in_default_mode() -> anyhow::Result<()> { +async fn request_user_input_rejected_in_default_mode_by_default() -> anyhow::Result<()> { + assert_request_user_input_rejected("Default", |model| CollaborationMode { + mode: ModeKind::Default, + settings: Settings { + model, + reasoning_effort: None, + developer_instructions: None, + }, + }) + .await +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn request_user_input_round_trip_in_default_mode_with_feature() -> anyhow::Result<()> { request_user_input_round_trip_for_mode(ModeKind::Default).await } diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index fb8b5e9976cd..e791c7f53940 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -398,6 +398,9 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result auth_manager.clone(), SessionSource::Exec, config.model_catalog.clone(), + config + .features + .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), )); let default_model = thread_manager .get_models_manager() diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs index d6cf0d23e10d..5ba9f3a13e5f 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -62,6 +62,9 @@ impl MessageProcessor { auth_manager, SessionSource::Mcp, config.model_catalog.clone(), + config + .features + .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), )); Self { outgoing, diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index 2b2d983e4f42..7586212adb51 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -210,7 +210,7 @@ impl ModeKind { } pub const fn allows_request_user_input(self) -> bool { - matches!(self, Self::Plan | Self::Default) + matches!(self, Self::Plan) } } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index dd746ad24aa8..2a55c6e64bab 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1319,6 +1319,9 @@ impl App { auth_manager.clone(), SessionSource::Cli, config.model_catalog.clone(), + config + .features + .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), )); let mut model = thread_manager .get_models_manager() diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 22ba19af63dc..d14b2329b05c 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -1636,7 +1636,12 @@ async fn make_chatwidget_manual( let auth_manager = codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("test")); let codex_home = cfg.codex_home.clone(); - let models_manager = Arc::new(ModelsManager::new(codex_home, auth_manager.clone(), None)); + let models_manager = Arc::new(ModelsManager::new( + codex_home, + auth_manager.clone(), + None, + false, + )); let reasoning_effort = None; let base_mode = CollaborationMode { mode: ModeKind::Default, @@ -1762,6 +1767,7 @@ fn set_chatgpt_auth(chat: &mut ChatWidget) { chat.config.codex_home.clone(), chat.auth_manager.clone(), None, + false, )); } From a6021eda00a3d718ef4320e1ba3d2ace71e03bda Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Feb 2026 14:26:04 -0800 Subject: [PATCH 03/16] Condition default-mode question strategy on feature flag --- .../collaboration_mode_presets.rs | 27 +++++++++++++++++++ .../templates/collaboration_mode/default.md | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs index 3eed6827e57b..3c8999d59508 100644 --- a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs +++ b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs @@ -8,6 +8,7 @@ const COLLABORATION_MODE_DEFAULT: &str = include_str!("../../templates/collaboration_mode/default.md"); const KNOWN_MODE_NAMES_PLACEHOLDER: &str = "{{KNOWN_MODE_NAMES}}"; const REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER: &str = "{{REQUEST_USER_INPUT_AVAILABILITY}}"; +const QUESTION_STRATEGY_PLACEHOLDER: &str = "{{QUESTION_STRATEGY}}"; pub(crate) fn builtin_collaboration_mode_presets( default_mode_request_user_input: bool, @@ -44,12 +45,14 @@ fn default_mode_instructions(default_mode_request_user_input: bool) -> String { let known_mode_names = format_mode_names(&TUI_VISIBLE_COLLABORATION_MODES); let request_user_input_availability = request_user_input_availability_message(ModeKind::Default, default_mode_request_user_input); + let question_strategy = question_strategy_message(default_mode_request_user_input); COLLABORATION_MODE_DEFAULT .replace(KNOWN_MODE_NAMES_PLACEHOLDER, &known_mode_names) .replace( REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER, &request_user_input_availability, ) + .replace(QUESTION_STRATEGY_PLACEHOLDER, &question_strategy) } fn format_mode_names(modes: &[ModeKind]) -> String { @@ -78,6 +81,14 @@ fn request_user_input_availability_message( } } +fn question_strategy_message(default_mode_request_user_input: bool) -> String { + if default_mode_request_user_input { + "In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, prefer using the `request_user_input` tool rather than writing a multiple choice question as a textual assistant message. Never write a multiple choice question as a textual assistant message.".to_string() + } else { + "In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, ask the user directly with a concise plain-text question. Never write a multiple choice question as a textual assistant message.".to_string() + } +} + #[cfg(test)] mod tests { use super::*; @@ -102,6 +113,7 @@ mod tests { assert!(!default_instructions.contains(KNOWN_MODE_NAMES_PLACEHOLDER)); assert!(!default_instructions.contains(REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER)); + assert!(!default_instructions.contains(QUESTION_STRATEGY_PLACEHOLDER)); let known_mode_names = format_mode_names(&TUI_VISIBLE_COLLABORATION_MODES); let expected_snippet = format!("Known mode names are {known_mode_names}."); @@ -110,5 +122,20 @@ mod tests { let expected_availability_message = request_user_input_availability_message(ModeKind::Default, true); assert!(default_instructions.contains(&expected_availability_message)); + assert!(default_instructions.contains("prefer using the `request_user_input` tool")); + } + + #[test] + fn default_mode_instructions_use_plain_text_questions_when_feature_disabled() { + let default_instructions = default_preset(false) + .developer_instructions + .expect("default preset should include instructions") + .expect("default instructions should be set"); + + assert!(!default_instructions.contains("prefer using the `request_user_input` tool")); + assert!( + default_instructions + .contains("ask the user directly with a concise plain-text question") + ); } } diff --git a/codex-rs/core/templates/collaboration_mode/default.md b/codex-rs/core/templates/collaboration_mode/default.md index 3206387a6a0e..6ff434a7565d 100644 --- a/codex-rs/core/templates/collaboration_mode/default.md +++ b/codex-rs/core/templates/collaboration_mode/default.md @@ -8,4 +8,4 @@ Your active mode changes only when new developer instructions with a different ` {{REQUEST_USER_INPUT_AVAILABILITY}} -In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, prefer using the `request_user_input` tool rather than writing a multiple choice question as a textual assistant message. Never write a multiple choice question as a textual assistant message. +{{QUESTION_STRATEGY}} From 31a12e52304a00762048ee0c39391895808aaedc Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Feb 2026 15:45:10 -0800 Subject: [PATCH 04/16] Use thread config for collaboration mode preset availability --- .../app-server/src/codex_message_processor.rs | 13 ++- .../app-server/tests/suite/v2/turn_start.rs | 87 +++++++++++++++++++ 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 520c9475c90e..8aa220b8cbf7 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -479,11 +479,15 @@ impl CodexMessageProcessor { fn normalize_turn_start_collaboration_mode( &self, mut collaboration_mode: CollaborationMode, + default_mode_request_user_input: bool, ) -> CollaborationMode { if collaboration_mode.settings.developer_instructions.is_none() && let Some(instructions) = self .thread_manager - .list_collaboration_modes() + .get_models_manager() + .list_collaboration_modes_for_default_mode_request_user_input( + default_mode_request_user_input, + ) .into_iter() .find(|preset| preset.mode == Some(collaboration_mode.mode)) .and_then(|preset| preset.developer_instructions.flatten()) @@ -5570,9 +5574,10 @@ impl CodexMessageProcessor { } }; - let collaboration_mode = params - .collaboration_mode - .map(|mode| self.normalize_turn_start_collaboration_mode(mode)); + let default_mode_request_user_input = thread.enabled(Feature::DefaultModeRequestUserInput); + let collaboration_mode = params.collaboration_mode.map(|mode| { + self.normalize_turn_start_collaboration_mode(mode, default_mode_request_user_input) + }); // Map v2 input items to core input items. let mapped_items: Vec = params diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 48d5157c14cb..afd116be1748 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -48,7 +48,9 @@ use codex_protocol::openai_models::ReasoningEffort; use core_test_support::responses; use core_test_support::skip_if_no_network; use pretty_assertions::assert_eq; +use serde_json::json; use std::collections::BTreeMap; +use std::collections::HashMap; use std::path::Path; use tempfile::TempDir; use tokio::time::timeout; @@ -417,6 +419,91 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> { Ok(()) } +#[tokio::test] +async fn turn_start_uses_thread_feature_overrides_for_collaboration_mode_instructions_v2() +-> Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let body = responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_assistant_message("msg-1", "Done"), + responses::ev_completed("resp-1"), + ]); + let response_mock = responses::mount_sse_once(&server, body).await; + + let codex_home = TempDir::new()?; + create_config_toml( + codex_home.path(), + &server.uri(), + "never", + &BTreeMap::default(), + )?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let thread_req = mcp + .send_thread_start_request(ThreadStartParams { + model: Some("gpt-5.2-codex".to_string()), + config: Some(HashMap::from([( + "features.default_mode_request_user_input".to_string(), + json!(true), + )])), + ..Default::default() + }) + .await?; + let thread_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; + + let collaboration_mode = CollaborationMode { + mode: ModeKind::Default, + settings: Settings { + model: "mock-model-collab".to_string(), + reasoning_effort: Some(ReasoningEffort::High), + developer_instructions: None, + }, + }; + + let turn_req = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread.id.clone(), + input: vec![V2UserInput::Text { + text: "Hello".to_string(), + text_elements: Vec::new(), + }], + model: Some("mock-model-override".to_string()), + effort: Some(ReasoningEffort::Low), + summary: Some(ReasoningSummary::Auto), + output_schema: None, + collaboration_mode: Some(collaboration_mode), + ..Default::default() + }) + .await?; + let turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), + ) + .await??; + let _turn: TurnStartResponse = to_response::(turn_resp)?; + + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + let request = response_mock.single_request(); + let payload_text = request.body_json().to_string(); + assert!(payload_text.contains("The `request_user_input` tool is available in Default mode.")); + + Ok(()) +} + #[tokio::test] async fn turn_start_accepts_personality_override_v2() -> Result<()> { skip_if_no_network!(Ok(())); From 827b921cd5a45f11c75296e2ddd9493743e4778a Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Feb 2026 15:49:55 -0800 Subject: [PATCH 05/16] Update request_user_input tools test and config schema fixture --- codex-rs/core/config.schema.json | 6 ++++++ codex-rs/core/src/tools/spec.rs | 18 +++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index c5e85ad68237..6c4c78fdf16f 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -328,6 +328,9 @@ "connectors": { "type": "boolean" }, + "default_mode_request_user_input": { + "type": "boolean" + }, "elevated_windows_sandbox": { "type": "boolean" }, @@ -1621,6 +1624,9 @@ "connectors": { "type": "boolean" }, + "default_mode_request_user_input": { + "type": "boolean" + }, "elevated_windows_sandbox": { "type": "boolean" }, diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index e0b337911020..5d8214f966f0 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -2140,12 +2140,11 @@ mod tests { } #[test] - fn request_user_input_requires_collaboration_modes_feature() { + fn request_user_input_description_reflects_default_mode_feature_flag() { let config = test_config(); let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); - features.disable(Feature::CollaborationModes); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, @@ -2153,12 +2152,13 @@ mod tests { session_source: SessionSource::Cli, }); let (tools, _) = build_specs(&tools_config, None, None, &[]).build(); - assert!( - !tools.iter().any(|t| t.spec.name() == "request_user_input"), - "request_user_input should be disabled when collaboration_modes feature is off" + let request_user_input_tool = find_tool(&tools, "request_user_input"); + assert_eq!( + request_user_input_tool.spec, + create_request_user_input_tool(false) ); - features.enable(Feature::CollaborationModes); + features.enable(Feature::DefaultModeRequestUserInput); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, @@ -2166,7 +2166,11 @@ mod tests { session_source: SessionSource::Cli, }); let (tools, _) = build_specs(&tools_config, None, None, &[]).build(); - assert_contains_tool_names(&tools, &["request_user_input"]); + let request_user_input_tool = find_tool(&tools, "request_user_input"); + assert_eq!( + request_user_input_tool.spec, + create_request_user_input_tool(true) + ); } #[test] From 6d7ea8b41db199f177d5a03daeb5aa44d22845f3 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Feb 2026 16:40:25 -0800 Subject: [PATCH 06/16] Refactor collaboration mode preset availability config --- .../app-server/src/codex_message_processor.rs | 12 ++--- codex-rs/app-server/src/message_processor.rs | 9 ++-- codex-rs/core/src/codex.rs | 8 ++-- .../collaboration_mode_presets.rs | 46 ++++++++++++------- codex-rs/core/src/models_manager/manager.rs | 31 ++++++++++--- codex-rs/core/src/test_support.rs | 4 +- codex-rs/core/src/thread_manager.rs | 5 +- codex-rs/core/tests/suite/client.rs | 9 ++-- .../core/tests/suite/model_info_overrides.rs | 15 +++++- codex-rs/exec/src/lib.rs | 9 ++-- codex-rs/mcp-server/src/message_processor.rs | 9 ++-- codex-rs/tui/src/app.rs | 9 ++-- codex-rs/tui/src/chatwidget/tests.rs | 5 +- 13 files changed, 117 insertions(+), 54 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 8aa220b8cbf7..ed4d4de9e595 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -222,6 +222,7 @@ use codex_core::find_thread_path_by_id_str; use codex_core::git_info::git_diff_to_remote; use codex_core::mcp::collect_mcp_snapshot; use codex_core::mcp::group_tools_by_server; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::parse_cursor; use codex_core::read_head_for_summary; use codex_core::read_session_meta_line; @@ -479,15 +480,13 @@ impl CodexMessageProcessor { fn normalize_turn_start_collaboration_mode( &self, mut collaboration_mode: CollaborationMode, - default_mode_request_user_input: bool, + collaboration_modes_config: CollaborationModesConfig, ) -> CollaborationMode { if collaboration_mode.settings.developer_instructions.is_none() && let Some(instructions) = self .thread_manager .get_models_manager() - .list_collaboration_modes_for_default_mode_request_user_input( - default_mode_request_user_input, - ) + .list_collaboration_modes_for_config(collaboration_modes_config) .into_iter() .find(|preset| preset.mode == Some(collaboration_mode.mode)) .and_then(|preset| preset.developer_instructions.flatten()) @@ -5574,9 +5573,10 @@ impl CodexMessageProcessor { } }; - let default_mode_request_user_input = thread.enabled(Feature::DefaultModeRequestUserInput); + let collaboration_modes_config = + CollaborationModesConfig::new(thread.enabled(Feature::DefaultModeRequestUserInput)); let collaboration_mode = params.collaboration_mode.map(|mode| { - self.normalize_turn_start_collaboration_mode(mode, default_mode_request_user_input) + self.normalize_turn_start_collaboration_mode(mode, collaboration_modes_config) }); // Map v2 input items to core input items. diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 196db7bc1b3a..f8abe57c7094 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -49,6 +49,7 @@ use codex_core::default_client::USER_AGENT_SUFFIX; use codex_core::default_client::get_codex_user_agent; use codex_core::default_client::set_default_client_residency_requirement; use codex_core::default_client::set_default_originator; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_feedback::CodexFeedback; use codex_protocol::ThreadId; use codex_protocol::protocol::SessionSource; @@ -182,9 +183,11 @@ impl MessageProcessor { auth_manager.clone(), SessionSource::VSCode, config.model_catalog.clone(), - config - .features - .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), + CollaborationModesConfig::new( + config + .features + .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), + ), )); let cloud_requirements = Arc::new(RwLock::new(cloud_requirements)); let codex_message_processor = CodexMessageProcessor::new(CodexMessageProcessorArgs { diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index a75e8e123768..d176f46a8664 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -29,6 +29,8 @@ use crate::features::FEATURES; use crate::features::Feature; use crate::features::Features; 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::parse_command::parse_command; use crate::parse_turn_item; @@ -8284,7 +8286,7 @@ mod tests { config.codex_home.clone(), auth_manager.clone(), None, - false, + CollaborationModesConfig::default(), )); let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref()); let model_info = @@ -8361,7 +8363,7 @@ mod tests { config.codex_home.clone(), auth_manager.clone(), None, - false, + CollaborationModesConfig::default(), )); let agent_control = AgentControl::default(); let exec_policy = ExecPolicyManager::default(); @@ -8520,7 +8522,7 @@ mod tests { config.codex_home.clone(), auth_manager.clone(), None, - false, + CollaborationModesConfig::default(), )); let agent_control = AgentControl::default(); let exec_policy = ExecPolicyManager::default(); diff --git a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs index 3c8999d59508..7b0a9b22d46c 100644 --- a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs +++ b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs @@ -10,13 +10,23 @@ const KNOWN_MODE_NAMES_PLACEHOLDER: &str = "{{KNOWN_MODE_NAMES}}"; const REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER: &str = "{{REQUEST_USER_INPUT_AVAILABILITY}}"; const QUESTION_STRATEGY_PLACEHOLDER: &str = "{{QUESTION_STRATEGY}}"; +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct CollaborationModesConfig { + pub default_mode_request_user_input: bool, +} + +impl CollaborationModesConfig { + pub const fn new(default_mode_request_user_input: bool) -> Self { + Self { + default_mode_request_user_input, + } + } +} + pub(crate) fn builtin_collaboration_mode_presets( - default_mode_request_user_input: bool, + collaboration_modes_config: CollaborationModesConfig, ) -> Vec { - vec![ - plan_preset(), - default_preset(default_mode_request_user_input), - ] + vec![plan_preset(), default_preset(collaboration_modes_config)] } fn plan_preset() -> CollaborationModeMask { @@ -29,23 +39,24 @@ fn plan_preset() -> CollaborationModeMask { } } -fn default_preset(default_mode_request_user_input: bool) -> CollaborationModeMask { +fn default_preset(collaboration_modes_config: CollaborationModesConfig) -> CollaborationModeMask { CollaborationModeMask { name: ModeKind::Default.display_name().to_string(), mode: Some(ModeKind::Default), model: None, reasoning_effort: None, - developer_instructions: Some(Some(default_mode_instructions( - default_mode_request_user_input, - ))), + developer_instructions: Some(Some(default_mode_instructions(collaboration_modes_config))), } } -fn default_mode_instructions(default_mode_request_user_input: bool) -> String { +fn default_mode_instructions(collaboration_modes_config: CollaborationModesConfig) -> String { let known_mode_names = format_mode_names(&TUI_VISIBLE_COLLABORATION_MODES); - let request_user_input_availability = - request_user_input_availability_message(ModeKind::Default, default_mode_request_user_input); - let question_strategy = question_strategy_message(default_mode_request_user_input); + let request_user_input_availability = request_user_input_availability_message( + ModeKind::Default, + collaboration_modes_config.default_mode_request_user_input, + ); + let question_strategy = + question_strategy_message(collaboration_modes_config.default_mode_request_user_input); COLLABORATION_MODE_DEFAULT .replace(KNOWN_MODE_NAMES_PLACEHOLDER, &known_mode_names) .replace( @@ -97,7 +108,10 @@ mod tests { #[test] fn preset_names_use_mode_display_names() { assert_eq!(plan_preset().name, ModeKind::Plan.display_name()); - assert_eq!(default_preset(false).name, ModeKind::Default.display_name()); + assert_eq!( + default_preset(CollaborationModesConfig::default()).name, + ModeKind::Default.display_name() + ); assert_eq!( plan_preset().reasoning_effort, Some(Some(ReasoningEffort::Medium)) @@ -106,7 +120,7 @@ mod tests { #[test] fn default_mode_instructions_replace_mode_names_placeholder() { - let default_instructions = default_preset(true) + let default_instructions = default_preset(CollaborationModesConfig::new(true)) .developer_instructions .expect("default preset should include instructions") .expect("default instructions should be set"); @@ -127,7 +141,7 @@ mod tests { #[test] fn default_mode_instructions_use_plain_text_questions_when_feature_disabled() { - let default_instructions = default_preset(false) + let default_instructions = default_preset(CollaborationModesConfig::default()) .developer_instructions .expect("default preset should include instructions") .expect("default instructions should be set"); diff --git a/codex-rs/core/src/models_manager/manager.rs b/codex-rs/core/src/models_manager/manager.rs index 0e6098bca5ea..139a47a5fe2b 100644 --- a/codex-rs/core/src/models_manager/manager.rs +++ b/codex-rs/core/src/models_manager/manager.rs @@ -91,6 +91,7 @@ impl ModelsManager { Self { remote_models: RwLock::new(remote_models), catalog_mode, + catalog_mode, collaboration_modes_config, auth_manager, etag: RwLock::new(None), @@ -388,7 +389,11 @@ impl ModelsManager { Self::load_remote_models_from_file() .unwrap_or_else(|err| panic!("failed to load bundled models.json: {err}")), ), +<<<<<<< HEAD catalog_mode: CatalogMode::Default, +======= + has_custom_model_catalog: false, +>>>>>>> ec21465e1 (Refactor collaboration mode preset availability config) collaboration_modes_config: CollaborationModesConfig::default(), auth_manager, etag: RwLock::new(None), @@ -516,8 +521,12 @@ mod tests { .expect("load default test config"); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); - let manager = - ModelsManager::new(codex_home.path().to_path_buf(), auth_manager, None, false); + let manager = ModelsManager::new( + codex_home.path().to_path_buf(), + auth_manager, + None, + CollaborationModesConfig::default(), + ); let known_slug = manager .get_remote_models() .await @@ -554,7 +563,7 @@ mod tests { Some(ModelsResponse { models: vec![remote_model("gpt-overlay", "Overlay", 0)], }), - false, + CollaborationModesConfig::default(), ); let model_info = manager @@ -578,8 +587,12 @@ mod tests { .expect("load default test config"); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); - let manager = - ModelsManager::new(codex_home.path().to_path_buf(), auth_manager, None, false); + let manager = ModelsManager::new( + codex_home.path().to_path_buf(), + auth_manager, + None, + CollaborationModesConfig::default(), + ); let known_slug = manager .get_remote_models() .await @@ -605,8 +618,12 @@ mod tests { .expect("load default test config"); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); - let manager = - ModelsManager::new(codex_home.path().to_path_buf(), auth_manager, None, false); + let manager = ModelsManager::new( + codex_home.path().to_path_buf(), + auth_manager, + None, + CollaborationModesConfig::default(), + ); let known_slug = manager .get_remote_models() .await diff --git a/codex-rs/core/src/test_support.rs b/codex-rs/core/src/test_support.rs index 3084d92fa872..12ba7cda829b 100644 --- a/codex-rs/core/src/test_support.rs +++ b/codex-rs/core/src/test_support.rs @@ -85,5 +85,7 @@ pub fn all_model_presets() -> &'static Vec { } pub fn builtin_collaboration_mode_presets() -> Vec { - collaboration_mode_presets::builtin_collaboration_mode_presets(false) + collaboration_mode_presets::builtin_collaboration_mode_presets( + collaboration_mode_presets::CollaborationModesConfig::default(), + ) } diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index 4b18dd21e8b3..41280456ddad 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -11,6 +11,7 @@ use crate::error::CodexErr; use crate::error::Result as CodexResult; use crate::file_watcher::FileWatcher; use crate::file_watcher::FileWatcherEvent; +use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig; use crate::models_manager::manager::ModelsManager; use crate::protocol::Event; use crate::protocol::EventMsg; @@ -143,7 +144,7 @@ impl ThreadManager { auth_manager: Arc, session_source: SessionSource, model_catalog: Option, - default_mode_request_user_input: bool, + collaboration_modes_config: CollaborationModesConfig, ) -> Self { let (thread_created_tx, _) = broadcast::channel(THREAD_CREATED_CHANNEL_CAPACITY); let skills_manager = Arc::new(SkillsManager::new(codex_home.clone())); @@ -156,7 +157,7 @@ impl ThreadManager { codex_home, auth_manager.clone(), model_catalog, - default_mode_request_user_input, + collaboration_modes_config, )), skills_manager, file_watcher, diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 8dcabc428e6f..f61a3a33f253 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -12,6 +12,7 @@ use codex_core::built_in_model_providers; use codex_core::default_client::originator; use codex_core::error::CodexErr; use codex_core::features::Feature; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_otel::OtelManager; use codex_otel::TelemetryAuthMode; use codex_protocol::ThreadId; @@ -584,9 +585,11 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { auth_manager, SessionSource::Exec, config.model_catalog.clone(), - config - .features - .enabled(Feature::DefaultModeRequestUserInput), + CollaborationModesConfig::new( + config + .features + .enabled(Feature::DefaultModeRequestUserInput), + ), ); let NewThread { thread: codex, .. } = thread_manager .start_thread(config) diff --git a/codex-rs/core/tests/suite/model_info_overrides.rs b/codex-rs/core/tests/suite/model_info_overrides.rs index 4fb12f18be13..ee4afc36bf24 100644 --- a/codex-rs/core/tests/suite/model_info_overrides.rs +++ b/codex-rs/core/tests/suite/model_info_overrides.rs @@ -1,4 +1,5 @@ use codex_core::CodexAuth; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::models_manager::manager::ModelsManager; use codex_protocol::openai_models::TruncationPolicyConfig; use core_test_support::load_default_config_for_test; @@ -12,7 +13,12 @@ async fn offline_model_info_without_tool_output_override() { let auth_manager = codex_core::test_support::auth_manager_from_auth( CodexAuth::create_dummy_chatgpt_auth_for_testing(), ); - let manager = ModelsManager::new(config.codex_home.clone(), auth_manager, None, false); + let manager = ModelsManager::new( + config.codex_home.clone(), + auth_manager, + None, + CollaborationModesConfig::default(), + ); let model_info = manager.get_model_info("gpt-5.1", &config).await; @@ -30,7 +36,12 @@ async fn offline_model_info_with_tool_output_override() { let auth_manager = codex_core::test_support::auth_manager_from_auth( CodexAuth::create_dummy_chatgpt_auth_for_testing(), ); - let manager = ModelsManager::new(config.codex_home.clone(), auth_manager, None, false); + let manager = ModelsManager::new( + config.codex_home.clone(), + auth_manager, + None, + CollaborationModesConfig::default(), + ); let model_info = manager.get_model_info("gpt-5.1-codex", &config).await; diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index e791c7f53940..3eaced795bcb 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -32,6 +32,7 @@ use codex_core::config_loader::ConfigLoadError; use codex_core::config_loader::format_config_error_with_source; use codex_core::format_exec_policy_error_with_source; use codex_core::git_info::get_git_repo_root; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::models_manager::manager::RefreshStrategy; use codex_protocol::approvals::ElicitationAction; use codex_protocol::config_types::SandboxMode; @@ -398,9 +399,11 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result auth_manager.clone(), SessionSource::Exec, config.model_catalog.clone(), - config - .features - .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), + CollaborationModesConfig::new( + config + .features + .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), + ), )); let default_model = thread_manager .get_models_manager() diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs index 5ba9f3a13e5f..7a93fcb34fea 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -6,6 +6,7 @@ use codex_core::ThreadManager; use codex_core::config::Config; use codex_core::default_client::USER_AGENT_SUFFIX; use codex_core::default_client::get_codex_user_agent; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_protocol::ThreadId; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::Submission; @@ -62,9 +63,11 @@ impl MessageProcessor { auth_manager, SessionSource::Mcp, config.model_catalog.clone(), - config - .features - .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), + CollaborationModesConfig::new( + config + .features + .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), + ), )); Self { outgoing, diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 2a55c6e64bab..367cee59ffb4 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -47,6 +47,7 @@ use codex_core::config::edit::ConfigEdit; use codex_core::config::edit::ConfigEditsBuilder; use codex_core::config_loader::ConfigLayerStackOrdering; use codex_core::features::Feature; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::models_manager::manager::RefreshStrategy; use codex_core::models_manager::model_presets::HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG; use codex_core::models_manager::model_presets::HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG; @@ -1319,9 +1320,11 @@ impl App { auth_manager.clone(), SessionSource::Cli, config.model_catalog.clone(), - config - .features - .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), + CollaborationModesConfig::new( + config + .features + .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), + ), )); let mut model = thread_manager .get_models_manager() diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index d14b2329b05c..e6ce131f7d61 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -25,6 +25,7 @@ use codex_core::config::types::WindowsSandboxModeToml; use codex_core::config_loader::RequirementSource; use codex_core::features::FEATURES; use codex_core::features::Feature; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::models_manager::manager::ModelsManager; use codex_core::skills::model::SkillMetadata; use codex_core::terminal::TerminalName; @@ -1640,7 +1641,7 @@ async fn make_chatwidget_manual( codex_home, auth_manager.clone(), None, - false, + CollaborationModesConfig::default(), )); let reasoning_effort = None; let base_mode = CollaborationMode { @@ -1767,7 +1768,7 @@ fn set_chatgpt_auth(chat: &mut ChatWidget) { chat.config.codex_home.clone(), chat.auth_manager.clone(), None, - false, + CollaborationModesConfig::default(), )); } From ba797952d6fe63584795c68711e7acfded2163f8 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Feb 2026 17:05:46 -0800 Subject: [PATCH 07/16] Document collaboration mode feature-flag config --- .../core/src/models_manager/collaboration_mode_presets.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs index 7b0a9b22d46c..23ea1b22789c 100644 --- a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs +++ b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs @@ -10,8 +10,14 @@ const KNOWN_MODE_NAMES_PLACEHOLDER: &str = "{{KNOWN_MODE_NAMES}}"; const REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER: &str = "{{REQUEST_USER_INPUT_AVAILABILITY}}"; const QUESTION_STRATEGY_PLACEHOLDER: &str = "{{QUESTION_STRATEGY}}"; +/// Stores feature flags that control collaboration-mode behavior. +/// +/// Keep mode-related flags here so new collaboration-mode capabilities can be +/// added without large cross-cutting diffs to constructor and call-site +/// signatures. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct CollaborationModesConfig { + /// Enables `request_user_input` availability in Default mode. pub default_mode_request_user_input: bool, } From 4b1c6e69257c7ce65744f1fa2246228c4dfbd7f9 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Feb 2026 17:23:51 -0800 Subject: [PATCH 08/16] Rename asking guidance placeholder in default mode template --- .../models_manager/collaboration_mode_presets.rs | 16 ++++++++++------ .../core/templates/collaboration_mode/default.md | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs index 23ea1b22789c..a9a7321714a7 100644 --- a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs +++ b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs @@ -8,7 +8,7 @@ const COLLABORATION_MODE_DEFAULT: &str = include_str!("../../templates/collaboration_mode/default.md"); const KNOWN_MODE_NAMES_PLACEHOLDER: &str = "{{KNOWN_MODE_NAMES}}"; const REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER: &str = "{{REQUEST_USER_INPUT_AVAILABILITY}}"; -const QUESTION_STRATEGY_PLACEHOLDER: &str = "{{QUESTION_STRATEGY}}"; +const ASKING_QUESTIONS_GUIDANCE_PLACEHOLDER: &str = "{{ASKING_QUESTIONS_GUIDANCE}}"; /// Stores feature flags that control collaboration-mode behavior. /// @@ -61,15 +61,19 @@ fn default_mode_instructions(collaboration_modes_config: CollaborationModesConfi ModeKind::Default, collaboration_modes_config.default_mode_request_user_input, ); - let question_strategy = - question_strategy_message(collaboration_modes_config.default_mode_request_user_input); + let asking_questions_guidance = asking_questions_guidance_message( + collaboration_modes_config.default_mode_request_user_input, + ); COLLABORATION_MODE_DEFAULT .replace(KNOWN_MODE_NAMES_PLACEHOLDER, &known_mode_names) .replace( REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER, &request_user_input_availability, ) - .replace(QUESTION_STRATEGY_PLACEHOLDER, &question_strategy) + .replace( + ASKING_QUESTIONS_GUIDANCE_PLACEHOLDER, + &asking_questions_guidance, + ) } fn format_mode_names(modes: &[ModeKind]) -> String { @@ -98,7 +102,7 @@ fn request_user_input_availability_message( } } -fn question_strategy_message(default_mode_request_user_input: bool) -> String { +fn asking_questions_guidance_message(default_mode_request_user_input: bool) -> String { if default_mode_request_user_input { "In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, prefer using the `request_user_input` tool rather than writing a multiple choice question as a textual assistant message. Never write a multiple choice question as a textual assistant message.".to_string() } else { @@ -133,7 +137,7 @@ mod tests { assert!(!default_instructions.contains(KNOWN_MODE_NAMES_PLACEHOLDER)); assert!(!default_instructions.contains(REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER)); - assert!(!default_instructions.contains(QUESTION_STRATEGY_PLACEHOLDER)); + assert!(!default_instructions.contains(ASKING_QUESTIONS_GUIDANCE_PLACEHOLDER)); let known_mode_names = format_mode_names(&TUI_VISIBLE_COLLABORATION_MODES); let expected_snippet = format!("Known mode names are {known_mode_names}."); diff --git a/codex-rs/core/templates/collaboration_mode/default.md b/codex-rs/core/templates/collaboration_mode/default.md index 6ff434a7565d..ff00857c6d16 100644 --- a/codex-rs/core/templates/collaboration_mode/default.md +++ b/codex-rs/core/templates/collaboration_mode/default.md @@ -8,4 +8,4 @@ Your active mode changes only when new developer instructions with a different ` {{REQUEST_USER_INPUT_AVAILABILITY}} -{{QUESTION_STRATEGY}} +{{ASKING_QUESTIONS_GUIDANCE}} From efc0535cebf389e5231531615bb4f78b82cac755 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Feb 2026 17:39:46 -0800 Subject: [PATCH 09/16] Use struct literals for collaboration mode config --- .../app-server/src/codex_message_processor.rs | 5 +++-- codex-rs/app-server/src/message_processor.rs | 6 +++--- .../collaboration_mode_presets.rs | 18 ++++++------------ codex-rs/core/tests/suite/client.rs | 6 +++--- codex-rs/exec/src/lib.rs | 6 +++--- codex-rs/mcp-server/src/message_processor.rs | 6 +++--- codex-rs/tui/src/app.rs | 6 +++--- 7 files changed, 24 insertions(+), 29 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index ed4d4de9e595..3a2d928d98d7 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -5573,8 +5573,9 @@ impl CodexMessageProcessor { } }; - let collaboration_modes_config = - CollaborationModesConfig::new(thread.enabled(Feature::DefaultModeRequestUserInput)); + let collaboration_modes_config = CollaborationModesConfig { + default_mode_request_user_input: thread.enabled(Feature::DefaultModeRequestUserInput), + }; let collaboration_mode = params.collaboration_mode.map(|mode| { self.normalize_turn_start_collaboration_mode(mode, collaboration_modes_config) }); diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index f8abe57c7094..9f1b182cfa08 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -183,11 +183,11 @@ impl MessageProcessor { auth_manager.clone(), SessionSource::VSCode, config.model_catalog.clone(), - CollaborationModesConfig::new( - config + CollaborationModesConfig { + default_mode_request_user_input: config .features .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), - ), + }, )); let cloud_requirements = Arc::new(RwLock::new(cloud_requirements)); let codex_message_processor = CodexMessageProcessor::new(CodexMessageProcessorArgs { diff --git a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs index a9a7321714a7..5c5b2124037d 100644 --- a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs +++ b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs @@ -21,14 +21,6 @@ pub struct CollaborationModesConfig { pub default_mode_request_user_input: bool, } -impl CollaborationModesConfig { - pub const fn new(default_mode_request_user_input: bool) -> Self { - Self { - default_mode_request_user_input, - } - } -} - pub(crate) fn builtin_collaboration_mode_presets( collaboration_modes_config: CollaborationModesConfig, ) -> Vec { @@ -130,10 +122,12 @@ mod tests { #[test] fn default_mode_instructions_replace_mode_names_placeholder() { - let default_instructions = default_preset(CollaborationModesConfig::new(true)) - .developer_instructions - .expect("default preset should include instructions") - .expect("default instructions should be set"); + let default_instructions = default_preset(CollaborationModesConfig { + default_mode_request_user_input: true, + }) + .developer_instructions + .expect("default preset should include instructions") + .expect("default instructions should be set"); assert!(!default_instructions.contains(KNOWN_MODE_NAMES_PLACEHOLDER)); assert!(!default_instructions.contains(REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER)); diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index f61a3a33f253..e040e61e77fa 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -585,11 +585,11 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { auth_manager, SessionSource::Exec, config.model_catalog.clone(), - CollaborationModesConfig::new( - config + CollaborationModesConfig { + default_mode_request_user_input: config .features .enabled(Feature::DefaultModeRequestUserInput), - ), + }, ); let NewThread { thread: codex, .. } = thread_manager .start_thread(config) diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 3eaced795bcb..1dca110aff65 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -399,11 +399,11 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result auth_manager.clone(), SessionSource::Exec, config.model_catalog.clone(), - CollaborationModesConfig::new( - config + CollaborationModesConfig { + default_mode_request_user_input: config .features .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), - ), + }, )); let default_model = thread_manager .get_models_manager() diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs index 7a93fcb34fea..6b634e194731 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -63,11 +63,11 @@ impl MessageProcessor { auth_manager, SessionSource::Mcp, config.model_catalog.clone(), - CollaborationModesConfig::new( - config + CollaborationModesConfig { + default_mode_request_user_input: config .features .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), - ), + }, )); Self { outgoing, diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 367cee59ffb4..62d459fc6ff9 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1320,11 +1320,11 @@ impl App { auth_manager.clone(), SessionSource::Cli, config.model_catalog.clone(), - CollaborationModesConfig::new( - config + CollaborationModesConfig { + default_mode_request_user_input: config .features .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), - ), + }, )); let mut model = thread_manager .get_models_manager() From 374d3df1f512b006965f174374780386b98bec86 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Feb 2026 19:00:02 -0800 Subject: [PATCH 10/16] Honor thread overrides in collaboration mode list --- .../src/protocol/common.rs | 4 +- .../app-server-protocol/src/protocol/v2.rs | 7 +- codex-rs/app-server/README.md | 2 +- .../app-server/src/codex_message_processor.rs | 40 +++++++++- .../tests/suite/v2/collaboration_mode_list.rs | 79 ++++++++++++++++++- 5 files changed, 126 insertions(+), 6 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 357341cc4b31..2b81323010f7 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1326,7 +1326,9 @@ mod tests { json!({ "method": "collaborationMode/list", "id": 7, - "params": {} + "params": { + "threadId": null + } }), serde_json::to_value(&request)?, ); diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 2fc2344304e6..a1a0ffc17c59 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1407,7 +1407,12 @@ pub struct ModelListResponse { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] -pub struct CollaborationModeListParams {} +pub struct CollaborationModeListParams { + /// Optional loaded thread id. When provided, built-in instructions are + /// derived from that thread's effective feature overrides. + #[ts(optional = nullable)] + pub thread_id: Option, +} /// EXPERIMENTAL - collaboration mode presets response. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 2c73429e2b81..3e3a1c4f9b30 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -144,7 +144,7 @@ Example with notification opt-out: - `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation). - `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options and optional `upgrade` model ids. - `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. For non-beta flags, `displayName`/`description`/`announcement` are `null`. -- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). +- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination); accepts optional `threadId` to derive built-in instructions from that thread's effective feature overrides. - `skills/list` — list skills for one or more `cwd` values (optional `forceReload`). - `skills/remote/list` — list public remote skills (**under development; do not call from production clients yet**). - `skills/remote/export` — download a remote skill by `hazelnutId` into `skills` under `codex_home` (**under development; do not call from production clients yet**). diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 3a2d928d98d7..cf0b6ee2ec45 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -3913,8 +3913,44 @@ impl CodexMessageProcessor { request_id: ConnectionRequestId, params: CollaborationModeListParams, ) { - let CollaborationModeListParams {} = params; - let items = thread_manager.list_collaboration_modes(); + let CollaborationModeListParams { thread_id } = params; + let items = if let Some(thread_id) = thread_id { + let thread_id = match ThreadId::from_string(thread_id.as_str()) { + Ok(thread_id) => thread_id, + Err(err) => { + let error = JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: format!("invalid thread id: {err}"), + data: None, + }; + outgoing.send_error(request_id, error).await; + return; + } + }; + + let thread = match thread_manager.get_thread(thread_id).await { + Ok(thread) => thread, + Err(_) => { + let error = JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: format!("thread not found: {thread_id}"), + data: None, + }; + outgoing.send_error(request_id, error).await; + return; + } + }; + + let collaboration_modes_config = CollaborationModesConfig { + default_mode_request_user_input: thread + .enabled(Feature::DefaultModeRequestUserInput), + }; + thread_manager + .get_models_manager() + .list_collaboration_modes_for_config(collaboration_modes_config) + } else { + thread_manager.list_collaboration_modes() + }; let response = CollaborationModeListResponse { data: items }; outgoing.send_response(request_id, response).await; } diff --git a/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs b/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs index 443554290989..5d3d37c2b760 100644 --- a/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs +++ b/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs @@ -10,15 +10,20 @@ use std::time::Duration; use anyhow::Result; use app_test_support::McpProcess; +use app_test_support::create_mock_responses_server_repeating_assistant; use app_test_support::to_response; use codex_app_server_protocol::CollaborationModeListParams; use codex_app_server_protocol::CollaborationModeListResponse; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::ThreadStartParams; +use codex_app_server_protocol::ThreadStartResponse; use codex_core::test_support::builtin_collaboration_mode_presets; use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::config_types::ModeKind; use pretty_assertions::assert_eq; +use serde_json::json; +use std::collections::HashMap; use tempfile::TempDir; use tokio::time::timeout; @@ -33,7 +38,7 @@ async fn list_collaboration_modes_returns_presets() -> Result<()> { timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp - .send_list_collaboration_modes_request(CollaborationModeListParams {}) + .send_list_collaboration_modes_request(CollaborationModeListParams::default()) .await?; let response: JSONRPCResponse = timeout( @@ -50,6 +55,78 @@ async fn list_collaboration_modes_returns_presets() -> Result<()> { Ok(()) } +#[tokio::test] +async fn list_collaboration_modes_honors_thread_feature_overrides() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + std::fs::write( + codex_home.path().join("config.toml"), + format!( + r#" +model = "mock-model" +approval_policy = "never" +sandbox_mode = "read-only" + +model_provider = "mock_provider" + +[features] +default_mode_request_user_input = false + +[model_providers.mock_provider] +name = "Mock provider for test" +base_url = "{}/v1" +wire_api = "responses" +request_max_retries = 0 +stream_max_retries = 0 +"#, + server.uri() + ), + )?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let thread_request_id = mcp + .send_thread_start_request(ThreadStartParams { + config: Some(HashMap::from([( + "features.default_mode_request_user_input".to_string(), + json!(true), + )])), + ..Default::default() + }) + .await?; + let thread_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_request_id)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(thread_response)?; + + let list_request_id = mcp + .send_list_collaboration_modes_request(CollaborationModeListParams { + thread_id: Some(thread.id), + }) + .await?; + let list_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(list_request_id)), + ) + .await??; + let CollaborationModeListResponse { data } = + to_response::(list_response)?; + let default_mode = data + .into_iter() + .find(|preset| preset.mode == Some(ModeKind::Default)) + .expect("default mode should be present"); + let instructions = default_mode + .developer_instructions + .flatten() + .expect("default mode should include built-in instructions"); + assert!(instructions.contains("The `request_user_input` tool is available in Default mode.")); + + Ok(()) +} + /// Builds the plan preset that the list response is expected to return. /// /// If the defaults change in the app server, this helper should be updated alongside the From 068033ea57749418213f29c465ce94b01c08f0ac Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Wed, 25 Feb 2026 11:46:40 -0800 Subject: [PATCH 11/16] Deprecate list-provided collaboration mode instructions --- .../codex_app_server_protocol.schemas.json | 9 +- .../typescript/CollaborationModeMask.ts | 11 -- .../schema/typescript/index.ts | 1 - .../typescript/v2/CollaborationModePreset.ts | 10 ++ .../schema/typescript/v2/index.ts | 1 + .../src/protocol/common.rs | 4 +- .../app-server-protocol/src/protocol/v2.rs | 36 +++++- codex-rs/app-server/README.md | 2 +- .../app-server/src/codex_message_processor.rs | 44 +------ .../tests/suite/v2/collaboration_mode_list.rs | 113 ++---------------- 10 files changed, 66 insertions(+), 165 deletions(-) delete mode 100644 codex-rs/app-server-protocol/schema/typescript/CollaborationModeMask.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModePreset.ts diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 8c7db1f972d9..944ecc04f76e 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -8149,10 +8149,11 @@ ], "type": "object" }, - "CollaborationModeMask": { - "description": "A mask for collaboration mode settings, allowing partial updates. All fields except `name` are optional, enabling selective updates.", + "CollaborationModePreset": { + "description": "EXPERIMENTAL - collaboration mode preset metadata for clients.", "properties": { - "developer_instructions": { + "developerInstructions": { + "deprecated": true, "type": [ "string", "null" @@ -8177,7 +8178,7 @@ "name": { "type": "string" }, - "reasoning_effort": { + "reasoningEffort": { "anyOf": [ { "anyOf": [ diff --git a/codex-rs/app-server-protocol/schema/typescript/CollaborationModeMask.ts b/codex-rs/app-server-protocol/schema/typescript/CollaborationModeMask.ts deleted file mode 100644 index 05902676d7de..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/CollaborationModeMask.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ModeKind } from "./ModeKind"; -import type { ReasoningEffort } from "./ReasoningEffort"; - -/** - * A mask for collaboration mode settings, allowing partial updates. - * All fields except `name` are optional, enabling selective updates. - */ -export type CollaborationModeMask = { name: string, mode: ModeKind | null, model: string | null, reasoning_effort: ReasoningEffort | null | null, developer_instructions: string | null | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index 8b1347d0269e..a954eecd2863 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -44,7 +44,6 @@ export type { CollabResumeEndEvent } from "./CollabResumeEndEvent"; export type { CollabWaitingBeginEvent } from "./CollabWaitingBeginEvent"; export type { CollabWaitingEndEvent } from "./CollabWaitingEndEvent"; export type { CollaborationMode } from "./CollaborationMode"; -export type { CollaborationModeMask } from "./CollaborationModeMask"; export type { ContentItem } from "./ContentItem"; export type { ContextCompactedEvent } from "./ContextCompactedEvent"; export type { ContextCompactionItem } from "./ContextCompactionItem"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModePreset.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModePreset.ts new file mode 100644 index 000000000000..c0763806a87d --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModePreset.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ModeKind } from "../ModeKind"; +import type { ReasoningEffort } from "../ReasoningEffort"; + +/** + * EXPERIMENTAL - collaboration mode preset metadata for clients. + */ +export type CollaborationModePreset = { name: string, mode: ModeKind | null, model: string | null, reasoningEffort: ReasoningEffort | null | null, developerInstructions: string | null | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index e49f5b49fa8c..e4a9f24c981b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -34,6 +34,7 @@ export type { CollabAgentState } from "./CollabAgentState"; export type { CollabAgentStatus } from "./CollabAgentStatus"; export type { CollabAgentTool } from "./CollabAgentTool"; export type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus"; +export type { CollaborationModePreset } from "./CollaborationModePreset"; export type { CommandAction } from "./CommandAction"; export type { CommandExecParams } from "./CommandExecParams"; export type { CommandExecResponse } from "./CommandExecResponse"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 2b81323010f7..357341cc4b31 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1326,9 +1326,7 @@ mod tests { json!({ "method": "collaborationMode/list", "id": 7, - "params": { - "threadId": null - } + "params": {} }), serde_json::to_value(&request)?, ); diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index a1a0ffc17c59..6e7a97011c47 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -12,6 +12,7 @@ use codex_protocol::approvals::NetworkPolicyRuleAction as CoreNetworkPolicyRuleA use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::config_types::ForcedLoginMethod; +use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::SandboxMode as CoreSandboxMode; @@ -1407,11 +1408,34 @@ pub struct ModelListResponse { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] -pub struct CollaborationModeListParams { - /// Optional loaded thread id. When provided, built-in instructions are - /// derived from that thread's effective feature overrides. - #[ts(optional = nullable)] - pub thread_id: Option, +pub struct CollaborationModeListParams {} + +/// EXPERIMENTAL - collaboration mode preset metadata for clients. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CollaborationModePreset { + pub name: String, + pub mode: Option, + pub model: Option, + pub reasoning_effort: Option>, + #[deprecated( + note = "Always null in collaborationMode/list. Clients should pass settings.developer_instructions: null when setting a mode to use built-in instructions, or provide their own instructions explicitly." + )] + pub developer_instructions: Option>, +} + +impl From for CollaborationModePreset { + #[allow(deprecated)] + fn from(value: CollaborationModeMask) -> Self { + Self { + name: value.name, + mode: value.mode, + model: value.model, + reasoning_effort: value.reasoning_effort, + developer_instructions: None, + } + } } /// EXPERIMENTAL - collaboration mode presets response. @@ -1419,7 +1443,7 @@ pub struct CollaborationModeListParams { #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct CollaborationModeListResponse { - pub data: Vec, + pub data: Vec, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 3e3a1c4f9b30..2ce84ea1ee82 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -144,7 +144,7 @@ Example with notification opt-out: - `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation). - `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options and optional `upgrade` model ids. - `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. For non-beta flags, `displayName`/`description`/`announcement` are `null`. -- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination); accepts optional `threadId` to derive built-in instructions from that thread's effective feature overrides. +- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). Returned `developerInstructions` is deprecated and always `null`; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly. - `skills/list` — list skills for one or more `cwd` values (optional `forceReload`). - `skills/remote/list` — list public remote skills (**under development; do not call from production clients yet**). - `skills/remote/export` — download a remote skill by `hazelnutId` into `skills` under `codex_home` (**under development; do not call from production clients yet**). diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index cf0b6ee2ec45..3a84bf1750fe 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -3913,44 +3913,12 @@ impl CodexMessageProcessor { request_id: ConnectionRequestId, params: CollaborationModeListParams, ) { - let CollaborationModeListParams { thread_id } = params; - let items = if let Some(thread_id) = thread_id { - let thread_id = match ThreadId::from_string(thread_id.as_str()) { - Ok(thread_id) => thread_id, - Err(err) => { - let error = JSONRPCErrorError { - code: INVALID_REQUEST_ERROR_CODE, - message: format!("invalid thread id: {err}"), - data: None, - }; - outgoing.send_error(request_id, error).await; - return; - } - }; - - let thread = match thread_manager.get_thread(thread_id).await { - Ok(thread) => thread, - Err(_) => { - let error = JSONRPCErrorError { - code: INVALID_REQUEST_ERROR_CODE, - message: format!("thread not found: {thread_id}"), - data: None, - }; - outgoing.send_error(request_id, error).await; - return; - } - }; - - let collaboration_modes_config = CollaborationModesConfig { - default_mode_request_user_input: thread - .enabled(Feature::DefaultModeRequestUserInput), - }; - thread_manager - .get_models_manager() - .list_collaboration_modes_for_config(collaboration_modes_config) - } else { - thread_manager.list_collaboration_modes() - }; + let CollaborationModeListParams {} = params; + let items = thread_manager + .list_collaboration_modes() + .into_iter() + .map(Into::into) + .collect(); let response = CollaborationModeListResponse { data: items }; outgoing.send_response(request_id, response).await; } diff --git a/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs b/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs index 5d3d37c2b760..eb52cca41bf2 100644 --- a/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs +++ b/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs @@ -10,26 +10,21 @@ use std::time::Duration; use anyhow::Result; use app_test_support::McpProcess; -use app_test_support::create_mock_responses_server_repeating_assistant; use app_test_support::to_response; use codex_app_server_protocol::CollaborationModeListParams; use codex_app_server_protocol::CollaborationModeListResponse; +use codex_app_server_protocol::CollaborationModePreset; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; -use codex_app_server_protocol::ThreadStartParams; -use codex_app_server_protocol::ThreadStartResponse; use codex_core::test_support::builtin_collaboration_mode_presets; -use codex_protocol::config_types::CollaborationModeMask; -use codex_protocol::config_types::ModeKind; use pretty_assertions::assert_eq; -use serde_json::json; -use std::collections::HashMap; use tempfile::TempDir; use tokio::time::timeout; const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); /// Confirms the server returns the default collaboration mode presets in a stable order. +#[allow(deprecated)] #[tokio::test] async fn list_collaboration_modes_returns_presets() -> Result<()> { let codex_home = TempDir::new()?; @@ -50,100 +45,16 @@ async fn list_collaboration_modes_returns_presets() -> Result<()> { let CollaborationModeListResponse { data: items } = to_response::(response)?; - let expected = vec![plan_preset(), default_preset()]; - assert_eq!(expected, items); - Ok(()) -} - -#[tokio::test] -async fn list_collaboration_modes_honors_thread_feature_overrides() -> Result<()> { - let server = create_mock_responses_server_repeating_assistant("Done").await; - let codex_home = TempDir::new()?; - std::fs::write( - codex_home.path().join("config.toml"), - format!( - r#" -model = "mock-model" -approval_policy = "never" -sandbox_mode = "read-only" - -model_provider = "mock_provider" - -[features] -default_mode_request_user_input = false - -[model_providers.mock_provider] -name = "Mock provider for test" -base_url = "{}/v1" -wire_api = "responses" -request_max_retries = 0 -stream_max_retries = 0 -"#, - server.uri() - ), - )?; - - let mut mcp = McpProcess::new(codex_home.path()).await?; - timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; - - let thread_request_id = mcp - .send_thread_start_request(ThreadStartParams { - config: Some(HashMap::from([( - "features.default_mode_request_user_input".to_string(), - json!(true), - )])), - ..Default::default() - }) - .await?; - let thread_response: JSONRPCResponse = timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(thread_request_id)), - ) - .await??; - let ThreadStartResponse { thread, .. } = to_response::(thread_response)?; - - let list_request_id = mcp - .send_list_collaboration_modes_request(CollaborationModeListParams { - thread_id: Some(thread.id), - }) - .await?; - let list_response: JSONRPCResponse = timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(list_request_id)), - ) - .await??; - let CollaborationModeListResponse { data } = - to_response::(list_response)?; - let default_mode = data + let expected: Vec = builtin_collaboration_mode_presets() .into_iter() - .find(|preset| preset.mode == Some(ModeKind::Default)) - .expect("default mode should be present"); - let instructions = default_mode - .developer_instructions - .flatten() - .expect("default mode should include built-in instructions"); - assert!(instructions.contains("The `request_user_input` tool is available in Default mode.")); - + .map(|preset| CollaborationModePreset { + name: preset.name, + mode: preset.mode, + model: preset.model, + reasoning_effort: preset.reasoning_effort, + developer_instructions: None, + }) + .collect(); + assert_eq!(expected, items); Ok(()) } - -/// Builds the plan preset that the list response is expected to return. -/// -/// If the defaults change in the app server, this helper should be updated alongside the -/// contract, or the test will fail in ways that imply a regression in the API. -fn plan_preset() -> CollaborationModeMask { - let presets = builtin_collaboration_mode_presets(); - presets - .into_iter() - .find(|p| p.mode == Some(ModeKind::Plan)) - .unwrap() -} - -/// Builds the default preset that the list response is expected to return. -fn default_preset() -> CollaborationModeMask { - let presets = builtin_collaboration_mode_presets(); - presets - .into_iter() - .find(|p| p.mode == Some(ModeKind::Default)) - .unwrap() -} From 7156627c6d47123c1957f41221e7705af664ba9c Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Wed, 25 Feb 2026 11:56:49 -0800 Subject: [PATCH 12/16] Preserve collaboration mode list field casing --- .../schema/json/codex_app_server_protocol.schemas.json | 4 ++-- .../schema/typescript/v2/CollaborationModePreset.ts | 2 +- codex-rs/app-server-protocol/src/protocol/v2.rs | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 944ecc04f76e..c66e60129804 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -8152,7 +8152,7 @@ "CollaborationModePreset": { "description": "EXPERIMENTAL - collaboration mode preset metadata for clients.", "properties": { - "developerInstructions": { + "developer_instructions": { "deprecated": true, "type": [ "string", @@ -8178,7 +8178,7 @@ "name": { "type": "string" }, - "reasoningEffort": { + "reasoning_effort": { "anyOf": [ { "anyOf": [ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModePreset.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModePreset.ts index c0763806a87d..9f5d9753b85f 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModePreset.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModePreset.ts @@ -7,4 +7,4 @@ import type { ReasoningEffort } from "../ReasoningEffort"; /** * EXPERIMENTAL - collaboration mode preset metadata for clients. */ -export type CollaborationModePreset = { name: string, mode: ModeKind | null, model: string | null, reasoningEffort: ReasoningEffort | null | null, developerInstructions: string | null | null, }; +export type CollaborationModePreset = { name: string, mode: ModeKind | null, model: string | null, reasoning_effort: ReasoningEffort | null | null, developer_instructions: string | null | null, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 6e7a97011c47..a0cad5ad6b6e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1418,7 +1418,11 @@ pub struct CollaborationModePreset { pub name: String, pub mode: Option, pub model: Option, + #[serde(rename = "reasoning_effort")] + #[ts(rename = "reasoning_effort")] pub reasoning_effort: Option>, + #[serde(rename = "developer_instructions")] + #[ts(rename = "developer_instructions")] #[deprecated( note = "Always null in collaborationMode/list. Clients should pass settings.developer_instructions: null when setting a mode to use built-in instructions, or provide their own instructions explicitly." )] From 981df956680ad2921c55c973c7151d95a81d0b1c Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Wed, 25 Feb 2026 12:34:11 -0800 Subject: [PATCH 13/16] Pass collaboration mode config to request_user_input spec --- codex-rs/core/src/tools/spec.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 5d8214f966f0..47682cf0e794 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -6,6 +6,7 @@ use crate::config::AgentRoleConfig; use crate::features::Feature; use crate::features::Features; use crate::mcp_connection_manager::ToolInfo; +use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig; use crate::tools::handlers::PLAN_TOOL; use crate::tools::handlers::SEARCH_TOOL_BM25_DEFAULT_LIMIT; use crate::tools::handlers::SEARCH_TOOL_BM25_TOOL_NAME; @@ -873,7 +874,9 @@ fn create_wait_tool() -> ToolSpec { }) } -fn create_request_user_input_tool(default_mode_request_user_input: bool) -> ToolSpec { +fn create_request_user_input_tool( + collaboration_modes_config: CollaborationModesConfig, +) -> ToolSpec { let mut option_props = BTreeMap::new(); option_props.insert( "label".to_string(), @@ -944,7 +947,9 @@ fn create_request_user_input_tool(default_mode_request_user_input: bool) -> Tool ToolSpec::Function(ResponsesApiTool { name: "request_user_input".to_string(), - description: request_user_input_tool_description(default_mode_request_user_input), + description: request_user_input_tool_description( + collaboration_modes_config.default_mode_request_user_input, + ), strict: false, parameters: JsonSchema::Object { properties, @@ -1730,9 +1735,9 @@ pub(crate) fn build_specs( builder.register_handler("js_repl_reset", js_repl_reset_handler); } - builder.push_spec(create_request_user_input_tool( - config.default_mode_request_user_input, - )); + builder.push_spec(create_request_user_input_tool(CollaborationModesConfig { + default_mode_request_user_input: config.default_mode_request_user_input, + })); builder.register_handler("request_user_input", request_user_input_handler); if config.search_tool @@ -2057,7 +2062,7 @@ mod tests { create_exec_command_tool(true, false), create_write_stdin_tool(), PLAN_TOOL.clone(), - create_request_user_input_tool(false), + create_request_user_input_tool(CollaborationModesConfig::default()), create_apply_patch_freeform_tool(), ToolSpec::WebSearch { external_web_access: Some(true), @@ -2155,7 +2160,7 @@ mod tests { let request_user_input_tool = find_tool(&tools, "request_user_input"); assert_eq!( request_user_input_tool.spec, - create_request_user_input_tool(false) + create_request_user_input_tool(CollaborationModesConfig::default()) ); features.enable(Feature::DefaultModeRequestUserInput); @@ -2169,7 +2174,9 @@ mod tests { let request_user_input_tool = find_tool(&tools, "request_user_input"); assert_eq!( request_user_input_tool.spec, - create_request_user_input_tool(true) + create_request_user_input_tool(CollaborationModesConfig { + default_mode_request_user_input: true, + }) ); } From 877186081fdca338268cb9054e3e86cf68926e60 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Wed, 25 Feb 2026 13:33:06 -0800 Subject: [PATCH 14/16] Restore collaboration mode mask type name --- .../schema/json/codex_app_server_protocol.schemas.json | 2 +- ...aborationModePreset.ts => CollaborationModeMask.ts} | 2 +- .../app-server-protocol/schema/typescript/v2/index.ts | 2 +- codex-rs/app-server-protocol/src/protocol/v2.rs | 10 +++++----- .../tests/suite/v2/collaboration_mode_list.rs | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) rename codex-rs/app-server-protocol/schema/typescript/v2/{CollaborationModePreset.ts => CollaborationModeMask.ts} (62%) diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index c66e60129804..f7b42d2077b3 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -8149,7 +8149,7 @@ ], "type": "object" }, - "CollaborationModePreset": { + "CollaborationModeMask": { "description": "EXPERIMENTAL - collaboration mode preset metadata for clients.", "properties": { "developer_instructions": { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModePreset.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModeMask.ts similarity index 62% rename from codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModePreset.ts rename to codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModeMask.ts index 9f5d9753b85f..a3fa4f769bc7 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModePreset.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModeMask.ts @@ -7,4 +7,4 @@ import type { ReasoningEffort } from "../ReasoningEffort"; /** * EXPERIMENTAL - collaboration mode preset metadata for clients. */ -export type CollaborationModePreset = { name: string, mode: ModeKind | null, model: string | null, reasoning_effort: ReasoningEffort | null | null, developer_instructions: string | null | null, }; +export type CollaborationModeMask = { name: string, mode: ModeKind | null, model: string | null, reasoning_effort: ReasoningEffort | null | null, developer_instructions: string | null | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index e4a9f24c981b..19594723db4c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -34,7 +34,7 @@ export type { CollabAgentState } from "./CollabAgentState"; export type { CollabAgentStatus } from "./CollabAgentStatus"; export type { CollabAgentTool } from "./CollabAgentTool"; export type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus"; -export type { CollaborationModePreset } from "./CollaborationModePreset"; +export type { CollaborationModeMask } from "./CollaborationModeMask"; export type { CommandAction } from "./CommandAction"; export type { CommandExecParams } from "./CommandExecParams"; export type { CommandExecResponse } from "./CommandExecResponse"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index a0cad5ad6b6e..81078e324344 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -10,7 +10,7 @@ use codex_protocol::approvals::NetworkApprovalProtocol as CoreNetworkApprovalPro use codex_protocol::approvals::NetworkPolicyAmendment as CoreNetworkPolicyAmendment; use codex_protocol::approvals::NetworkPolicyRuleAction as CoreNetworkPolicyRuleAction; use codex_protocol::config_types::CollaborationMode; -use codex_protocol::config_types::CollaborationModeMask; +use codex_protocol::config_types::CollaborationModeMask as CoreCollaborationModeMask; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Personality; @@ -1414,7 +1414,7 @@ pub struct CollaborationModeListParams {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] -pub struct CollaborationModePreset { +pub struct CollaborationModeMask { pub name: String, pub mode: Option, pub model: Option, @@ -1429,9 +1429,9 @@ pub struct CollaborationModePreset { pub developer_instructions: Option>, } -impl From for CollaborationModePreset { +impl From for CollaborationModeMask { #[allow(deprecated)] - fn from(value: CollaborationModeMask) -> Self { + fn from(value: CoreCollaborationModeMask) -> Self { Self { name: value.name, mode: value.mode, @@ -1447,7 +1447,7 @@ impl From for CollaborationModePreset { #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct CollaborationModeListResponse { - pub data: Vec, + pub data: Vec, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] diff --git a/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs b/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs index eb52cca41bf2..ea5686994f30 100644 --- a/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs +++ b/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs @@ -13,7 +13,7 @@ use app_test_support::McpProcess; use app_test_support::to_response; use codex_app_server_protocol::CollaborationModeListParams; use codex_app_server_protocol::CollaborationModeListResponse; -use codex_app_server_protocol::CollaborationModePreset; +use codex_app_server_protocol::CollaborationModeMask; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; use codex_core::test_support::builtin_collaboration_mode_presets; @@ -45,9 +45,9 @@ async fn list_collaboration_modes_returns_presets() -> Result<()> { let CollaborationModeListResponse { data: items } = to_response::(response)?; - let expected: Vec = builtin_collaboration_mode_presets() + let expected: Vec = builtin_collaboration_mode_presets() .into_iter() - .map(|preset| CollaborationModePreset { + .map(|preset| CollaborationModeMask { name: preset.name, mode: preset.mode, model: preset.model, From d6d0dfc5ce2f53bb7a31b45f2ceb7a7f4ece8e81 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Wed, 25 Feb 2026 13:54:58 -0800 Subject: [PATCH 15/16] Remove list-provided collaboration mode instructions --- .../schema/json/codex_app_server_protocol.schemas.json | 7 ------- .../schema/typescript/v2/CollaborationModeMask.ts | 2 +- codex-rs/app-server-protocol/src/protocol/v2.rs | 8 -------- codex-rs/app-server/README.md | 2 +- .../app-server/tests/suite/v2/collaboration_mode_list.rs | 2 -- 5 files changed, 2 insertions(+), 19 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index f7b42d2077b3..3da788229cf6 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -8152,13 +8152,6 @@ "CollaborationModeMask": { "description": "EXPERIMENTAL - collaboration mode preset metadata for clients.", "properties": { - "developer_instructions": { - "deprecated": true, - "type": [ - "string", - "null" - ] - }, "mode": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModeMask.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModeMask.ts index a3fa4f769bc7..83adc6446fd2 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModeMask.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModeMask.ts @@ -7,4 +7,4 @@ import type { ReasoningEffort } from "../ReasoningEffort"; /** * EXPERIMENTAL - collaboration mode preset metadata for clients. */ -export type CollaborationModeMask = { name: string, mode: ModeKind | null, model: string | null, reasoning_effort: ReasoningEffort | null | null, developer_instructions: string | null | null, }; +export type CollaborationModeMask = { name: string, mode: ModeKind | null, model: string | null, reasoning_effort: ReasoningEffort | null | null, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 81078e324344..d563287fa83c 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1421,23 +1421,15 @@ pub struct CollaborationModeMask { #[serde(rename = "reasoning_effort")] #[ts(rename = "reasoning_effort")] pub reasoning_effort: Option>, - #[serde(rename = "developer_instructions")] - #[ts(rename = "developer_instructions")] - #[deprecated( - note = "Always null in collaborationMode/list. Clients should pass settings.developer_instructions: null when setting a mode to use built-in instructions, or provide their own instructions explicitly." - )] - pub developer_instructions: Option>, } impl From for CollaborationModeMask { - #[allow(deprecated)] fn from(value: CoreCollaborationModeMask) -> Self { Self { name: value.name, mode: value.mode, model: value.model, reasoning_effort: value.reasoning_effort, - developer_instructions: None, } } } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 2ce84ea1ee82..505abb0457cc 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -144,7 +144,7 @@ Example with notification opt-out: - `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation). - `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options and optional `upgrade` model ids. - `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. For non-beta flags, `displayName`/`description`/`announcement` are `null`. -- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). Returned `developerInstructions` is deprecated and always `null`; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly. +- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly. - `skills/list` — list skills for one or more `cwd` values (optional `forceReload`). - `skills/remote/list` — list public remote skills (**under development; do not call from production clients yet**). - `skills/remote/export` — download a remote skill by `hazelnutId` into `skills` under `codex_home` (**under development; do not call from production clients yet**). diff --git a/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs b/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs index ea5686994f30..7c36827e6dd7 100644 --- a/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs +++ b/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs @@ -24,7 +24,6 @@ use tokio::time::timeout; const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); /// Confirms the server returns the default collaboration mode presets in a stable order. -#[allow(deprecated)] #[tokio::test] async fn list_collaboration_modes_returns_presets() -> Result<()> { let codex_home = TempDir::new()?; @@ -52,7 +51,6 @@ async fn list_collaboration_modes_returns_presets() -> Result<()> { mode: preset.mode, model: preset.model, reasoning_effort: preset.reasoning_effort, - developer_instructions: None, }) .collect(); assert_eq!(expected, items); From fcac11bb286e0743ec5d32c58eb1976b0ee41dd2 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Wed, 25 Feb 2026 14:06:27 -0800 Subject: [PATCH 16/16] Fix leftover rebase markers in models manager --- codex-rs/core/src/models_manager/manager.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/codex-rs/core/src/models_manager/manager.rs b/codex-rs/core/src/models_manager/manager.rs index 139a47a5fe2b..1d7309020e8a 100644 --- a/codex-rs/core/src/models_manager/manager.rs +++ b/codex-rs/core/src/models_manager/manager.rs @@ -91,7 +91,6 @@ impl ModelsManager { Self { remote_models: RwLock::new(remote_models), catalog_mode, - catalog_mode, collaboration_modes_config, auth_manager, etag: RwLock::new(None), @@ -389,11 +388,7 @@ impl ModelsManager { Self::load_remote_models_from_file() .unwrap_or_else(|err| panic!("failed to load bundled models.json: {err}")), ), -<<<<<<< HEAD catalog_mode: CatalogMode::Default, -======= - has_custom_model_catalog: false, ->>>>>>> ec21465e1 (Refactor collaboration mode preset availability config) collaboration_modes_config: CollaborationModesConfig::default(), auth_manager, etag: RwLock::new(None),