add tools.description_overrides

This commit is contained in:
Camillo Lugaresi
2026-04-07 19:05:35 -07:00
parent 600c3e49e0
commit fe7717da75
10 changed files with 282 additions and 0 deletions

View File

@@ -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<bool>,
/// Tool descriptions keyed by tool name. Overrides only apply to tools
/// that are enabled for the current turn.
#[serde(default)]
pub description_overrides: BTreeMap<String, String>,
}
#[derive(Deserialize)]

View File

@@ -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.",

View File

@@ -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(

View File

@@ -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::<ConfigToml>(
@@ -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(),

View File

@@ -510,6 +510,9 @@ pub struct Config {
/// Additional parameters for the web search tool when it is enabled.
pub web_search_config: Option<WebSearchConfig>,
/// Tool descriptions keyed by tool name. Overrides only apply to enabled tools.
pub tool_description_overrides: BTreeMap<String, String>,
/// 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<String, String> {
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<WebSearchMode>,
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,

View File

@@ -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<String, String>,
}
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<String, String>,
) -> 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

View File

@@ -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
}

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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