From 0f474549743937e0d289cc09c5935480987f1d4e Mon Sep 17 00:00:00 2001 From: Dylan Hurd Date: Sun, 1 Mar 2026 11:04:16 -0700 Subject: [PATCH] feat(config) Plan mode instructions --- codex-rs/app-server/src/message_processor.rs | 1 + codex-rs/core/config.schema.json | 6 ++++ codex-rs/core/src/config/mod.rs | 8 ++++++ codex-rs/core/src/config/profile.rs | 1 + .../collaboration_mode_presets.rs | 28 +++++++++++++++---- codex-rs/core/src/models_manager/manager.rs | 11 +++++++- codex-rs/core/src/test_support.rs | 1 + codex-rs/core/src/thread_manager.rs | 2 ++ codex-rs/core/tests/suite/client.rs | 1 + .../core/tests/suite/model_info_overrides.rs | 2 ++ codex-rs/exec/src/lib.rs | 1 + codex-rs/mcp-server/src/message_processor.rs | 1 + codex-rs/tui/src/app.rs | 1 + codex-rs/tui/src/chatwidget/tests.rs | 2 ++ 14 files changed, 60 insertions(+), 6 deletions(-) diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 51049e624a..eb999aad9e 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -193,6 +193,7 @@ impl MessageProcessor { .features .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), }, + config.plan_mode_developer_instructions.clone(), )); thread_manager .plugins_manager() diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index c9d65cc36d..bf31333eb1 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -521,6 +521,9 @@ "personality": { "$ref": "#/definitions/Personality" }, + "plan_mode_developer_instructions": { + "type": "string" + }, "plan_mode_reasoning_effort": { "$ref": "#/definitions/ReasoningEffort" }, @@ -2158,6 +2161,9 @@ ], "description": "Optionally specify a personality for the model" }, + "plan_mode_developer_instructions": { + "type": "string" + }, "plan_mode_reasoning_effort": { "$ref": "#/definitions/ReasoningEffort" }, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index f7ef78b26d..c53f03140a 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -433,6 +433,10 @@ pub struct Config { /// Plan preset. The `none` value means "no reasoning" (not "inherit the /// global default"). pub plan_mode_reasoning_effort: Option, + /// Optional Plan-mode-specific developer instructions override. + /// + /// When unset, Plan mode uses the built-in Plan preset instructions. + pub plan_mode_developer_instructions: Option, /// Optional value to use for `reasoning.summary` when making a request /// using the Responses API. When unset, the model catalog default is used. @@ -1201,6 +1205,7 @@ pub struct ConfigToml { pub model_reasoning_effort: Option, pub plan_mode_reasoning_effort: Option, + pub plan_mode_developer_instructions: Option, pub model_reasoning_summary: Option, /// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`). pub model_verbosity: Option, @@ -2422,6 +2427,9 @@ impl Config { plan_mode_reasoning_effort: config_profile .plan_mode_reasoning_effort .or(cfg.plan_mode_reasoning_effort), + plan_mode_developer_instructions: config_profile + .plan_mode_developer_instructions + .or(cfg.plan_mode_developer_instructions), model_reasoning_summary: config_profile .model_reasoning_summary .or(cfg.model_reasoning_summary), diff --git a/codex-rs/core/src/config/profile.rs b/codex-rs/core/src/config/profile.rs index ce454ff0a8..59b945b6ab 100644 --- a/codex-rs/core/src/config/profile.rs +++ b/codex-rs/core/src/config/profile.rs @@ -28,6 +28,7 @@ pub struct ConfigProfile { pub sandbox_mode: Option, pub model_reasoning_effort: Option, pub plan_mode_reasoning_effort: Option, + pub plan_mode_developer_instructions: Option, pub model_reasoning_summary: Option, pub model_verbosity: Option, /// Optional path to a JSON model catalog (applied on startup only). 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 5c5b212403..1a3d69db06 100644 --- a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs +++ b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs @@ -23,17 +23,25 @@ pub struct CollaborationModesConfig { pub(crate) fn builtin_collaboration_mode_presets( collaboration_modes_config: CollaborationModesConfig, + plan_mode_developer_instructions: Option<&str>, ) -> Vec { - vec![plan_preset(), default_preset(collaboration_modes_config)] + vec![ + plan_preset(plan_mode_developer_instructions), + default_preset(collaboration_modes_config), + ] } -fn plan_preset() -> CollaborationModeMask { +fn plan_preset(plan_mode_developer_instructions: Option<&str>) -> CollaborationModeMask { CollaborationModeMask { name: ModeKind::Plan.display_name().to_string(), mode: Some(ModeKind::Plan), model: None, reasoning_effort: Some(Some(ReasoningEffort::Medium)), - developer_instructions: Some(Some(COLLABORATION_MODE_PLAN.to_string())), + developer_instructions: Some(Some( + plan_mode_developer_instructions + .unwrap_or(COLLABORATION_MODE_PLAN) + .to_string(), + )), } } @@ -109,17 +117,27 @@ mod tests { #[test] fn preset_names_use_mode_display_names() { - assert_eq!(plan_preset().name, ModeKind::Plan.display_name()); + assert_eq!(plan_preset(None).name, ModeKind::Plan.display_name()); assert_eq!( default_preset(CollaborationModesConfig::default()).name, ModeKind::Default.display_name() ); assert_eq!( - plan_preset().reasoning_effort, + plan_preset(None).reasoning_effort, Some(Some(ReasoningEffort::Medium)) ); } + #[test] + fn plan_preset_uses_configured_developer_instructions_override() { + let override_instructions = "Use plan override."; + let plan = plan_preset(Some(override_instructions)); + assert_eq!( + plan.developer_instructions, + Some(Some(override_instructions.to_string())) + ); + } + #[test] fn default_mode_instructions_replace_mode_names_placeholder() { let default_instructions = default_preset(CollaborationModesConfig { diff --git a/codex-rs/core/src/models_manager/manager.rs b/codex-rs/core/src/models_manager/manager.rs index 35e723d253..3a4c02aa19 100644 --- a/codex-rs/core/src/models_manager/manager.rs +++ b/codex-rs/core/src/models_manager/manager.rs @@ -61,6 +61,7 @@ pub struct ModelsManager { etag: RwLock>, cache_manager: ModelsCacheManager, provider: ModelProviderInfo, + plan_mode_developer_instructions: Option, } impl ModelsManager { @@ -74,6 +75,7 @@ impl ModelsManager { auth_manager: Arc, model_catalog: Option, collaboration_modes_config: CollaborationModesConfig, + plan_mode_developer_instructions: Option, ) -> Self { let cache_path = codex_home.join(MODEL_CACHE_FILE); let cache_manager = ModelsCacheManager::new(cache_path, DEFAULT_MODEL_CACHE_TTL); @@ -96,6 +98,7 @@ impl ModelsManager { etag: RwLock::new(None), cache_manager, provider: ModelProviderInfo::create_openai_provider(), + plan_mode_developer_instructions, } } @@ -121,7 +124,10 @@ impl ModelsManager { &self, collaboration_modes_config: CollaborationModesConfig, ) -> Vec { - builtin_collaboration_mode_presets(collaboration_modes_config) + builtin_collaboration_mode_presets( + collaboration_modes_config, + self.plan_mode_developer_instructions.as_deref(), + ) } /// Attempt to list models without blocking, using the current cached state. @@ -394,6 +400,7 @@ impl ModelsManager { etag: RwLock::new(None), cache_manager, provider, + plan_mode_developer_instructions: None, } } @@ -522,6 +529,7 @@ mod tests { auth_manager, None, CollaborationModesConfig::default(), + None, ); let known_slug = manager .get_remote_models() @@ -562,6 +570,7 @@ mod tests { models: vec![overlay], }), CollaborationModesConfig::default(), + None, ); let model_info = manager diff --git a/codex-rs/core/src/test_support.rs b/codex-rs/core/src/test_support.rs index 12ba7cda82..70a52c0249 100644 --- a/codex-rs/core/src/test_support.rs +++ b/codex-rs/core/src/test_support.rs @@ -87,5 +87,6 @@ pub fn all_model_presets() -> &'static Vec { pub fn builtin_collaboration_mode_presets() -> Vec { collaboration_mode_presets::builtin_collaboration_mode_presets( collaboration_mode_presets::CollaborationModesConfig::default(), + None, ) } diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index 68ccae1776..bcf41f7cf3 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -150,6 +150,7 @@ impl ThreadManager { session_source: SessionSource, model_catalog: Option, collaboration_modes_config: CollaborationModesConfig, + plan_mode_developer_instructions: Option, ) -> Self { let (thread_created_tx, _) = broadcast::channel(THREAD_CREATED_CHANNEL_CAPACITY); let plugins_manager = Arc::new(PluginsManager::new(codex_home.clone())); @@ -168,6 +169,7 @@ impl ThreadManager { auth_manager.clone(), model_catalog, collaboration_modes_config, + plan_mode_developer_instructions, )), skills_manager, plugins_manager, diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 30eac507cf..ffe5e21440 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -824,6 +824,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { .features .enabled(Feature::DefaultModeRequestUserInput), }, + config.plan_mode_developer_instructions.clone(), ); 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 ee4afc36bf..5f0c673574 100644 --- a/codex-rs/core/tests/suite/model_info_overrides.rs +++ b/codex-rs/core/tests/suite/model_info_overrides.rs @@ -18,6 +18,7 @@ async fn offline_model_info_without_tool_output_override() { auth_manager, None, CollaborationModesConfig::default(), + None, ); let model_info = manager.get_model_info("gpt-5.1", &config).await; @@ -41,6 +42,7 @@ async fn offline_model_info_with_tool_output_override() { auth_manager, None, CollaborationModesConfig::default(), + None, ); 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 55978021f3..9c9db9b02c 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -484,6 +484,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { .features .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), }, + config.plan_mode_developer_instructions.clone(), )); 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 e51c3d619d..4723f3b533 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -68,6 +68,7 @@ impl MessageProcessor { .features .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), }, + config.plan_mode_developer_instructions.clone(), )); Self { outgoing, diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 9608cf563e..9c240dc7c2 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1585,6 +1585,7 @@ impl App { .features .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), }, + config.plan_mode_developer_instructions.clone(), )); 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 a9819fb188..a86e3a07ad 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -1754,6 +1754,7 @@ async fn make_chatwidget_manual( auth_manager.clone(), None, CollaborationModesConfig::default(), + None, )); let reasoning_effort = None; let base_mode = CollaborationMode { @@ -1884,6 +1885,7 @@ fn set_chatgpt_auth(chat: &mut ChatWidget) { chat.auth_manager.clone(), None, CollaborationModesConfig::default(), + None, )); }