mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Make multi-agent v2 tool namespace configurable (#23147)
## Summary - Add `features.multi_agent_v2.tool_namespace` with config/schema validation for Responses-compatible namespace values. - Thread the resolved namespace into `ToolsConfig` for normal turns and review turns. - Wrap MultiAgentV2 tool specs and registry names in the configured namespace when namespace tools are supported, while falling back to the plain tool names when they are not. ## Validation - `just fmt` - `just write-config-schema` - `cargo test -p codex-features multi_agent_v2_feature_config -- --nocapture` - `cargo test -p codex-core test_build_specs_multi_agent_v2 -- --nocapture` - `cargo test -p codex-core multi_agent_v2_config -- --nocapture` - `cargo test -p codex-core multi_agent_v2_rejects_invalid_tool_namespace -- --nocapture` - `cargo test -p codex-tools` - `git diff --check`
This commit is contained in:
committed by
GitHub
Unverified
parent
f0166cadbb
commit
545ede569c
@@ -10112,6 +10112,7 @@ usage_hint_enabled = false
|
||||
usage_hint_text = "Custom delegation guidance."
|
||||
root_agent_usage_hint_text = "Root guidance."
|
||||
subagent_usage_hint_text = "Subagent guidance."
|
||||
tool_namespace = "agents"
|
||||
hide_spawn_agent_metadata = true
|
||||
non_code_mode_only = true
|
||||
"#,
|
||||
@@ -10142,6 +10143,10 @@ non_code_mode_only = true
|
||||
config.multi_agent_v2.subagent_usage_hint_text.as_deref(),
|
||||
Some("Subagent guidance.")
|
||||
);
|
||||
assert_eq!(
|
||||
config.multi_agent_v2.tool_namespace.as_deref(),
|
||||
Some("agents")
|
||||
);
|
||||
assert!(config.multi_agent_v2.hide_spawn_agent_metadata);
|
||||
assert!(config.multi_agent_v2.non_code_mode_only);
|
||||
|
||||
@@ -10164,6 +10169,7 @@ usage_hint_enabled = true
|
||||
usage_hint_text = "base hint"
|
||||
root_agent_usage_hint_text = "base root hint"
|
||||
subagent_usage_hint_text = "base subagent hint"
|
||||
tool_namespace = "base_agents"
|
||||
hide_spawn_agent_metadata = true
|
||||
non_code_mode_only = false
|
||||
|
||||
@@ -10176,6 +10182,7 @@ usage_hint_enabled = false
|
||||
usage_hint_text = "profile hint"
|
||||
root_agent_usage_hint_text = "profile root hint"
|
||||
subagent_usage_hint_text = "profile subagent hint"
|
||||
tool_namespace = "profile_agents"
|
||||
hide_spawn_agent_metadata = false
|
||||
non_code_mode_only = true
|
||||
"#,
|
||||
@@ -10204,6 +10211,10 @@ non_code_mode_only = true
|
||||
config.multi_agent_v2.subagent_usage_hint_text.as_deref(),
|
||||
Some("profile subagent hint")
|
||||
);
|
||||
assert_eq!(
|
||||
config.multi_agent_v2.tool_namespace.as_deref(),
|
||||
Some("profile_agents")
|
||||
);
|
||||
assert!(!config.multi_agent_v2.hide_spawn_agent_metadata);
|
||||
assert!(config.multi_agent_v2.non_code_mode_only);
|
||||
|
||||
@@ -10464,6 +10475,42 @@ default_wait_timeout_ms = 2500
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_rejects_invalid_tool_namespace() -> std::io::Result<()> {
|
||||
for (namespace, expected_message) in [
|
||||
(
|
||||
"bad namespace",
|
||||
"features.multi_agent_v2.tool_namespace must match ^[a-zA-Z0-9_-]+$",
|
||||
),
|
||||
(
|
||||
"functions",
|
||||
"features.multi_agent_v2.tool_namespace uses a reserved namespace: functions",
|
||||
),
|
||||
] {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
format!(
|
||||
r#"[features.multi_agent_v2]
|
||||
enabled = true
|
||||
tool_namespace = "{namespace}"
|
||||
"#
|
||||
),
|
||||
)?;
|
||||
|
||||
let err = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect_err("invalid multi_agent_v2 tool namespace should fail");
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||
assert_eq!(err.to_string(), expected_message);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_session_thread_cap_one_disallows_subagents() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -953,6 +953,7 @@ pub struct MultiAgentV2Config {
|
||||
pub usage_hint_text: Option<String>,
|
||||
pub root_agent_usage_hint_text: Option<String>,
|
||||
pub subagent_usage_hint_text: Option<String>,
|
||||
pub tool_namespace: Option<String>,
|
||||
pub hide_spawn_agent_metadata: bool,
|
||||
pub non_code_mode_only: bool,
|
||||
}
|
||||
@@ -969,6 +970,7 @@ impl Default for MultiAgentV2Config {
|
||||
usage_hint_text: None,
|
||||
root_agent_usage_hint_text: None,
|
||||
subagent_usage_hint_text: None,
|
||||
tool_namespace: None,
|
||||
hide_spawn_agent_metadata: false,
|
||||
non_code_mode_only: false,
|
||||
}
|
||||
@@ -2178,6 +2180,11 @@ fn resolve_multi_agent_v2_config(
|
||||
.or_else(|| base.and_then(|config| config.subagent_usage_hint_text.as_ref()))
|
||||
.cloned()
|
||||
.or(default.subagent_usage_hint_text);
|
||||
let tool_namespace = profile
|
||||
.and_then(|config| config.tool_namespace.as_ref())
|
||||
.or_else(|| base.and_then(|config| config.tool_namespace.as_ref()))
|
||||
.cloned()
|
||||
.or(default.tool_namespace);
|
||||
let hide_spawn_agent_metadata = profile
|
||||
.and_then(|config| config.hide_spawn_agent_metadata)
|
||||
.or_else(|| base.and_then(|config| config.hide_spawn_agent_metadata))
|
||||
@@ -2196,6 +2203,7 @@ fn resolve_multi_agent_v2_config(
|
||||
usage_hint_text,
|
||||
root_agent_usage_hint_text,
|
||||
subagent_usage_hint_text,
|
||||
tool_namespace,
|
||||
hide_spawn_agent_metadata,
|
||||
non_code_mode_only,
|
||||
}
|
||||
@@ -2290,6 +2298,69 @@ fn validate_multi_agent_v2_wait_timeout(label: &str, value: i64) -> std::io::Res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_multi_agent_v2_tool_namespace(namespace: Option<&str>) -> std::io::Result<()> {
|
||||
const LABEL: &str = "features.multi_agent_v2.tool_namespace";
|
||||
const MAX_LEN: usize = 64;
|
||||
const RESERVED_RESPONSES_NAMESPACES: &[&str] = &[
|
||||
"api_tool",
|
||||
"browser",
|
||||
"computer",
|
||||
"container",
|
||||
"file_search",
|
||||
"functions",
|
||||
"image_gen",
|
||||
"multi_tool_use",
|
||||
"python",
|
||||
"python_user_visible",
|
||||
"submodel_delegator",
|
||||
"terminal",
|
||||
"tool_search",
|
||||
"web",
|
||||
];
|
||||
|
||||
let Some(namespace) = namespace else {
|
||||
return Ok(());
|
||||
};
|
||||
if namespace.is_empty() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("{LABEL} must not be empty"),
|
||||
));
|
||||
}
|
||||
if namespace.trim() != namespace {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("{LABEL} must not have leading or trailing whitespace"),
|
||||
));
|
||||
}
|
||||
if !namespace
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
|
||||
{
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("{LABEL} must match ^[a-zA-Z0-9_-]+$"),
|
||||
));
|
||||
}
|
||||
if namespace.chars().count() > MAX_LEN {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("{LABEL} must be at most {MAX_LEN} characters"),
|
||||
));
|
||||
}
|
||||
if namespace == "mcp"
|
||||
|| namespace.starts_with("mcp__")
|
||||
|| RESERVED_RESPONSES_NAMESPACES.contains(&namespace)
|
||||
{
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("{LABEL} uses a reserved namespace: {namespace}"),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Config {
|
||||
#[cfg(test)]
|
||||
async fn load_from_base_config_with_overrides(
|
||||
@@ -2911,6 +2982,7 @@ impl Config {
|
||||
"features.multi_agent_v2.default_wait_timeout_ms must be at most features.multi_agent_v2.max_wait_timeout_ms",
|
||||
));
|
||||
}
|
||||
validate_multi_agent_v2_tool_namespace(multi_agent_v2.tool_namespace.as_deref())?;
|
||||
let agent_max_threads_from_config = cfg.agents.as_ref().and_then(|agents| agents.max_threads);
|
||||
let agent_max_threads = if features.enabled(Feature::MultiAgentV2) {
|
||||
if agent_max_threads_from_config.is_some() {
|
||||
|
||||
Reference in New Issue
Block a user