mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
@@ -231,9 +231,7 @@ impl AgentControl {
|
||||
&config,
|
||||
)
|
||||
.await;
|
||||
let agent_max_threads = config
|
||||
.effective_agent_max_threads(multi_agent_version)
|
||||
.map_err(|err| CodexErr::InvalidRequest(err.to_string()))?;
|
||||
let agent_max_threads = config.effective_agent_max_threads(multi_agent_version);
|
||||
let mut reservation = self.state.reserve_spawn_slot(agent_max_threads)?;
|
||||
let inheritance = SpawnAgentThreadInheritance {
|
||||
shell_snapshot: self
|
||||
@@ -632,9 +630,7 @@ impl AgentControl {
|
||||
&config,
|
||||
)
|
||||
.await;
|
||||
let agent_max_threads = config
|
||||
.effective_agent_max_threads(multi_agent_version)
|
||||
.map_err(|err| CodexErr::InvalidRequest(err.to_string()))?;
|
||||
let agent_max_threads = config.effective_agent_max_threads(multi_agent_version);
|
||||
let mut reservation = self.state.reserve_spawn_slot(agent_max_threads)?;
|
||||
let (session_source, agent_metadata) = match session_source {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
|
||||
@@ -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))
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,11 +557,8 @@ impl Codex {
|
||||
.await;
|
||||
let multi_agent_version =
|
||||
resolve_multi_agent_version(&conversation_history, inherited_multi_agent_version);
|
||||
let startup_multi_agent_version = multi_agent_version
|
||||
.or(model_info.multi_agent_version)
|
||||
.unwrap_or_else(|| config.multi_agent_version_from_features());
|
||||
let _ = config
|
||||
.effective_agent_max_threads(startup_multi_agent_version)
|
||||
config
|
||||
.validate_multi_agent_v2_config()
|
||||
.map_err(|err| CodexErr::InvalidRequest(err.to_string()))?;
|
||||
let base_instructions = config
|
||||
.base_instructions
|
||||
|
||||
@@ -116,10 +116,7 @@ async fn build_runner_options(
|
||||
"multi-agent runtime is disabled; this session cannot spawn workers".to_string(),
|
||||
));
|
||||
}
|
||||
let agent_max_threads = turn
|
||||
.config
|
||||
.effective_agent_max_threads(multi_agent_version)
|
||||
.map_err(|err| FunctionCallError::Fatal(err.to_string()))?;
|
||||
let agent_max_threads = turn.config.effective_agent_max_threads(multi_agent_version);
|
||||
if agent_max_threads == Some(0) {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"agent thread limit reached; this session cannot spawn more subagents".to_string(),
|
||||
|
||||
@@ -190,6 +190,7 @@ async fn remote_multi_agent_selector_overrides_feature_flags() -> Result<()> {
|
||||
let mut v2_model = remote_model("test-multi-agent-v2");
|
||||
v2_model.multi_agent_version = Some(MultiAgentVersion::V2);
|
||||
let v2_body = response_body_for_remote_model(v2_model, |config| {
|
||||
config.agent_max_threads = Some(3);
|
||||
config
|
||||
.features
|
||||
.enable(Feature::Collab)
|
||||
|
||||
Reference in New Issue
Block a user