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
@@ -639,6 +639,7 @@ pub(super) async fn handle_pending_thread_resume_request(
active_permission_profile,
workspace_roots,
reasoning_effort,
multi_agent_mode,
..
} = config_snapshot;
let instruction_sources = pending.instruction_sources;
@@ -661,6 +662,7 @@ pub(super) async fn handle_pending_thread_resume_request(
sandbox,
active_permission_profile,
reasoning_effort,
multi_agent_mode,
initial_turns_page,
};
outgoing.send_response(request_id, response).await;
@@ -2,6 +2,7 @@ use super::*;
use crate::error_code::method_not_found;
use codex_app_server_protocol::SelectedCapabilityRoot;
use codex_extension_api::ExtensionDataInit;
use codex_protocol::config_types::MultiAgentMode;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
@@ -902,6 +903,7 @@ impl ThreadRequestProcessor {
mock_experimental_field: _mock_experimental_field,
experimental_raw_events,
personality,
multi_agent_mode,
ephemeral,
session_start_source,
thread_source,
@@ -955,6 +957,7 @@ impl ThreadRequestProcessor {
supports_openai_form_elicitation,
config,
typesafe_overrides,
multi_agent_mode,
dynamic_tools,
selected_capability_roots.unwrap_or_default(),
session_start_source,
@@ -1029,6 +1032,7 @@ impl ThreadRequestProcessor {
supports_openai_form_elicitation: bool,
config_overrides: Option<HashMap<String, serde_json::Value>>,
typesafe_overrides: ConfigOverrides,
multi_agent_mode: Option<MultiAgentMode>,
dynamic_tools: Option<Vec<DynamicToolSpec>>,
selected_capability_roots: Vec<SelectedCapabilityRoot>,
session_start_source: Option<codex_app_server_protocol::ThreadStartSource>,
@@ -1152,6 +1156,7 @@ impl ThreadRequestProcessor {
thread_source,
dynamic_tools,
metrics_service_name: service_name,
multi_agent_mode,
parent_trace: request_trace,
environments,
thread_extension_init,
@@ -1257,6 +1262,7 @@ impl ThreadRequestProcessor {
sandbox,
active_permission_profile,
reasoning_effort: config_snapshot.reasoning_effort,
multi_agent_mode: config_snapshot.multi_agent_mode,
};
let notif = thread_started_notification(thread);
listener_task_context
@@ -2787,6 +2793,7 @@ impl ThreadRequestProcessor {
sandbox,
active_permission_profile,
reasoning_effort: session_configured.reasoning_effort,
multi_agent_mode: config_snapshot.multi_agent_mode,
initial_turns_page,
};
@@ -3508,6 +3515,7 @@ impl ThreadRequestProcessor {
sandbox,
active_permission_profile,
reasoning_effort: session_configured.reasoning_effort,
multi_agent_mode: config_snapshot.multi_agent_mode,
};
let notif = thread_started_notification(thread);
@@ -1,5 +1,4 @@
use super::*;
#[cfg(test)]
use chrono::DateTime;
#[cfg(test)]
@@ -206,6 +205,7 @@ pub(crate) fn thread_settings_from_config_snapshot(
effort: config_snapshot.reasoning_effort.clone(),
summary: config_snapshot.reasoning_summary,
collaboration_mode: config_snapshot.collaboration_mode.clone(),
multi_agent_mode: config_snapshot.multi_agent_mode,
personality: config_snapshot.personality,
}
}
@@ -226,6 +226,7 @@ pub(crate) fn thread_settings_from_core_snapshot(
reasoning_summary,
personality,
collaboration_mode,
multi_agent_mode,
} = snapshot;
let sandbox_policy = thread_response_sandbox_policy(&permission_profile, cwd.as_path());
ThreadSettings {
@@ -242,6 +243,7 @@ pub(crate) fn thread_settings_from_core_snapshot(
effort: reasoning_effort,
summary: reasoning_summary,
collaboration_mode,
multi_agent_mode,
personality,
}
}
@@ -729,7 +729,7 @@ impl TurnRequestProcessor {
effort: params.effort,
summary: params.summary,
collaboration_mode: params.collaboration_mode,
multi_agent_mode: None,
multi_agent_mode: params.multi_agent_mode,
personality: params.personality,
},
)
+1
View File
@@ -241,6 +241,7 @@ mod tests {
developer_instructions: None,
},
},
multi_agent_mode: Default::default(),
personality: None,
}
}