Expose thread-level multi-agent mode (#28792)

## Why

Once multi-agent mode can be selected per turn, clients also need to
choose the initial selection when creating a thread and observe that
selection through lifecycle and settings APIs.

The selected value is intentionally distinct from the effective
model-visible value: no client selection is represented as `null`, even
though an eligible multi-agent v2 turn derives `explicitRequestOnly` as
its effective default.

## What changed

- Add the optional experimental `thread/start.multiAgentMode` parameter
and pass it through thread creation.
- Preserve an omitted initial value as an unset selection rather than
eagerly storing `explicitRequestOnly`.
- Apply an explicit `thread/start` selection to the first turn through
the session configuration established at thread creation.
- Restore the latest persisted effective mode as the selected baseline
on cold resume when rollout history contains one.
- Inherit the optional selected mode from a loaded parent when creating
related runtime threads.
- Return the current selected `multiAgentMode` from `thread/start`,
`thread/resume`, `thread/fork`, and thread settings, using `null` when
no mode is selected.
- Keep lifecycle reporting independent from model capability and feature
eligibility; core turn construction remains responsible for calculating
and persisting the effective mode.

## Not covered

- Clearing an existing loaded-session selection back to unset through
`turn/start`; omitted or `null` currently retains the session's
selection.
- A TUI control, slash command, or `config.toml` preference.

## Verification

- `CARGO_INCREMENTAL=0 just test -p codex-app-server-protocol`
- `CARGO_INCREMENTAL=0 just test -p codex-app-server multi_agent_mode`

The focused app-server coverage verifies explicit `thread/start`
initialization, first-turn prompting, nullable reporting for an omitted
selection, and retention of selections that are not currently
runtime-eligible.

## Stack

Stacked on #28685. This PR contains only the thread initialization and
lifecycle/settings API layer.
This commit is contained in:
Shijie Rao
2026-06-19 01:50:44 -07:00
committed by GitHub
Unverified
parent fc8c6b7384
commit 7abfcf220b
46 changed files with 620 additions and 36 deletions
+2
View File
@@ -19,6 +19,7 @@ use crate::thread_rollout_truncation::truncate_rollout_to_last_n_fork_turns;
use codex_protocol::AgentPath;
use codex_protocol::SessionId;
use codex_protocol::ThreadId;
use codex_protocol::config_types::MultiAgentMode;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::models::ContentItem;
@@ -68,6 +69,7 @@ pub(crate) struct SpawnAgentOptions {
pub(crate) fork_mode: Option<SpawnAgentForkMode>,
pub(crate) parent_thread_id: Option<ThreadId>,
pub(crate) environments: Option<Vec<TurnEnvironmentSelection>>,
pub(crate) initial_multi_agent_mode: Option<MultiAgentMode>,
}
#[derive(Clone, Debug)]
@@ -142,6 +142,7 @@ async fn spawn_v2_subagent(
/*forked_from_thread_id*/ None,
Some(ThreadSource::Subagent),
/*metrics_service_name*/ None,
/*initial_multi_agent_mode*/ None,
/*inherited_environments*/ None,
/*inherited_exec_policy*/ None,
/*environments*/ None,
+6
View File
@@ -1,11 +1,13 @@
use super::residency::is_v2_resident_session_source;
use super::*;
use codex_protocol::config_types::MultiAgentMode;
const AGENT_NAMES: &str = include_str!("../agent_names.txt");
struct SpawnAgentThreadInheritance {
environments: Option<TurnEnvironmentSnapshot>,
exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
inherited_multi_agent_mode: Option<MultiAgentMode>,
}
fn default_agent_nickname_list() -> Vec<&'static str> {
@@ -237,6 +239,7 @@ impl AgentControl {
exec_policy: self
.inherited_exec_policy_for_source(&state, session_source.as_ref(), &config)
.await,
inherited_multi_agent_mode: options.initial_multi_agent_mode,
};
let (session_source, mut agent_metadata) = match session_source {
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
@@ -283,6 +286,7 @@ impl AgentControl {
/*forked_from_thread_id*/ None,
/*thread_source*/ Some(ThreadSource::Subagent),
/*metrics_service_name*/ None,
inheritance.inherited_multi_agent_mode,
inheritance.environments,
inheritance.exec_policy,
options.environments.clone(),
@@ -388,6 +392,7 @@ impl AgentControl {
let SpawnAgentThreadInheritance {
environments: inherited_environments,
exec_policy: inherited_exec_policy,
inherited_multi_agent_mode,
} = inheritance;
if options.fork_parent_spawn_call_id.is_none() {
return Err(CodexErr::Fatal(
@@ -513,6 +518,7 @@ impl AgentControl {
/*thread_source*/ Some(ThreadSource::Subagent),
/*parent_thread_id*/ Some(parent_thread_id),
/*forked_from_thread_id*/ Some(parent_thread_id),
inherited_multi_agent_mode,
inherited_environments,
inherited_exec_policy,
options.environments.clone(),
+106 -2
View File
@@ -14,6 +14,7 @@ use codex_features::Feature;
use codex_login::CodexAuth;
use codex_protocol::AgentPath;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::MultiAgentMode;
use codex_protocol::models::ContentItem;
use codex_protocol::models::MessagePhase;
use codex_protocol::models::ResponseItem;
@@ -816,6 +817,45 @@ async fn spawn_agent_creates_thread_and_sends_prompt() {
assert_eq!(captured, Some(expected));
}
#[tokio::test]
async fn spawn_thread_subagent_uses_supplied_initial_multi_agent_mode_without_history() {
let harness = AgentControlHarness::new().await;
let (parent_thread_id, _parent_thread) = harness.start_thread().await;
let child_thread_id = harness
.control
.spawn_agent_with_metadata(
harness.config.clone(),
text_input("child task"),
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,
agent_path: None,
agent_nickname: None,
agent_role: None,
})),
SpawnAgentOptions {
initial_multi_agent_mode: Some(MultiAgentMode::Proactive),
..Default::default()
},
)
.await
.expect("spawn child without parent history")
.thread_id;
let child_snapshot = harness
.manager
.get_thread(child_thread_id)
.await
.expect("child thread should be registered")
.config_snapshot()
.await;
assert_eq!(
child_snapshot.multi_agent_mode,
Some(MultiAgentMode::Proactive)
);
}
#[tokio::test]
async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
let harness = AgentControlHarness::new().await;
@@ -838,6 +878,15 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
.expect("start parent thread");
let parent_thread_id = new_thread.thread_id;
let parent_thread = new_thread.thread;
parent_thread
.codex
.session
.update_settings(crate::session::SessionSettingsUpdate {
multi_agent_mode: Some(MultiAgentMode::Proactive),
..Default::default()
})
.await
.expect("update parent multi-agent mode");
parent_thread
.inject_user_message_without_turn("parent seed context".to_string())
.await;
@@ -923,6 +972,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
SpawnAgentOptions {
fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()),
fork_mode: Some(SpawnAgentForkMode::FullHistory),
initial_multi_agent_mode: Some(MultiAgentMode::Proactive),
..Default::default()
},
)
@@ -935,6 +985,10 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
.get_thread(child_thread_id)
.await
.expect("child thread should be registered");
assert_eq!(
child_thread.config_snapshot().await.multi_agent_mode,
Some(MultiAgentMode::Proactive)
);
assert_ne!(child_thread_id, parent_thread_id);
let history = child_thread.codex.session.clone_history().await;
let expected_history = [
@@ -1242,6 +1296,15 @@ async fn spawn_agent_fork_flushes_parent_rollout_before_loading_history() {
async fn spawn_agent_fork_last_n_turns_keeps_only_recent_turns() {
let harness = AgentControlHarness::new().await;
let (parent_thread_id, parent_thread) = harness.start_thread().await;
parent_thread
.codex
.session
.update_settings(crate::session::SessionSettingsUpdate {
multi_agent_mode: Some(MultiAgentMode::Proactive),
..Default::default()
})
.await
.expect("update parent multi-agent mode");
parent_thread
.inject_user_message_without_turn("old parent context".to_string())
@@ -1326,6 +1389,7 @@ async fn spawn_agent_fork_last_n_turns_keeps_only_recent_turns() {
SpawnAgentOptions {
fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()),
fork_mode: Some(SpawnAgentForkMode::LastNTurns(2)),
initial_multi_agent_mode: Some(MultiAgentMode::Proactive),
..Default::default()
},
)
@@ -1338,6 +1402,10 @@ async fn spawn_agent_fork_last_n_turns_keeps_only_recent_turns() {
.get_thread(child_thread_id)
.await
.expect("child thread should be registered");
assert_eq!(
child_thread.config_snapshot().await.multi_agent_mode,
Some(MultiAgentMode::Proactive)
);
let history = child_thread.codex.session.clone_history().await;
assert!(
@@ -2185,7 +2253,7 @@ async fn spawn_thread_subagent_uses_role_specific_nickname_candidates() {
}
#[tokio::test]
async fn resume_thread_subagent_restores_stored_nickname_and_role() {
async fn resume_thread_subagent_restores_stored_metadata_and_effective_multi_agent_mode() {
let (home, mut config) = test_config().await;
config
.features
@@ -2207,7 +2275,7 @@ async fn resume_thread_subagent_restores_stored_nickname_and_role() {
manager,
control,
};
let (parent_thread_id, _parent_thread) = harness.start_thread().await;
let (parent_thread_id, parent_thread) = harness.start_thread().await;
let agent_path = AgentPath::from_string("/root/explorer".to_string())
.expect("test agent path should be valid");
@@ -2232,6 +2300,38 @@ async fn resume_thread_subagent_restores_stored_nickname_and_role() {
.get_thread(child_thread_id)
.await
.expect("child thread should exist");
let mut child_turn_context = child_thread
.codex
.session
.new_default_turn()
.await
.to_turn_context_item();
child_turn_context.multi_agent_mode = Some(MultiAgentMode::Proactive);
child_thread
.codex
.session
.persist_rollout_items(&[RolloutItem::TurnContext(child_turn_context)])
.await;
child_thread
.codex
.session
.ensure_rollout_materialized()
.await;
child_thread
.codex
.session
.flush_rollout()
.await
.expect("flush child effective multi-agent mode");
parent_thread
.codex
.session
.update_settings(crate::session::SessionSettingsUpdate {
multi_agent_mode: Some(MultiAgentMode::ExplicitRequestOnly),
..Default::default()
})
.await
.expect("change parent multi-agent mode before child resume");
let mut status_rx = harness
.control
.subscribe_status(child_thread_id)
@@ -2320,6 +2420,10 @@ async fn resume_thread_subagent_restores_stored_nickname_and_role() {
assert_eq!(resumed_agent_path, Some(agent_path));
assert_eq!(resumed_nickname, Some(original_nickname));
assert_eq!(resumed_role, Some("explorer".to_string()));
assert_eq!(
resumed_snapshot.multi_agent_mode,
Some(MultiAgentMode::Proactive)
);
let _ = harness
.control
+1
View File
@@ -122,6 +122,7 @@ pub(crate) async fn run_codex_thread_interactive(
attestation_provider: parent_session.services.attestation_provider.clone(),
external_time_provider: Some(Arc::clone(&parent_session.services.time_provider)),
inherited_multi_agent_version: Some(MultiAgentVersion::Disabled),
initial_multi_agent_mode: None,
}))
.or_cancel(&cancel_token)
.await??;
+1 -1
View File
@@ -106,7 +106,7 @@ fn build_multi_agent_mode_update_item(
&next.config.multi_agent_v2,
&next.session_source,
next.multi_agent_mode,
next.features.enabled(Feature::MultiAgentMode),
next.config.features.enabled(Feature::MultiAgentMode),
);
let previous = previous?;
if previous.multi_agent_mode == effective_multi_agent_mode {
+1
View File
@@ -178,6 +178,7 @@ async fn thread_settings_applied_event(sess: &Session) -> EventMsg {
reasoning_summary: snapshot.reasoning_summary,
personality: snapshot.personality,
collaboration_mode: snapshot.collaboration_mode,
multi_agent_mode: snapshot.multi_agent_mode,
},
})
}
+7 -2
View File
@@ -445,6 +445,7 @@ pub(crate) struct CodexSpawnArgs {
pub(crate) attestation_provider: Option<Arc<dyn AttestationProvider>>,
pub(crate) external_time_provider: Option<Arc<dyn TimeProvider>>,
pub(crate) inherited_multi_agent_version: Option<MultiAgentVersion>,
pub(crate) initial_multi_agent_mode: Option<MultiAgentMode>,
}
pub(crate) fn resolve_multi_agent_version(
@@ -529,6 +530,7 @@ impl Codex {
attestation_provider,
external_time_provider,
inherited_multi_agent_version,
initial_multi_agent_mode,
} = args;
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
let (tx_event, rx_event) = async_channel::unbounded();
@@ -583,7 +585,7 @@ impl Codex {
.await;
let multi_agent_version =
resolve_multi_agent_version(&conversation_history, inherited_multi_agent_version);
let multi_agent_mode = conversation_history.get_multi_agent_mode();
let multi_agent_mode = initial_multi_agent_mode;
config
.validate_multi_agent_v2_config()
.map_err(|err| CodexErr::InvalidRequest(err.to_string()))?;
@@ -3249,7 +3251,10 @@ impl Session {
&turn_context.config.multi_agent_v2,
&session_source,
turn_context.multi_agent_mode,
turn_context.features.enabled(Feature::MultiAgentMode),
turn_context
.config
.features
.enabled(Feature::MultiAgentMode),
) {
items.push(ContextualUserFragment::into(
MultiAgentModeInstructions::new(multi_agent_mode),
@@ -738,6 +738,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
attestation_provider: None,
external_time_provider: None,
inherited_multi_agent_version: None,
initial_multi_agent_mode: None,
})
.await
.expect("spawn guardian subagent");
+1 -1
View File
@@ -388,7 +388,7 @@ impl TurnContext {
&self.config.multi_agent_v2,
&self.session_source,
self.multi_agent_mode,
self.features.enabled(Feature::MultiAgentMode),
self.config.features.enabled(Feature::MultiAgentMode),
),
realtime_active: Some(self.realtime_active),
effort: self.reasoning_effort.clone(),
+31 -3
View File
@@ -36,6 +36,7 @@ use codex_models_manager::manager::RefreshStrategy;
use codex_models_manager::manager::SharedModelsManager;
use codex_protocol::ThreadId;
use codex_protocol::config_types::CollaborationModeMask;
use codex_protocol::config_types::MultiAgentMode;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::openai_models::ModelPreset;
@@ -184,6 +185,7 @@ pub struct StartThreadOptions {
pub thread_source: Option<ThreadSource>,
pub dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
pub metrics_service_name: Option<String>,
pub multi_agent_mode: Option<MultiAgentMode>,
pub parent_trace: Option<W3cTraceContext>,
pub environments: Vec<TurnEnvironmentSelection>,
pub thread_extension_init: ExtensionDataInit,
@@ -607,6 +609,7 @@ impl ThreadManager {
thread_source: None,
dynamic_tools,
metrics_service_name: None,
multi_agent_mode: None,
parent_trace: None,
environments,
thread_extension_init: ExtensionDataInit::default(),
@@ -646,6 +649,7 @@ impl ThreadManager {
thread_source,
options.dynamic_tools,
options.metrics_service_name,
options.multi_agent_mode,
/*inherited_environments*/ None,
/*inherited_exec_policy*/ None,
options.parent_trace,
@@ -730,6 +734,7 @@ impl ThreadManager {
let (session_source, thread_source) = initial_history
.get_resumed_session_sources()
.unwrap_or_else(|| (self.state.session_source.clone(), None));
let initial_multi_agent_mode = initial_history.get_latest_effective_multi_agent_mode();
Box::pin(self.state.spawn_thread_with_source(
config,
initial_history,
@@ -741,6 +746,7 @@ impl ThreadManager {
thread_source,
Vec::new(),
/*metrics_service_name*/ None,
initial_multi_agent_mode,
/*inherited_environments*/ None,
/*inherited_exec_policy*/ None,
parent_trace,
@@ -773,6 +779,7 @@ impl ThreadManager {
/*thread_source*/ None,
Vec::new(),
/*metrics_service_name*/ None,
/*initial_multi_agent_mode*/ None,
/*parent_trace*/ None,
environments,
/*thread_extension_init*/ ExtensionDataInit::default(),
@@ -799,6 +806,7 @@ impl ThreadManager {
let (session_source, thread_source) = initial_history
.get_resumed_session_sources()
.unwrap_or_else(|| (self.state.session_source.clone(), None));
let initial_multi_agent_mode = initial_history.get_latest_effective_multi_agent_mode();
Box::pin(self.state.spawn_thread_with_source(
config,
initial_history,
@@ -810,6 +818,7 @@ impl ThreadManager {
thread_source,
Vec::new(),
/*metrics_service_name*/ None,
initial_multi_agent_mode,
/*inherited_environments*/ None,
/*inherited_exec_policy*/ None,
/*parent_trace*/ None,
@@ -960,18 +969,25 @@ impl ThreadManager {
) -> CodexResult<NewThread> {
// `forked_from_id()` describes this history's existing lineage. When
// forking a resumed thread, the child copies the resumed thread itself.
let forked_from_thread_id = match &history {
let source_thread_id = match &history {
InitialHistory::Resumed(resumed) => Some(resumed.conversation_id),
InitialHistory::Forked(_) => history.forked_from_id(),
InitialHistory::New | InitialHistory::Cleared => None,
};
let initial_multi_agent_mode = match source_thread_id {
Some(thread_id) => match self.get_thread(thread_id).await {
Ok(thread) => thread.config_snapshot().await.multi_agent_mode,
Err(_) => history.get_latest_effective_multi_agent_mode(),
},
None => history.get_latest_effective_multi_agent_mode(),
};
let multi_agent_version = self
.state
.effective_multi_agent_version_for_spawn(
&history,
/*session_source*/ None,
/*parent_thread_id*/ None,
forked_from_thread_id,
source_thread_id,
&config,
)
.await;
@@ -989,10 +1005,11 @@ impl ThreadManager {
Arc::clone(&self.state.auth_manager),
agent_control,
/*parent_thread_id*/ None,
forked_from_thread_id,
source_thread_id,
thread_source,
Vec::new(),
/*metrics_service_name*/ None,
initial_multi_agent_mode,
parent_trace,
environments,
/*thread_extension_init*/ ExtensionDataInit::default(),
@@ -1217,6 +1234,7 @@ impl ThreadManagerState {
/*forked_from_thread_id*/ None,
/*thread_source*/ None,
/*metrics_service_name*/ None,
/*initial_multi_agent_mode*/ None,
/*inherited_environments*/ None,
/*inherited_exec_policy*/ None,
/*environments*/ None,
@@ -1234,6 +1252,7 @@ impl ThreadManagerState {
forked_from_thread_id: Option<ThreadId>,
thread_source: Option<ThreadSource>,
metrics_service_name: Option<String>,
initial_multi_agent_mode: Option<MultiAgentMode>,
inherited_environments: Option<TurnEnvironmentSnapshot>,
inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
environments: Option<Vec<TurnEnvironmentSelection>>,
@@ -1252,6 +1271,7 @@ impl ThreadManagerState {
thread_source,
Vec::new(),
metrics_service_name,
initial_multi_agent_mode,
inherited_environments,
inherited_exec_policy,
/*parent_trace*/ None,
@@ -1279,6 +1299,7 @@ impl ThreadManagerState {
let environments =
default_thread_environment_selections(self.environment_manager.as_ref(), &config.cwd);
let thread_source = initial_history.get_resumed_thread_source();
let initial_multi_agent_mode = initial_history.get_latest_effective_multi_agent_mode();
Box::pin(self.spawn_thread_with_source(
config,
initial_history,
@@ -1290,6 +1311,7 @@ impl ThreadManagerState {
thread_source,
Vec::new(),
/*metrics_service_name*/ None,
initial_multi_agent_mode,
inherited_environments,
inherited_exec_policy,
/*parent_trace*/ None,
@@ -1311,6 +1333,7 @@ impl ThreadManagerState {
thread_source: Option<ThreadSource>,
parent_thread_id: Option<ThreadId>,
forked_from_thread_id: Option<ThreadId>,
initial_multi_agent_mode: Option<MultiAgentMode>,
inherited_environments: Option<TurnEnvironmentSnapshot>,
inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
environments: Option<Vec<TurnEnvironmentSelection>>,
@@ -1329,6 +1352,7 @@ impl ThreadManagerState {
thread_source,
Vec::new(),
/*metrics_service_name*/ None,
initial_multi_agent_mode,
inherited_environments,
inherited_exec_policy,
/*parent_trace*/ None,
@@ -1353,6 +1377,7 @@ impl ThreadManagerState {
thread_source: Option<ThreadSource>,
dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
metrics_service_name: Option<String>,
initial_multi_agent_mode: Option<MultiAgentMode>,
parent_trace: Option<W3cTraceContext>,
environments: Vec<TurnEnvironmentSelection>,
thread_extension_init: ExtensionDataInit,
@@ -1370,6 +1395,7 @@ impl ThreadManagerState {
thread_source,
dynamic_tools,
metrics_service_name,
initial_multi_agent_mode,
/*inherited_environments*/ None,
/*inherited_exec_policy*/ None,
parent_trace,
@@ -1394,6 +1420,7 @@ impl ThreadManagerState {
thread_source: Option<ThreadSource>,
dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
metrics_service_name: Option<String>,
initial_multi_agent_mode: Option<MultiAgentMode>,
inherited_environments: Option<TurnEnvironmentSnapshot>,
inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
parent_trace: Option<W3cTraceContext>,
@@ -1473,6 +1500,7 @@ impl ThreadManagerState {
attestation_provider: self.attestation_provider.clone(),
external_time_provider: self.external_time_provider.clone(),
inherited_multi_agent_version: multi_agent_version,
initial_multi_agent_mode,
}))
.await?;
let new_thread = self
@@ -322,6 +322,7 @@ async fn start_thread_keeps_internal_threads_hidden_from_normal_lookups() {
thread_source: None,
dynamic_tools: Vec::new(),
metrics_service_name: None,
multi_agent_mode: None,
parent_trace: None,
environments: Vec::new(),
thread_extension_init: Default::default(),
@@ -462,6 +463,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
thread_source: None,
dynamic_tools: Vec::new(),
metrics_service_name: None,
multi_agent_mode: None,
parent_trace: None,
environments: Vec::new(),
thread_extension_init: selected_root_init("selected-a", "env-a"),
@@ -477,6 +479,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
thread_source: None,
dynamic_tools: Vec::new(),
metrics_service_name: None,
multi_agent_mode: None,
parent_trace: None,
environments: Vec::new(),
thread_extension_init: selected_root_init("selected-b", "env-b"),
@@ -569,6 +572,7 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
thread_source: None,
dynamic_tools: Vec::new(),
metrics_service_name: None,
multi_agent_mode: None,
parent_trace: None,
environments: environments.clone(),
thread_extension_init: Default::default(),
@@ -852,6 +856,7 @@ async fn resume_stopped_thread_from_rollout_preserves_thread_source() {
thread_source: Some(ThreadSource::User),
dynamic_tools: Vec::new(),
metrics_service_name: None,
multi_agent_mode: None,
parent_trace: None,
environments: Vec::new(),
thread_extension_init: Default::default(),
@@ -132,6 +132,7 @@ async fn handle_spawn_agent(
fork_mode: args.fork_context.then_some(SpawnAgentForkMode::FullHistory),
parent_thread_id: Some(session.thread_id),
environments: Some(turn.environments.to_selections()),
initial_multi_agent_mode: None,
},
))
.await
@@ -24,6 +24,7 @@ use codex_model_provider_info::built_in_model_providers;
use codex_protocol::AgentPath;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::MultiAgentMode;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::ShellEnvironmentPolicy;
use codex_protocol::models::BaseInstructions;
@@ -1147,6 +1148,11 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
config
.features
.enable(Feature::MultiAgentMode)
.expect("test config should allow feature update");
turn.multi_agent_mode = Some(MultiAgentMode::Proactive);
set_turn_config(&mut turn, config);
let session = Arc::new(session);
@@ -1185,6 +1191,10 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
child_snapshot.session_source.get_agent_path().as_deref(),
Some("/root/test_process")
);
assert_eq!(
child_snapshot.multi_agent_mode,
Some(MultiAgentMode::Proactive)
);
assert!(manager.captured_ops().iter().any(|(id, op)| {
*id == child_thread_id
&& matches!(
@@ -50,6 +50,15 @@ async fn handle_spawn_agent(
let arguments = function_arguments(payload)?;
let args: SpawnAgentArgs = parse_arguments(&arguments)?;
let fork_mode = args.fork_mode()?;
let multi_agent_mode = crate::session::multi_agents::effective_multi_agent_mode(
turn.multi_agent_version,
&turn.config.multi_agent_v2,
&turn.session_source,
turn.multi_agent_mode,
turn.config
.features
.enabled(codex_features::Feature::MultiAgentMode),
);
let role_name = args
.agent_type
.as_deref()
@@ -134,6 +143,7 @@ async fn handle_spawn_agent(
fork_mode,
parent_thread_id: Some(session.thread_id),
environments: Some(turn.environments.to_selections()),
initial_multi_agent_mode: multi_agent_mode,
},
),
)