diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index 67b5668b1..1f58fb437 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -475,6 +475,7 @@ impl AgentControl { } if preserve_reference_context_item && config.features.enabled(Feature::MultiAgentV2) + && config.multi_agent_v2.usage_hint_enabled && let Some(subagent_usage_hint_text) = config.multi_agent_v2.subagent_usage_hint_text.clone() && let Some(subagent_usage_hint_message) = diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index c73c80eb1..3e0242c03 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -717,6 +717,53 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { "full-history forked child should preserve the parent diff baseline" ); + let mut disabled_hint_child_config = harness.config.clone(); + let _ = disabled_hint_child_config + .features + .enable(Feature::MultiAgentV2); + disabled_hint_child_config.multi_agent_v2.usage_hint_enabled = false; + disabled_hint_child_config + .multi_agent_v2 + .subagent_usage_hint_text = Some("Disabled child subagent guidance.".to_string()); + let disabled_hint_child_thread_id = harness + .control + .spawn_agent_with_metadata( + disabled_hint_child_config, + text_input("child task without hints"), + Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth: 1, + agent_path: None, + agent_nickname: None, + agent_role: None, + })), + SpawnAgentOptions { + fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()), + fork_mode: Some(SpawnAgentForkMode::FullHistory), + ..Default::default() + }, + ) + .await + .expect("forked spawn should honor disabled usage hints") + .thread_id; + let disabled_hint_child_thread = harness + .manager + .get_thread(disabled_hint_child_thread_id) + .await + .expect("disabled-hint child thread should be registered"); + let disabled_hint_history = disabled_hint_child_thread + .codex + .session + .clone_history() + .await; + assert!( + !history_contains_text( + disabled_hint_history.raw_items(), + "Disabled child subagent guidance.", + ), + "full-history forked child should not add subagent guidance when usage hints are disabled" + ); + let expected = ( child_thread_id, Op::UserInput { @@ -743,6 +790,11 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { .shutdown_live_agent(child_thread_id) .await .expect("child shutdown should submit"); + let _ = harness + .control + .shutdown_live_agent(disabled_hint_child_thread_id) + .await + .expect("disabled-hint child shutdown should submit"); let _ = parent_thread .submit(Op::Shutdown {}) .await diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index fa024c0bc..70719843b 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -9846,7 +9846,55 @@ enabled = true 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.agent_max_threads, Some(3)); - assert!(!config.multi_agent_v2.non_code_mode_only); + 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.non_code_mode_only); + + Ok(()) +} + +#[tokio::test] +async fn multi_agent_v2_empty_usage_hint_overrides_clear_default_hints() -> 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 +root_agent_usage_hint_text = "" +subagent_usage_hint_text = "" +"#, + )?; + + 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.root_agent_usage_hint_text, None); + assert_eq!(config.multi_agent_v2.subagent_usage_hint_text, None); Ok(()) } diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 9624f42eb..0f01e8faf 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -189,6 +189,44 @@ pub(crate) const DEFAULT_MULTI_AGENT_V2_MAX_CONCURRENT_THREADS_PER_SESSION: usiz pub(crate) const DEFAULT_MULTI_AGENT_V2_MIN_WAIT_TIMEOUT_MS: i64 = 10_000; pub(crate) const DEFAULT_MULTI_AGENT_V2_MAX_WAIT_TIMEOUT_MS: i64 = 3600 * 1000; pub(crate) const DEFAULT_MULTI_AGENT_V2_DEFAULT_WAIT_TIMEOUT_MS: i64 = 30_000; +const DEFAULT_MULTI_AGENT_V2_ROOT_AGENT_USAGE_HINT_TEXT: &str = r#"You are `/root`, the primary agent in a team of agents collaborating to fulfill the user's goals. + +At the start of your turn, you are the active agent. +You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents. +All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools. + +You can use `spawn_agent` to create a new agent, `assign_task` to give an existing agent a new task and trigger a turn, and `send_message` to pass a message to a running agent without triggering a turn. +Child agents can also spawn their own sub-agents. +You can decide how much context you want to propagate to your sub-agents with the `fork_turns` parameter. + +You will receive messages in the analysis channel in the form: +``` +Message Type: MESSAGE | FINAL_ANSWER +Sender: +Payload: + +``` +They may be addressed as to=/root +"#; +const DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT: &str = r#"You are an agent in a team of agents collaborating to complete a task. + +You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents. All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools. + +You can use `spawn_agent` to create a new agent, `assign_task` to give an existing agent a new task and trigger a turn, and `send_message` to pass a message to a running agent. +Child agents can also spawn their own sub-agents. + +When you provide a response in the final channel, that content is immediately delivered back to your parent agent. + +You will receive messages in the analysis channel in the form: +``` +Message Type: NEW_TASK | MESSAGE | FINAL_ANSWER +Task name: # only for NEW_TASK -- this determines your identity +Sender: +Payload: + +``` +You may also see them addressed as to=/root/..., which indicates your identity is /root/... +"#; 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; @@ -1019,11 +1057,15 @@ impl Default for MultiAgentV2Config { 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: None, - subagent_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(), + ), tool_namespace: None, hide_spawn_agent_metadata: false, - non_code_mode_only: false, + non_code_mode_only: true, } } } @@ -2239,14 +2281,14 @@ fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config .and_then(|config| config.usage_hint_text.as_ref()) .cloned() .or(default.usage_hint_text); - let root_agent_usage_hint_text = base - .and_then(|config| config.root_agent_usage_hint_text.as_ref()) - .cloned() - .or(default.root_agent_usage_hint_text); - let subagent_usage_hint_text = base - .and_then(|config| config.subagent_usage_hint_text.as_ref()) - .cloned() - .or(default.subagent_usage_hint_text); + let root_agent_usage_hint_text = resolve_optional_prompt_text( + base.map(|config| &config.root_agent_usage_hint_text), + default.root_agent_usage_hint_text, + ); + let subagent_usage_hint_text = resolve_optional_prompt_text( + base.map(|config| &config.subagent_usage_hint_text), + default.subagent_usage_hint_text, + ); let tool_namespace = base .and_then(|config| config.tool_namespace.as_ref()) .cloned() @@ -2287,6 +2329,17 @@ fn resolve_terminal_resize_reflow_config(config_toml: &ConfigToml) -> TerminalRe } } +fn resolve_optional_prompt_text( + configured: Option<&Option>, + default: Option, +) -> Option { + match configured { + Some(Some(value)) if value.is_empty() => None, + Some(Some(value)) => Some(value.clone()), + Some(None) | None => default, + } +} + fn multi_agent_v2_toml_config(features: Option<&FeaturesToml>) -> Option<&MultiAgentV2ConfigToml> { match features?.multi_agent_v2.as_ref()? { FeatureToml::Enabled(_) => None, diff --git a/codex-rs/core/src/session/multi_agents.rs b/codex-rs/core/src/session/multi_agents.rs index 2cab13cec..eb3da0da4 100644 --- a/codex-rs/core/src/session/multi_agents.rs +++ b/codex-rs/core/src/session/multi_agents.rs @@ -12,6 +12,10 @@ pub(super) fn usage_hint_text<'a>( } let multi_agent_v2 = &turn_context.config.multi_agent_v2; + if !multi_agent_v2.usage_hint_enabled { + return None; + } + match session_source { SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. }) => { multi_agent_v2.subagent_usage_hint_text.as_deref() diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index d03177fd6..18ea81787 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -7185,6 +7185,34 @@ async fn build_initial_context_omits_multi_agent_v2_usage_hints_when_feature_dis ); } +#[tokio::test] +async fn build_initial_context_omits_multi_agent_v2_usage_hints_when_hint_disabled() { + let (session, turn_context, _rx_event) = make_session_and_context_with_auth_and_config_and_rx( + CodexAuth::from_api_key("Test API Key"), + Vec::new(), + |config| { + let _ = config.features.enable(Feature::MultiAgentV2); + config.multi_agent_v2.usage_hint_enabled = false; + config.multi_agent_v2.root_agent_usage_hint_text = Some("Root guidance.".to_string()); + config.multi_agent_v2.subagent_usage_hint_text = Some("Subagent guidance.".to_string()); + }, + ) + .await; + + let initial_context = session.build_initial_context(turn_context.as_ref()).await; + + let developer_messages = developer_message_texts(&initial_context); + assert!( + !developer_messages.iter().any(|message| { + matches!( + message.as_slice(), + ["Root guidance."] | ["Subagent guidance."] + ) + }), + "did not expect multi-agent v2 usage hint developer messages, got {developer_messages:?}" + ); +} + #[tokio::test] async fn build_initial_context_omits_default_image_save_location_with_image_history() { let (session, turn_context) = make_session_and_context().await; diff --git a/codex-rs/core/src/tools/handlers/multi_agents_spec.rs b/codex-rs/core/src/tools/handlers/multi_agents_spec.rs index ba74c2280..96acb595b 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_spec.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_spec.rs @@ -11,9 +11,9 @@ use std::collections::BTreeMap; pub const MULTI_AGENT_V1_NAMESPACE: &str = "multi_agent_v1"; const MULTI_AGENT_V1_NAMESPACE_DESCRIPTION: &str = "Tools for spawning and managing sub-agents."; -const SPAWN_AGENT_INHERITED_MODEL_GUIDANCE: &str = "Spawned agents inherit your current model by default. If provided, `model` specifies the model to use for the spawned agent."; +const SPAWN_AGENT_INHERITED_MODEL_GUIDANCE: &str = "Spawned agents inherit your current model by default. Omit `model` to use that preferred default; set `model` only when an explicit override is needed."; const SPAWN_AGENT_MODEL_OVERRIDE_DESCRIPTION: &str = - "Model override for the new agent. Omit to inherit the parent model."; + "Model override for the new agent. Omit unless an explicit override is needed."; const SPAWN_AGENT_SERVICE_TIER_OVERRIDE_DESCRIPTION: &str = "Service tier override for the new agent. Omit unless explicitly requested."; const MAX_MODEL_OVERRIDES_IN_SPAWN_AGENT_DESCRIPTION: usize = 5; diff --git a/codex-rs/core/tests/suite/spawn_agent_description.rs b/codex-rs/core/tests/suite/spawn_agent_description.rs index f9be02349..895ef8545 100644 --- a/codex-rs/core/tests/suite/spawn_agent_description.rs +++ b/codex-rs/core/tests/suite/spawn_agent_description.rs @@ -184,7 +184,7 @@ async fn spawn_agent_description_lists_visible_models_and_reasoning_efforts() -> ); assert!( description.contains( - "Spawned agents inherit your current model by default. If provided, `model` specifies the model to use for the spawned agent." + "Spawned agents inherit your current model by default. Omit `model` to use that preferred default; set `model` only when an explicit override is needed." ), "expected inherited-model guidance in spawn_agent description: {description:?}" );