mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
multi_agent_v2: move thread cap into feature config (#19792)
## Why `features.multi_agent_v2.max_concurrent_threads_per_session` is meant to be the MultiAgentV2-specific session thread cap: it counts the root thread and all open subagent threads. The previous implementation kept this surface tied to `agents.max_threads`, which made it a global subagent-only cap and allowed the legacy setting to coexist with MultiAgentV2. ## What Changed - Added `max_concurrent_threads_per_session` to `[features.multi_agent_v2]` with default `4`. - Removed the `[agents] max_concurrent_threads_per_session` alias to `agents.max_threads`. - When MultiAgentV2 is enabled, reject `agents.max_threads` and derive the existing internal subagent slot limit as `max_concurrent_threads_per_session - 1`. - Regenerated `core/config.schema.json` and added coverage for the new config semantics. ## Result ``` ➜ codex git:(jif/clean-multi-agent-v2-config) codex -c features.multi_agent_v2.enabled=true -c features.multi_agent_v2.max_concurrent_threads_per_session=3 ╭────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.0.0) │ │ │ │ model: gpt-5.5 xhigh fast /model to change │ │ directory: ~/code/codex │ ╰────────────────────────────────────────────────────╯ Tip: Update Required - This version will no longer be supported starting May 8th. Please upgrade to the latest version (https://github.com/openai/codex/releases/latest) using your preferred package manager. › Can you try to spawn 4 agents • I’ll try to start four lightweight agents at once and report exactly what the runtime accepts. • Spawned Russell [no-apps] (gpt-5.5 xhigh) └ Spawn probe 1: reply briefly that you started, then wait for further instructions. Do not do any repo work. • Spawned Descartes [no-apps] (gpt-5.5 xhigh) └ Spawn probe 2: reply briefly that you started, then wait for further instructions. Do not do any repo work. • Agent spawn failed └ Spawn probe 3: reply briefly that you started, then wait for further instructions. Do not do any repo work. • Agent spawn failed └ Spawn probe 4: reply briefly that you started, then wait for further instructions. Do not do any repo work. • The runtime accepted the first two and rejected the next two with agent thread limit reached. I’m checking whether the two accepted probes have returned cleanly, then I’ll close them if needed. ``` --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
4f1d5f00f0
commit
f8c527e529
@@ -8,18 +8,11 @@ struct ConfigKeyAlias {
|
||||
canonical_key: &'static str,
|
||||
}
|
||||
|
||||
const CONFIG_KEY_ALIASES: &[ConfigKeyAlias] = &[
|
||||
ConfigKeyAlias {
|
||||
table_path: &["memories"],
|
||||
legacy_key: "no_memories_if_mcp_or_web_search",
|
||||
canonical_key: "disable_on_external_context",
|
||||
},
|
||||
ConfigKeyAlias {
|
||||
table_path: &["agents"],
|
||||
legacy_key: "max_concurrent_threads_per_session",
|
||||
canonical_key: "max_threads",
|
||||
},
|
||||
];
|
||||
const CONFIG_KEY_ALIASES: &[ConfigKeyAlias] = &[ConfigKeyAlias {
|
||||
table_path: &["memories"],
|
||||
legacy_key: "no_memories_if_mcp_or_web_search",
|
||||
canonical_key: "disable_on_external_context",
|
||||
}];
|
||||
|
||||
pub(crate) fn normalize_key_aliases(path: &[String], table: &mut TomlMap<String, TomlValue>) {
|
||||
for alias in CONFIG_KEY_ALIASES {
|
||||
|
||||
@@ -1310,6 +1310,11 @@
|
||||
"hide_spawn_agent_metadata": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"max_concurrent_threads_per_session": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": "integer"
|
||||
},
|
||||
"usage_hint_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -7348,6 +7348,7 @@ async fn multi_agent_v2_config_from_feature_table() -> std::io::Result<()> {
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"[features.multi_agent_v2]
|
||||
enabled = true
|
||||
max_concurrent_threads_per_session = 5
|
||||
usage_hint_enabled = false
|
||||
usage_hint_text = "Custom delegation guidance."
|
||||
hide_spawn_agent_metadata = true
|
||||
@@ -7361,6 +7362,8 @@ hide_spawn_agent_metadata = true
|
||||
.await?;
|
||||
|
||||
assert!(config.features.enabled(Feature::MultiAgentV2));
|
||||
assert_eq!(config.multi_agent_v2.max_concurrent_threads_per_session, 5);
|
||||
assert_eq!(config.agent_max_threads, Some(4));
|
||||
assert!(!config.multi_agent_v2.usage_hint_enabled);
|
||||
assert_eq!(
|
||||
config.multi_agent_v2.usage_hint_text.as_deref(),
|
||||
@@ -7379,11 +7382,13 @@ async fn profile_multi_agent_v2_config_overrides_base() -> std::io::Result<()> {
|
||||
r#"profile = "no_hint"
|
||||
|
||||
[features.multi_agent_v2]
|
||||
max_concurrent_threads_per_session = 4
|
||||
usage_hint_enabled = true
|
||||
usage_hint_text = "base hint"
|
||||
hide_spawn_agent_metadata = true
|
||||
|
||||
[profiles.no_hint.features.multi_agent_v2]
|
||||
max_concurrent_threads_per_session = 6
|
||||
usage_hint_enabled = false
|
||||
usage_hint_text = "profile hint"
|
||||
hide_spawn_agent_metadata = false
|
||||
@@ -7396,6 +7401,7 @@ hide_spawn_agent_metadata = false
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert_eq!(config.multi_agent_v2.max_concurrent_threads_per_session, 6);
|
||||
assert!(!config.multi_agent_v2.usage_hint_enabled);
|
||||
assert_eq!(
|
||||
config.multi_agent_v2.usage_hint_text.as_deref(),
|
||||
@@ -7406,6 +7412,80 @@ hide_spawn_agent_metadata = false
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_default_session_thread_cap_counts_root() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"[features.multi_agent_v2]
|
||||
enabled = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
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?;
|
||||
|
||||
assert_eq!(config.multi_agent_v2.max_concurrent_threads_per_session, 4);
|
||||
assert_eq!(config.agent_max_threads, Some(3));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_rejects_agents_max_threads() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"[features.multi_agent_v2]
|
||||
enabled = true
|
||||
|
||||
[agents]
|
||||
max_threads = 3
|
||||
"#,
|
||||
)?;
|
||||
|
||||
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("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 multi_agent_v2 is enabled"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_session_thread_cap_one_disallows_subagents() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"[features.multi_agent_v2]
|
||||
enabled = true
|
||||
max_concurrent_threads_per_session = 1
|
||||
"#,
|
||||
)?;
|
||||
|
||||
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?;
|
||||
|
||||
assert_eq!(config.multi_agent_v2.max_concurrent_threads_per_session, 1);
|
||||
assert_eq!(config.agent_max_threads, Some(0));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn feature_requirements_normalize_runtime_feature_mutations() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -131,6 +131,7 @@ pub use codex_git_utils::GhostSnapshotConfig;
|
||||
/// the context window.
|
||||
pub(crate) const AGENTS_MD_MAX_BYTES: usize = 32 * 1024; // 32 KiB
|
||||
pub(crate) const DEFAULT_AGENT_MAX_THREADS: Option<usize> = Some(6);
|
||||
pub(crate) const DEFAULT_MULTI_AGENT_V2_MAX_CONCURRENT_THREADS_PER_SESSION: usize = 4;
|
||||
pub(crate) const DEFAULT_AGENT_MAX_DEPTH: i32 = 1;
|
||||
pub(crate) const DEFAULT_AGENT_JOB_MAX_RUNTIME_SECONDS: Option<u64> = None;
|
||||
const LOCAL_DEV_BUILD_VERSION: &str = "0.0.0";
|
||||
@@ -704,6 +705,7 @@ pub struct Config {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MultiAgentV2Config {
|
||||
pub max_concurrent_threads_per_session: usize,
|
||||
pub usage_hint_enabled: bool,
|
||||
pub usage_hint_text: Option<String>,
|
||||
pub hide_spawn_agent_metadata: bool,
|
||||
@@ -712,6 +714,8 @@ pub struct MultiAgentV2Config {
|
||||
impl Default for MultiAgentV2Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_concurrent_threads_per_session:
|
||||
DEFAULT_MULTI_AGENT_V2_MAX_CONCURRENT_THREADS_PER_SESSION,
|
||||
usage_hint_enabled: true,
|
||||
usage_hint_text: None,
|
||||
hide_spawn_agent_metadata: false,
|
||||
@@ -1579,6 +1583,10 @@ fn resolve_multi_agent_v2_config(
|
||||
let profile = multi_agent_v2_toml_config(config_profile.features.as_ref());
|
||||
let default = MultiAgentV2Config::default();
|
||||
|
||||
let max_concurrent_threads_per_session = profile
|
||||
.and_then(|config| config.max_concurrent_threads_per_session)
|
||||
.or_else(|| base.and_then(|config| config.max_concurrent_threads_per_session))
|
||||
.unwrap_or(default.max_concurrent_threads_per_session);
|
||||
let usage_hint_enabled = profile
|
||||
.and_then(|config| config.usage_hint_enabled)
|
||||
.or_else(|| base.and_then(|config| config.usage_hint_enabled))
|
||||
@@ -1594,6 +1602,7 @@ fn resolve_multi_agent_v2_config(
|
||||
.unwrap_or(default.hide_spawn_agent_metadata);
|
||||
|
||||
MultiAgentV2Config {
|
||||
max_concurrent_threads_per_session,
|
||||
usage_hint_enabled,
|
||||
usage_hint_text,
|
||||
hide_spawn_agent_metadata,
|
||||
@@ -2078,17 +2087,35 @@ impl Config {
|
||||
|
||||
let history = cfg.history.unwrap_or_default();
|
||||
|
||||
let agent_max_threads = cfg
|
||||
.agents
|
||||
.as_ref()
|
||||
.and_then(|agents| agents.max_threads)
|
||||
.or(DEFAULT_AGENT_MAX_THREADS);
|
||||
if agent_max_threads == Some(0) {
|
||||
if multi_agent_v2.max_concurrent_threads_per_session == 0 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"agents.max_threads must be at least 1",
|
||||
"features.multi_agent_v2.max_concurrent_threads_per_session must be at least 1",
|
||||
));
|
||||
}
|
||||
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() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"agents.max_threads cannot be set when multi_agent_v2 is enabled",
|
||||
));
|
||||
}
|
||||
Some(
|
||||
multi_agent_v2
|
||||
.max_concurrent_threads_per_session
|
||||
.saturating_sub(1),
|
||||
)
|
||||
} else {
|
||||
let agent_max_threads = agent_max_threads_from_config.or(DEFAULT_AGENT_MAX_THREADS);
|
||||
if agent_max_threads == Some(0) {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"agents.max_threads must be at least 1",
|
||||
));
|
||||
}
|
||||
agent_max_threads
|
||||
};
|
||||
let agent_max_depth = cfg
|
||||
.agents
|
||||
.as_ref()
|
||||
|
||||
@@ -194,7 +194,12 @@ impl TurnContext {
|
||||
.with_spawn_agent_usage_hint_text(config.multi_agent_v2.usage_hint_text.clone())
|
||||
.with_hide_spawn_agent_metadata(config.multi_agent_v2.hide_spawn_agent_metadata)
|
||||
.with_goal_tools_allowed(self.tools_config.goal_tools)
|
||||
.with_max_concurrent_threads_per_session(config.agent_max_threads)
|
||||
.with_max_concurrent_threads_per_session(
|
||||
config
|
||||
.features
|
||||
.enabled(Feature::MultiAgentV2)
|
||||
.then_some(config.multi_agent_v2.max_concurrent_threads_per_session),
|
||||
)
|
||||
.with_agent_type_description(crate::agent::role::spawn_tool_spec::build(
|
||||
&config.agent_roles,
|
||||
));
|
||||
@@ -459,7 +464,16 @@ impl Session {
|
||||
.with_spawn_agent_usage_hint_text(per_turn_config.multi_agent_v2.usage_hint_text.clone())
|
||||
.with_hide_spawn_agent_metadata(per_turn_config.multi_agent_v2.hide_spawn_agent_metadata)
|
||||
.with_goal_tools_allowed(goal_tools_supported)
|
||||
.with_max_concurrent_threads_per_session(per_turn_config.agent_max_threads)
|
||||
.with_max_concurrent_threads_per_session(
|
||||
per_turn_config
|
||||
.features
|
||||
.enabled(Feature::MultiAgentV2)
|
||||
.then_some(
|
||||
per_turn_config
|
||||
.multi_agent_v2
|
||||
.max_concurrent_threads_per_session,
|
||||
),
|
||||
)
|
||||
.with_agent_type_description(crate::agent::role::spawn_tool_spec::build(
|
||||
&per_turn_config.agent_roles,
|
||||
));
|
||||
|
||||
@@ -534,6 +534,11 @@ async fn build_runner_options(
|
||||
"agent depth limit reached; this session cannot spawn more subagents".to_string(),
|
||||
));
|
||||
}
|
||||
if turn.config.agent_max_threads == Some(0) {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"agent thread limit reached; this session cannot spawn more subagents".to_string(),
|
||||
));
|
||||
}
|
||||
let max_concurrency =
|
||||
normalize_concurrency(requested_concurrency, turn.config.agent_max_threads);
|
||||
let base_instructions = session.get_base_instructions().await;
|
||||
|
||||
@@ -9,6 +9,9 @@ pub struct MultiAgentV2ConfigToml {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(range(min = 1))]
|
||||
pub max_concurrent_threads_per_session: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub usage_hint_enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub usage_hint_text: Option<String>,
|
||||
|
||||
@@ -396,6 +396,7 @@ fn multi_agent_v2_feature_config_deserializes_table() {
|
||||
r#"
|
||||
[multi_agent_v2]
|
||||
enabled = true
|
||||
max_concurrent_threads_per_session = 4
|
||||
usage_hint_enabled = false
|
||||
usage_hint_text = "Custom delegation guidance."
|
||||
hide_spawn_agent_metadata = true
|
||||
@@ -411,6 +412,7 @@ hide_spawn_agent_metadata = true
|
||||
features.multi_agent_v2,
|
||||
Some(crate::FeatureToml::Config(crate::MultiAgentV2ConfigToml {
|
||||
enabled: Some(true),
|
||||
max_concurrent_threads_per_session: Some(4),
|
||||
usage_hint_enabled: Some(false),
|
||||
usage_hint_text: Some("Custom delegation guidance.".to_string()),
|
||||
hide_spawn_agent_metadata: Some(true),
|
||||
@@ -442,6 +444,7 @@ usage_hint_enabled = false
|
||||
features_toml.multi_agent_v2,
|
||||
Some(crate::FeatureToml::Config(crate::MultiAgentV2ConfigToml {
|
||||
enabled: None,
|
||||
max_concurrent_threads_per_session: None,
|
||||
usage_hint_enabled: Some(false),
|
||||
usage_hint_text: None,
|
||||
hide_spawn_agent_metadata: None,
|
||||
|
||||
@@ -82,7 +82,7 @@ pub enum CodexErr {
|
||||
ContextWindowExceeded,
|
||||
#[error("no thread with id: {0}")]
|
||||
ThreadNotFound(ThreadId),
|
||||
#[error("agent thread limit reached (max {max_threads})")]
|
||||
#[error("agent thread limit reached")]
|
||||
AgentLimitReached { max_threads: usize },
|
||||
#[error("session configured event was not the first event in the stream")]
|
||||
SessionConfiguredNotFirstEvent,
|
||||
|
||||
Reference in New Issue
Block a user