Simplify multi-agent mode controls (#29324)

## Why

Multi-agent delegation policy was split across `multiAgentMode`,
`features.multi_agent_mode`, and `usage_hint_enabled`. These controls
could disagree: a requested mode could be downgraded by the feature
flag, and disabling usage hints also disabled mode instructions.

Some clients also need multi-agent tools without adding
delegation-policy text to model context. The previous two-mode API could
not express that directly.

## What changed

`multiAgentMode` is now the only live delegation-policy control:

| Mode | Behavior |
| --- | --- |
| `none` | Keep multi-agent tools available without adding mode
instructions. |
| `explicitRequestOnly` | Only delegate after an explicit user request.
|
| `proactive` | Delegate when parallel work materially improves speed or
quality. |

- new threads default to `explicitRequestOnly`; omitting the mode on
later turns keeps the current value
- thread start, resume, fork, and settings responses always report the
concrete current mode instead of `null`
- mode selection remains sticky across turns and resume
- usage-hint text no longer controls whether mode instructions apply
- `features.multi_agent_mode` and `usage_hint_enabled` remain accepted
as ignored compatibility settings so existing configs continue to load
- app-server documentation and generated schemas describe the three-mode
API

## Tests

- `just test -p codex-core multi_agent_mode`
- `just test -p codex-core multi_agent_v2_config_from_feature_table`
- `just test -p codex-core spawn_agent_description`
- `just test -p codex-features`
- `just test -p codex-app-server-protocol`
- `just test -p codex-app-server multi_agent_mode`
This commit is contained in:
jif
2026-06-22 10:05:36 +02:00
committed by GitHub
parent 6d15bb3d17
commit c03742ca0a
44 changed files with 253 additions and 350 deletions
@@ -1701,6 +1701,7 @@ mod tests {
use codex_protocol::ThreadId;
use codex_protocol::account::AmazonBedrockCredentialSource;
use codex_protocol::account::PlanType;
use codex_protocol::config_types::MultiAgentMode;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::RealtimeConversationVersion;
@@ -2584,7 +2585,7 @@ mod tests {
sandbox: v2::SandboxPolicy::DangerFullAccess,
active_permission_profile: None,
reasoning_effort: None,
multi_agent_mode: None,
multi_agent_mode: MultiAgentMode::ExplicitRequestOnly,
},
};
@@ -2633,7 +2634,7 @@ mod tests {
},
"activePermissionProfile": null,
"reasoningEffort": null,
"multiAgentMode": null
"multiAgentMode": "explicitRequestOnly"
}
}),
serde_json::to_value(&response)?,
@@ -1,5 +1,6 @@
use super::*;
use codex_protocol::approvals::ElicitationRequest as CoreElicitationRequest;
use codex_protocol::config_types::MultiAgentMode;
use codex_protocol::items::AgentMessageContent;
use codex_protocol::items::AgentMessageItem;
use codex_protocol::items::FileChangeItem;
@@ -3708,7 +3709,11 @@ fn thread_lifecycle_responses_default_missing_optional_fields() {
resume.multi_agent_mode,
fork.multi_agent_mode,
),
(None, None, None)
(
MultiAgentMode::ExplicitRequestOnly,
MultiAgentMode::ExplicitRequestOnly,
MultiAgentMode::ExplicitRequestOnly,
)
);
let foreign_source: LegacyAppPathString =
@@ -94,9 +94,9 @@ pub struct ThreadStartParams {
pub developer_instructions: Option<String>,
#[ts(optional = nullable)]
pub personality: Option<Personality>,
/// Set the initial multi-agent mode for this thread.
/// Omitted leaves the thread without a selected mode. Eligible multi-agent
/// v2 turns still default to `explicitRequestOnly`.
/// Set the initial multi-agent mode for this thread. `none` leaves the
/// multi-agent tools available without injecting mode instructions.
/// Omitted defaults to `explicitRequestOnly`.
#[experimental("thread/start.multiAgentMode")]
#[ts(optional = nullable)]
pub multi_agent_mode: Option<MultiAgentMode>,
@@ -186,10 +186,10 @@ pub struct ThreadStartResponse {
#[serde(default)]
pub active_permission_profile: Option<ActivePermissionProfile>,
pub reasoning_effort: Option<ReasoningEffort>,
/// Current selected multi-agent mode for this thread, if one was selected.
/// Current multi-agent mode for this thread.
#[experimental("thread/start.multiAgentMode")]
#[serde(default)]
pub multi_agent_mode: Option<MultiAgentMode>,
pub multi_agent_mode: MultiAgentMode,
}
impl ThreadStartResponse {
@@ -279,10 +279,10 @@ pub struct ThreadSettings {
pub effort: Option<ReasoningEffort>,
pub summary: Option<ReasoningSummary>,
pub collaboration_mode: CollaborationMode,
/// Current selected multi-agent mode for this thread, if one was selected.
/// Current multi-agent mode for this thread.
#[experimental("thread/settings.multiAgentMode")]
#[serde(default)]
pub multi_agent_mode: Option<MultiAgentMode>,
pub multi_agent_mode: MultiAgentMode,
pub personality: Option<Personality>,
}
@@ -419,10 +419,10 @@ pub struct ThreadResumeResponse {
#[serde(default)]
pub active_permission_profile: Option<ActivePermissionProfile>,
pub reasoning_effort: Option<ReasoningEffort>,
/// Current selected multi-agent mode for this thread, if one was selected.
/// Current multi-agent mode for this thread.
#[experimental("thread/resume.multiAgentMode")]
#[serde(default)]
pub multi_agent_mode: Option<MultiAgentMode>,
pub multi_agent_mode: MultiAgentMode,
/// `thread/turns/list` page returned when requested by `initialTurnsPage`.
#[experimental("thread/resume.initialTurnsPage")]
#[serde(default)]
@@ -578,10 +578,10 @@ pub struct ThreadForkResponse {
#[serde(default)]
pub active_permission_profile: Option<ActivePermissionProfile>,
pub reasoning_effort: Option<ReasoningEffort>,
/// Current selected multi-agent mode for this thread, if one was selected.
/// Current multi-agent mode for this thread.
#[experimental("thread/fork.multiAgentMode")]
#[serde(default)]
pub multi_agent_mode: Option<MultiAgentMode>,
pub multi_agent_mode: MultiAgentMode,
}
impl ThreadForkResponse {
@@ -151,8 +151,9 @@ pub struct TurnStartParams {
#[ts(optional = nullable)]
pub collaboration_mode: Option<CollaborationMode>,
/// Controls whether multi-agent v2 delegation requires an explicit user request.
/// Omitted keeps the loaded session's current mode.
/// Controls multi-agent v2 delegation instructions. `none` leaves the
/// multi-agent tools available without injecting mode instructions. Omitted
/// keeps the loaded session's current mode.
#[experimental("turn/start.multiAgentMode")]
#[ts(optional = nullable)]
pub multi_agent_mode: Option<MultiAgentMode>,