feat: catalog multi-agent v2 config (#26254)

## Why

Model metadata can now select multi-agent v2 even when a user has not
enabled `features.multi_agent_v2` in their config. Some existing configs
still set the legacy `agents.max_threads` knob for v1 multi-agent
behavior, so treating every v2 runtime as incompatible with
`agents.max_threads` would break users whose only v2 signal came from
the model catalog.

The incompatible configuration is specifically enabling
`features.multi_agent_v2` while also setting `agents.max_threads`.
Catalog-forced v2 should use the v2 concurrency setting and ignore the
legacy v1 cap instead of rejecting the config.

## What changed

- Split config validation from runtime concurrency calculation:
`effective_agent_max_threads` now just returns the effective cap for the
resolved multi-agent runtime.
- Added explicit validation for `features.multi_agent_v2` +
`agents.max_threads` at session startup.
- Preserved catalog-selected v2 behavior when `features.multi_agent_v2`
is disabled, so existing configs with `agents.max_threads` keep
starting.
- Updated model-runtime selector coverage so a catalog v2 model still
exposes v2 tools even when `agents.max_threads` is set and the config
flag is disabled.

## Validation

- `cargo check -p codex-core --lib`
- `just test -p codex-core --lib -E
"test(multi_agent_v2_feature_rejects_agents_max_threads) |
test(catalog_v2_allows_agents_max_threads_when_feature_disabled)"`
This commit is contained in:
jif
2026-06-04 00:24:40 +02:00
committed by GitHub
parent 2ca3810005
commit 11bceb8f8b
6 changed files with 62 additions and 36 deletions
+38 -6
View File
@@ -9836,7 +9836,7 @@ non_code_mode_only = true
assert_eq!(
(
config.agent_max_threads,
config.effective_agent_max_threads(MultiAgentVersion::V2)?
config.effective_agent_max_threads(MultiAgentVersion::V2)
),
(None, Some(4))
);
@@ -9886,7 +9886,7 @@ enabled = true
assert_eq!(
(
config.agent_max_threads,
config.effective_agent_max_threads(MultiAgentVersion::V2)?
config.effective_agent_max_threads(MultiAgentVersion::V2)
),
(None, Some(3))
);
@@ -9945,7 +9945,7 @@ subagent_usage_hint_text = ""
}
#[tokio::test]
async fn multi_agent_v2_rejects_agents_max_threads() -> std::io::Result<()> {
async fn multi_agent_v2_feature_rejects_agents_max_threads() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
@@ -9963,13 +9963,45 @@ max_threads = 3
.build()
.await?;
let err = config
.effective_agent_max_threads(MultiAgentVersion::V2)
.validate_multi_agent_v2_config()
.expect_err("agents.max_threads should conflict with multi_agent_v2");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(
err.to_string(),
"agents.max_threads cannot be set when the multi-agent runtime is v2"
"agents.max_threads cannot be set when features.multi_agent_v2 is enabled"
);
assert_eq!(
config.effective_agent_max_threads(MultiAgentVersion::V2),
Some(3)
);
Ok(())
}
#[tokio::test]
async fn catalog_v2_allows_agents_max_threads_when_feature_disabled() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"[features.multi_agent_v2]
enabled = false
[agents]
max_threads = 3
"#,
)?;
let config = 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?;
config.validate_multi_agent_v2_config()?;
assert_eq!(
config.effective_agent_max_threads(MultiAgentVersion::V2),
Some(3)
);
Ok(())
@@ -10231,7 +10263,7 @@ max_concurrent_threads_per_session = 1
assert_eq!(
(
config.agent_max_threads,
config.effective_agent_max_threads(MultiAgentVersion::V2)?
config.effective_agent_max_threads(MultiAgentVersion::V2)
),
(None, Some(0))
);
+18 -15
View File
@@ -1286,26 +1286,29 @@ impl Config {
}
}
pub(crate) fn validate_multi_agent_v2_config(&self) -> std::io::Result<()> {
if self.features.enabled(Feature::MultiAgentV2) && self.agent_max_threads.is_some() {
Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"agents.max_threads cannot be set when features.multi_agent_v2 is enabled",
))
} else {
Ok(())
}
}
pub(crate) fn effective_agent_max_threads(
&self,
multi_agent_version: MultiAgentVersion,
) -> std::io::Result<Option<usize>> {
) -> Option<usize> {
match multi_agent_version {
MultiAgentVersion::V2 => {
if self.agent_max_threads.is_some() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"agents.max_threads cannot be set when the multi-agent runtime is v2",
));
}
Ok(Some(
self.multi_agent_v2
.max_concurrent_threads_per_session
.saturating_sub(1),
))
}
MultiAgentVersion::V2 => Some(
self.multi_agent_v2
.max_concurrent_threads_per_session
.saturating_sub(1),
),
MultiAgentVersion::Disabled | MultiAgentVersion::V1 => {
Ok(self.agent_max_threads.or(DEFAULT_AGENT_MAX_THREADS))
self.agent_max_threads.or(DEFAULT_AGENT_MAX_THREADS)
}
}
}