From fe7717da7534db79cfaea7bd6dd2aabaca5999ed Mon Sep 17 00:00:00 2001 From: Camillo Lugaresi Date: Tue, 7 Apr 2026 19:05:35 -0700 Subject: [PATCH] add tools.description_overrides --- codex-rs/config/src/config_toml.rs | 5 + codex-rs/core/config.schema.json | 8 ++ codex-rs/core/src/codex.rs | 3 + codex-rs/core/src/config/config_tests.rs | 92 +++++++++++++++ codex-rs/core/src/config/mod.rs | 19 ++++ codex-rs/tools/src/tool_config.rs | 11 ++ codex-rs/tools/src/tool_registry_plan.rs | 16 +++ .../tools/src/tool_registry_plan_tests.rs | 105 ++++++++++++++++++ codex-rs/tools/src/tool_spec.rs | 11 ++ docs/config.md | 12 ++ 10 files changed, 282 insertions(+) diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index caf9d25b88..7dfc11eba5 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -482,6 +482,11 @@ pub struct ToolsToml { /// Enable the `view_image` tool that lets the agent attach local images. #[serde(default)] pub view_image: Option, + + /// Tool descriptions keyed by tool name. Overrides only apply to tools + /// that are enabled for the current turn. + #[serde(default)] + pub description_overrides: BTreeMap, } #[derive(Deserialize)] diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index e4e7ff85b7..8ec881d138 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -1699,6 +1699,14 @@ "ToolsToml": { "additionalProperties": false, "properties": { + "description_overrides": { + "additionalProperties": { + "type": "string" + }, + "default": {}, + "description": "Tool descriptions keyed by tool name. Overrides only apply to tools that are enabled for the current turn.", + "type": "object" + }, "view_image": { "default": null, "description": "Enable the `view_image` tool that lets the agent attach local images.", diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 66f7c020a5..da0987b787 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -959,6 +959,7 @@ impl TurnContext { }) .with_unified_exec_shell_mode(self.tools_config.unified_exec_shell_mode.clone()) .with_web_search_config(self.tools_config.web_search_config.clone()) + .with_tool_description_overrides(self.tools_config.tool_description_overrides.clone()) .with_allow_login_shell(self.tools_config.allow_login_shell) .with_has_environment(self.tools_config.has_environment) .with_agent_type_description(crate::agent::role::spawn_tool_spec::build( @@ -1441,6 +1442,7 @@ impl Session { main_execve_wrapper_exe, ) .with_web_search_config(per_turn_config.web_search_config.clone()) + .with_tool_description_overrides(per_turn_config.tool_description_overrides.clone()) .with_allow_login_shell(per_turn_config.permissions.allow_login_shell) .with_has_environment(environment.is_some()) .with_agent_type_description(crate::agent::role::spawn_tool_spec::build( @@ -5619,6 +5621,7 @@ async fn spawn_review_thread( sess.services.main_execve_wrapper_exe.as_ref(), ) .with_web_search_config(/*web_search_config*/ None) + .with_tool_description_overrides(config.tool_description_overrides.clone()) .with_allow_login_shell(config.permissions.allow_login_shell) .with_has_environment(parent_turn_context.environment.is_some()) .with_agent_type_description(crate::agent::role::spawn_tool_spec::build( diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index deba8a47b3..52a514e4b6 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -247,6 +247,7 @@ web_search = true Some(ToolsToml { web_search: None, view_image: None, + description_overrides: BTreeMap::new(), }) ); } @@ -266,10 +267,97 @@ web_search = false Some(ToolsToml { web_search: None, view_image: None, + description_overrides: BTreeMap::new(), }) ); } +#[test] +fn tools_description_overrides_deserialize() { + let cfg: ConfigToml = toml::from_str( + r#" +[tools.description_overrides] +spawn_agent = "Use custom spawn guidance." +update_plan = "Use custom plan guidance." +"#, + ) + .expect("TOML deserialization should succeed"); + + assert_eq!( + cfg.tools.expect("tools config").description_overrides, + BTreeMap::from([ + ( + "spawn_agent".to_string(), + "Use custom spawn guidance.".to_string() + ), + ( + "update_plan".to_string(), + "Use custom plan guidance.".to_string() + ), + ]) + ); +} + +#[test] +fn tool_description_overrides_profile_overlays_base() -> std::io::Result<()> { + let mut cfg = ConfigToml { + tools: Some(ToolsToml { + description_overrides: BTreeMap::from([ + ( + "spawn_agent".to_string(), + "Base spawn guidance.".to_string(), + ), + ("update_plan".to_string(), "Base plan guidance.".to_string()), + ]), + ..Default::default() + }), + profile: Some("custom".to_string()), + ..Default::default() + }; + cfg.profiles.insert( + "custom".to_string(), + ConfigProfile { + tools: Some(ToolsToml { + description_overrides: BTreeMap::from([ + ( + "spawn_agent".to_string(), + "Profile spawn guidance.".to_string(), + ), + ( + "wait_agent".to_string(), + "Profile wait guidance.".to_string(), + ), + ]), + ..Default::default() + }), + ..Default::default() + }, + ); + + let codex_home = tempdir()?; + let config = Config::load_from_base_config_with_overrides( + cfg, + ConfigOverrides::default(), + codex_home.path().to_path_buf(), + )?; + + assert_eq!( + config.tool_description_overrides, + BTreeMap::from([ + ( + "spawn_agent".to_string(), + "Profile spawn guidance.".to_string(), + ), + ("update_plan".to_string(), "Base plan guidance.".to_string()), + ( + "wait_agent".to_string(), + "Profile wait guidance.".to_string() + ), + ]) + ); + Ok(()) +} + #[test] fn rejects_provider_auth_with_env_key() { let err = toml::from_str::( @@ -4517,6 +4605,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { include_apply_patch_tool: false, web_search_mode: Constrained::allow_any(WebSearchMode::Cached), web_search_config: None, + tool_description_overrides: BTreeMap::new(), use_experimental_unified_exec_tool: !cfg!(windows), background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS, ghost_snapshot: GhostSnapshotConfig::default(), @@ -4662,6 +4751,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { include_apply_patch_tool: false, web_search_mode: Constrained::allow_any(WebSearchMode::Cached), web_search_config: None, + tool_description_overrides: BTreeMap::new(), use_experimental_unified_exec_tool: !cfg!(windows), background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS, ghost_snapshot: GhostSnapshotConfig::default(), @@ -4805,6 +4895,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { include_apply_patch_tool: false, web_search_mode: Constrained::allow_any(WebSearchMode::Cached), web_search_config: None, + tool_description_overrides: BTreeMap::new(), use_experimental_unified_exec_tool: !cfg!(windows), background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS, ghost_snapshot: GhostSnapshotConfig::default(), @@ -4934,6 +5025,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { include_apply_patch_tool: false, web_search_mode: Constrained::allow_any(WebSearchMode::Cached), web_search_config: None, + tool_description_overrides: BTreeMap::new(), use_experimental_unified_exec_tool: !cfg!(windows), background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS, ghost_snapshot: GhostSnapshotConfig::default(), diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index e0a24bcc10..e4c1f68ac6 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -510,6 +510,9 @@ pub struct Config { /// Additional parameters for the web search tool when it is enabled. pub web_search_config: Option, + /// Tool descriptions keyed by tool name. Overrides only apply to enabled tools. + pub tool_description_overrides: BTreeMap, + /// If set to `true`, used only the experimental unified exec tool. pub use_experimental_unified_exec_tool: bool, @@ -1279,6 +1282,20 @@ fn resolve_web_search_config( } } +fn resolve_tool_description_overrides( + config_toml: &ConfigToml, + config_profile: &ConfigProfile, +) -> BTreeMap { + let mut overrides = BTreeMap::new(); + if let Some(tools) = &config_toml.tools { + overrides.extend(tools.description_overrides.clone()); + } + if let Some(tools) = &config_profile.tools { + overrides.extend(tools.description_overrides.clone()); + } + overrides +} + pub(crate) fn resolve_web_search_mode_for_turn( web_search_mode: &Constrained, sandbox_policy: &SandboxPolicy, @@ -1607,6 +1624,7 @@ impl Config { let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features) .unwrap_or(WebSearchMode::Cached); let web_search_config = resolve_web_search_config(&cfg, &config_profile); + let tool_description_overrides = resolve_tool_description_overrides(&cfg, &config_profile); let agent_roles = agent_roles::load_agent_roles(&cfg, &config_layer_stack, &mut startup_warnings)?; @@ -2037,6 +2055,7 @@ impl Config { include_apply_patch_tool: include_apply_patch_tool_flag, web_search_mode: constrained_web_search_mode.value, web_search_config, + tool_description_overrides, use_experimental_unified_exec_tool, background_terminal_max_timeout, ghost_snapshot, diff --git a/codex-rs/tools/src/tool_config.rs b/codex-rs/tools/src/tool_config.rs index 1b55d2c4f4..24f3af8902 100644 --- a/codex-rs/tools/src/tool_config.rs +++ b/codex-rs/tools/src/tool_config.rs @@ -14,6 +14,7 @@ use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_utils_absolute_path::AbsolutePathBuf; +use std::collections::BTreeMap; use std::path::PathBuf; #[derive(Debug, Clone, Copy, Eq, PartialEq)] @@ -110,6 +111,7 @@ pub struct ToolsConfig { pub agent_jobs_tools: bool, pub agent_jobs_worker_tools: bool, pub agent_type_description: String, + pub tool_description_overrides: BTreeMap, } pub struct ToolsConfigParams<'a> { @@ -225,6 +227,7 @@ impl ToolsConfig { agent_jobs_tools: include_agent_jobs, agent_jobs_worker_tools, agent_type_description: String::new(), + tool_description_overrides: BTreeMap::new(), } } @@ -233,6 +236,14 @@ impl ToolsConfig { self } + pub fn with_tool_description_overrides( + mut self, + tool_description_overrides: BTreeMap, + ) -> Self { + self.tool_description_overrides = tool_description_overrides; + self + } + pub fn with_allow_login_shell(mut self, allow_login_shell: bool) -> Self { self.allow_login_shell = allow_login_shell; self diff --git a/codex-rs/tools/src/tool_registry_plan.rs b/codex-rs/tools/src/tool_registry_plan.rs index dc76c12d36..f560ad615e 100644 --- a/codex-rs/tools/src/tool_registry_plan.rs +++ b/codex-rs/tools/src/tool_registry_plan.rs @@ -491,6 +491,22 @@ pub fn build_tool_registry_plan( } } + if !config.tool_description_overrides.is_empty() { + for configured_tool in &mut plan.specs { + let Some(override_description) = config + .tool_description_overrides + .get(configured_tool.name()) + else { + continue; + }; + let Some(description) = configured_tool.spec.description_mut() else { + continue; + }; + + *description = override_description.clone(); + } + } + plan } diff --git a/codex-rs/tools/src/tool_registry_plan_tests.rs b/codex-rs/tools/src/tool_registry_plan_tests.rs index d8d264e2e8..5de3b76604 100644 --- a/codex-rs/tools/src/tool_registry_plan_tests.rs +++ b/codex-rs/tools/src/tool_registry_plan_tests.rs @@ -179,6 +179,74 @@ fn test_build_specs_collab_tools_enabled() { assert!(!properties.contains_key("fork_turns")); } +#[test] +fn tool_description_override_replaces_enabled_tool_description() { + let model_info = model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::Collab); + let available_models = Vec::new(); + let mut overrides = BTreeMap::new(); + overrides.insert( + "spawn_agent".to_string(), + "Use this custom spawn guidance.".to_string(), + ); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }) + .with_tool_description_overrides(overrides); + + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + let ToolSpec::Function(ResponsesApiTool { description, .. }) = + &find_tool(&tools, "spawn_agent").spec + else { + panic!("expected spawn_agent function tool"); + }; + + assert_eq!(description, "Use this custom spawn guidance."); +} + +#[test] +fn tool_description_override_for_disabled_tool_is_ignored() { + let model_info = model_info(); + let features = Features::with_defaults(); + let available_models = Vec::new(); + let mut overrides = BTreeMap::new(); + overrides.insert( + "spawn_agents_on_csv".to_string(), + "Use this custom batch guidance.".to_string(), + ); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }) + .with_tool_description_overrides(overrides); + + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + + assert_lacks_tool_name(&tools, "spawn_agents_on_csv"); +} + #[test] fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() { let model_info = model_info(); @@ -1660,6 +1728,43 @@ fn code_mode_only_exec_description_includes_full_nested_tool_details() { assert!(description.contains("### `view_image` (`view_image`)")); } +#[test] +fn code_mode_only_exec_description_uses_nested_tool_description_overrides() { + let model_info = model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::CodeMode); + features.enable(Feature::CodeModeOnly); + let available_models = Vec::new(); + let mut overrides = BTreeMap::new(); + overrides.insert( + "update_plan".to_string(), + "Use this custom plan guidance.".to_string(), + ); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }) + .with_tool_description_overrides(overrides); + + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + let ToolSpec::Freeform(FreeformTool { description, .. }) = &find_tool(&tools, "exec").spec + else { + panic!("expected freeform tool"); + }; + + assert!(description.contains("Use this custom plan guidance.")); +} + #[test] fn code_mode_exec_description_omits_nested_tool_details_when_not_code_mode_only() { let model_info = model_info(); diff --git a/codex-rs/tools/src/tool_spec.rs b/codex-rs/tools/src/tool_spec.rs index 24644e2260..926ca4c766 100644 --- a/codex-rs/tools/src/tool_spec.rs +++ b/codex-rs/tools/src/tool_spec.rs @@ -64,6 +64,17 @@ impl ToolSpec { ToolSpec::Freeform(tool) => tool.name.as_str(), } } + + pub fn description_mut(&mut self) -> Option<&mut String> { + match self { + ToolSpec::Function(tool) => Some(&mut tool.description), + ToolSpec::ToolSearch { description, .. } => Some(description), + ToolSpec::Freeform(tool) => Some(&mut tool.description), + ToolSpec::LocalShell {} + | ToolSpec::ImageGeneration { .. } + | ToolSpec::WebSearch { .. } => None, + } + } } pub fn create_local_shell_tool() -> ToolSpec { diff --git a/docs/config.md b/docs/config.md index 71f3548deb..8f07b775bc 100644 --- a/docs/config.md +++ b/docs/config.md @@ -40,6 +40,18 @@ When Codex knows which client started the turn, the legacy notify JSON payload a The generated JSON Schema for `config.toml` lives at `codex-rs/core/config.schema.json`. +## Tool Description Overrides + +Tool descriptions can be overridden by tool name under `[tools.description_overrides]`. +Overrides only apply when that tool is enabled for the current turn; entries for +disabled tools are ignored. + +```toml +[tools.description_overrides] +spawn_agent = "Use these custom spawn instructions." +update_plan = "Use these custom planning instructions." +``` + ## SQLite State DB Codex stores the SQLite-backed state DB under `sqlite_home` (config key) or the