mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
multi-agent: move concurrency guidance into v2 usage hints (#27569)
## Why Native Codex currently teaches multi-agent concurrency through the `spawn_agent` tool description, while bridge-driven evals frame the same limit as a shared pool of active agent slots. That mismatch makes the model-facing story harder to reason about, especially because the tool-level wording does not make it explicit that the limit covers the whole agent team, including the current agent. This change gives native Codex the same mental model: tell the root agent and subagents how many active slots exist, and remove the separate `spawn_agent` limit wording. ## What changed - Extend the built-in `multi_agent_v2` root and subagent usage hints with shared-slot guidance derived from the resolved `max_concurrent_threads_per_session` value. - Keep the complete default hints in `MultiAgentV2Config` so initial context and forked histories consume the same canonical strings. - Drop the redundant `spawn_agent` description text and remove the now-unused limit plumbing from the tool spec path. ## Testing - `just test -p codex-core usage_hint` - `just test -p codex-core multi_agent_v2_default_session_thread_cap_counts_root` - `just test -p codex-core multi_agent_v2_default_usage_hints_use_configured_thread_cap` - `just test -p codex-core spawn_agent_tool_v2_requires_task_name_and_lists_visible_models` - `just test -p codex-core multi_agent_feature_selects_one_agent_tool_family`
This commit is contained in:
@@ -9831,10 +9831,7 @@ enabled = true
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert_eq!(config.multi_agent_v2.max_concurrent_threads_per_session, 4);
|
||||
assert_eq!(config.multi_agent_v2.min_wait_timeout_ms, 10_000);
|
||||
assert_eq!(config.multi_agent_v2.max_wait_timeout_ms, 3_600_000);
|
||||
assert_eq!(config.multi_agent_v2.default_wait_timeout_ms, 30_000);
|
||||
assert_eq!(config.multi_agent_v2, MultiAgentV2Config::default());
|
||||
assert_eq!(
|
||||
(
|
||||
config.agent_max_threads,
|
||||
@@ -9842,36 +9839,32 @@ enabled = true
|
||||
),
|
||||
(None, Some(3))
|
||||
);
|
||||
assert_eq!(
|
||||
config.multi_agent_v2.root_agent_usage_hint_text.as_deref(),
|
||||
Some(DEFAULT_MULTI_AGENT_V2_ROOT_AGENT_USAGE_HINT_TEXT)
|
||||
);
|
||||
assert!(
|
||||
!config
|
||||
.multi_agent_v2
|
||||
.root_agent_usage_hint_text
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.contains("maximum concurrency"),
|
||||
);
|
||||
assert_eq!(
|
||||
config.multi_agent_v2.subagent_usage_hint_text.as_deref(),
|
||||
Some(DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT)
|
||||
);
|
||||
assert!(
|
||||
!config
|
||||
.multi_agent_v2
|
||||
.subagent_usage_hint_text
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.contains("maximum concurrency"),
|
||||
);
|
||||
assert!(config.multi_agent_v2.hide_spawn_agent_metadata);
|
||||
assert!(config.multi_agent_v2.non_code_mode_only);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_agent_v2_default_usage_hints_use_configured_thread_cap() {
|
||||
let config_toml = toml::from_str(
|
||||
r#"[features.multi_agent_v2]
|
||||
enabled = true
|
||||
max_concurrent_threads_per_session = 17
|
||||
"#,
|
||||
)
|
||||
.expect("multi-agent v2 config should parse");
|
||||
|
||||
let config = resolve_multi_agent_v2_config(&config_toml);
|
||||
let concurrency_guidance = "There are 17 available concurrency slots, meaning that up to 17 agents can be active at once, including you.";
|
||||
assert!(
|
||||
[
|
||||
config.root_agent_usage_hint_text,
|
||||
config.subagent_usage_hint_text,
|
||||
]
|
||||
.into_iter()
|
||||
.all(|hint| hint.is_some_and(|hint| hint.ends_with(concurrency_guidance)))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_empty_usage_hint_overrides_clear_default_hints() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -233,6 +233,13 @@ Payload:
|
||||
```
|
||||
You may also see them addressed as to=/root/..., which indicates your identity is /root/...
|
||||
"#;
|
||||
|
||||
fn default_multi_agent_v2_usage_hint_text(usage_hint_text: &str, max_concurrency: usize) -> String {
|
||||
format!(
|
||||
"{usage_hint_text}\nThere are {max_concurrency} available concurrency slots, meaning that up to {max_concurrency} agents can be active at once, including you."
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) const HARD_MIN_MULTI_AGENT_V2_TIMEOUT_MS: i64 = 0;
|
||||
pub(crate) const HARD_MAX_MULTI_AGENT_V2_TIMEOUT_MS: i64 =
|
||||
DEFAULT_MULTI_AGENT_V2_MAX_WAIT_TIMEOUT_MS;
|
||||
@@ -1055,22 +1062,23 @@ pub struct MultiAgentV2Config {
|
||||
pub non_code_mode_only: bool,
|
||||
}
|
||||
|
||||
impl Default for MultiAgentV2Config {
|
||||
fn default() -> Self {
|
||||
impl MultiAgentV2Config {
|
||||
fn defaults_for_max_concurrency(max_concurrent_threads_per_session: usize) -> Self {
|
||||
Self {
|
||||
max_concurrent_threads_per_session:
|
||||
DEFAULT_MULTI_AGENT_V2_MAX_CONCURRENT_THREADS_PER_SESSION,
|
||||
max_concurrent_threads_per_session,
|
||||
min_wait_timeout_ms: DEFAULT_MULTI_AGENT_V2_MIN_WAIT_TIMEOUT_MS,
|
||||
max_wait_timeout_ms: DEFAULT_MULTI_AGENT_V2_MAX_WAIT_TIMEOUT_MS,
|
||||
default_wait_timeout_ms: DEFAULT_MULTI_AGENT_V2_DEFAULT_WAIT_TIMEOUT_MS,
|
||||
usage_hint_enabled: true,
|
||||
usage_hint_text: None,
|
||||
root_agent_usage_hint_text: Some(
|
||||
DEFAULT_MULTI_AGENT_V2_ROOT_AGENT_USAGE_HINT_TEXT.to_string(),
|
||||
),
|
||||
subagent_usage_hint_text: Some(
|
||||
DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT.to_string(),
|
||||
),
|
||||
root_agent_usage_hint_text: Some(default_multi_agent_v2_usage_hint_text(
|
||||
DEFAULT_MULTI_AGENT_V2_ROOT_AGENT_USAGE_HINT_TEXT,
|
||||
max_concurrent_threads_per_session,
|
||||
)),
|
||||
subagent_usage_hint_text: Some(default_multi_agent_v2_usage_hint_text(
|
||||
DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT,
|
||||
max_concurrent_threads_per_session,
|
||||
)),
|
||||
tool_namespace: None,
|
||||
hide_spawn_agent_metadata: true,
|
||||
non_code_mode_only: true,
|
||||
@@ -1078,6 +1086,14 @@ impl Default for MultiAgentV2Config {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MultiAgentV2Config {
|
||||
fn default() -> Self {
|
||||
Self::defaults_for_max_concurrency(
|
||||
DEFAULT_MULTI_AGENT_V2_MAX_CONCURRENT_THREADS_PER_SESSION,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum TerminalResizeReflowMaxRows {
|
||||
/// Use the runtime terminal detector to choose a scrollback-sized cap.
|
||||
@@ -2313,11 +2329,11 @@ fn resolve_code_mode_config(config_toml: &ConfigToml) -> CodeModeConfig {
|
||||
|
||||
fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config {
|
||||
let base = multi_agent_v2_toml_config(config_toml.features.as_ref());
|
||||
let default = MultiAgentV2Config::default();
|
||||
|
||||
let max_concurrent_threads_per_session = base
|
||||
.and_then(|config| config.max_concurrent_threads_per_session)
|
||||
.unwrap_or(default.max_concurrent_threads_per_session);
|
||||
.unwrap_or(DEFAULT_MULTI_AGENT_V2_MAX_CONCURRENT_THREADS_PER_SESSION);
|
||||
let default =
|
||||
MultiAgentV2Config::defaults_for_max_concurrency(max_concurrent_threads_per_session);
|
||||
let min_wait_timeout_ms = base
|
||||
.and_then(|config| config.min_wait_timeout_ms)
|
||||
.unwrap_or(default.min_wait_timeout_ms);
|
||||
|
||||
@@ -26,7 +26,6 @@ pub struct SpawnAgentToolOptions {
|
||||
pub hide_agent_type_model_reasoning: bool,
|
||||
pub include_usage_hint: bool,
|
||||
pub usage_hint_text: Option<String>,
|
||||
pub max_concurrent_threads_per_session: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -102,7 +101,6 @@ pub fn create_spawn_agent_tool_v2(options: SpawnAgentToolOptions) -> ToolSpec {
|
||||
inherited_model_guidance,
|
||||
options.include_usage_hint,
|
||||
options.usage_hint_text,
|
||||
options.max_concurrent_threads_per_session,
|
||||
),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
@@ -722,17 +720,9 @@ fn spawn_agent_tool_description_v2(
|
||||
inherited_model_guidance: Option<&str>,
|
||||
include_usage_hint: bool,
|
||||
usage_hint_text: Option<String>,
|
||||
max_concurrent_threads_per_session: Option<usize>,
|
||||
) -> String {
|
||||
let agent_role_guidance = available_models_description.unwrap_or_default();
|
||||
let inherited_model_guidance = inherited_model_guidance.unwrap_or_default();
|
||||
let concurrency_guidance = max_concurrent_threads_per_session
|
||||
.map(|limit| {
|
||||
format!(
|
||||
"This session is configured with `max_concurrent_threads_per_session = {limit}` for concurrently open agent threads."
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let tool_description = format!(
|
||||
r#"
|
||||
@@ -743,8 +733,7 @@ The spawned agent will have the same tools as you and the ability to spawn its o
|
||||
{inherited_model_guidance}
|
||||
Only call this tool for a concrete, bounded subtask that can run independently alongside useful local work; otherwise continue locally.
|
||||
It will be able to send you and other running agents messages, and its final answer will be provided to you when it finishes.
|
||||
The new agent's canonical task name will be provided to it along with the message.
|
||||
{concurrency_guidance}"#
|
||||
The new agent's canonical task name will be provided to it along with the message."#
|
||||
);
|
||||
|
||||
if !include_usage_hint {
|
||||
|
||||
@@ -47,7 +47,6 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
|
||||
hide_agent_type_model_reasoning: false,
|
||||
include_usage_hint: true,
|
||||
usage_hint_text: None,
|
||||
max_concurrent_threads_per_session: Some(4),
|
||||
});
|
||||
|
||||
let ToolSpec::Function(ResponsesApiTool {
|
||||
@@ -69,7 +68,7 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
|
||||
.expect("spawn_agent should use object params");
|
||||
assert!(description.contains("Spawns an agent to work on the specified task."));
|
||||
assert!(description.contains("The spawned agent will have the same tools as you"));
|
||||
assert!(description.contains("`max_concurrent_threads_per_session = 4`"));
|
||||
assert!(!description.contains("max_concurrent_threads_per_session"));
|
||||
assert!(description.contains(SPAWN_AGENT_INHERITED_MODEL_GUIDANCE));
|
||||
assert!(
|
||||
description
|
||||
@@ -124,7 +123,6 @@ fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() {
|
||||
hide_agent_type_model_reasoning: false,
|
||||
include_usage_hint: true,
|
||||
usage_hint_text: None,
|
||||
max_concurrent_threads_per_session: None,
|
||||
});
|
||||
|
||||
let ToolSpec::Namespace(namespace) = tool else {
|
||||
@@ -182,7 +180,6 @@ fn spawn_agent_tool_caps_visible_model_summaries() {
|
||||
hide_agent_type_model_reasoning: false,
|
||||
include_usage_hint: true,
|
||||
usage_hint_text: None,
|
||||
max_concurrent_threads_per_session: Some(4),
|
||||
});
|
||||
|
||||
let ToolSpec::Function(ResponsesApiTool { description, .. }) = tool else {
|
||||
@@ -227,7 +224,6 @@ fn spawn_agent_tool_hides_service_tier_with_spawn_metadata() {
|
||||
hide_agent_type_model_reasoning: true,
|
||||
include_usage_hint: true,
|
||||
usage_hint_text: None,
|
||||
max_concurrent_threads_per_session: Some(4),
|
||||
});
|
||||
|
||||
let ToolSpec::Function(ResponsesApiTool {
|
||||
|
||||
@@ -388,15 +388,6 @@ fn wait_agent_timeout_options(turn_context: &TurnContext) -> WaitAgentTimeoutOpt
|
||||
}
|
||||
}
|
||||
|
||||
fn max_concurrent_threads_per_session(turn_context: &TurnContext) -> Option<usize> {
|
||||
multi_agent_v2_enabled(turn_context).then_some(
|
||||
turn_context
|
||||
.config
|
||||
.multi_agent_v2
|
||||
.max_concurrent_threads_per_session,
|
||||
)
|
||||
}
|
||||
|
||||
fn agent_type_description(
|
||||
turn_context: &TurnContext,
|
||||
default_agent_type_description: &str,
|
||||
@@ -729,9 +720,6 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mu
|
||||
.hide_spawn_agent_metadata,
|
||||
include_usage_hint: turn_context.config.multi_agent_v2.usage_hint_enabled,
|
||||
usage_hint_text: turn_context.config.multi_agent_v2.usage_hint_text.clone(),
|
||||
max_concurrent_threads_per_session: max_concurrent_threads_per_session(
|
||||
turn_context,
|
||||
),
|
||||
}),
|
||||
tool_namespace,
|
||||
),
|
||||
@@ -776,9 +764,6 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mu
|
||||
hide_agent_type_model_reasoning: false,
|
||||
include_usage_hint: turn_context.config.multi_agent_v2.usage_hint_enabled,
|
||||
usage_hint_text: turn_context.config.multi_agent_v2.usage_hint_text.clone(),
|
||||
max_concurrent_threads_per_session: max_concurrent_threads_per_session(
|
||||
turn_context,
|
||||
),
|
||||
}),
|
||||
exposure,
|
||||
);
|
||||
|
||||
@@ -1015,7 +1015,7 @@ async fn multi_agent_feature_selects_one_agent_tool_family() {
|
||||
ToolSpec::Function(tool) => tool.description.as_str(),
|
||||
other => panic!("expected spawn_agent function spec, got {other:?}"),
|
||||
};
|
||||
assert!(spawn_agent_description.contains("max_concurrent_threads_per_session = 17"));
|
||||
assert!(!spawn_agent_description.contains("max_concurrent_threads_per_session"));
|
||||
|
||||
let direct_model_only = probe(|turn| {
|
||||
set_features(
|
||||
|
||||
Reference in New Issue
Block a user