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 09:05:36 +01:00
committed by GitHub
Unverified
parent 6d15bb3d17
commit c03742ca0a
44 changed files with 253 additions and 350 deletions
+2 -1
View File
@@ -1761,8 +1761,9 @@
"type": "object"
},
"MultiAgentMode": {
"description": "Controls whether the model should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help.",
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
"enum": [
"none",
"explicitRequestOnly",
"proactive"
],
@@ -2645,8 +2645,9 @@
"type": "object"
},
"MultiAgentMode": {
"description": "Controls whether the model should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help.",
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
"enum": [
"none",
"explicitRequestOnly",
"proactive"
],
@@ -12693,8 +12693,9 @@
"type": "object"
},
"MultiAgentMode": {
"description": "Controls whether the model should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help.",
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
"enum": [
"none",
"explicitRequestOnly",
"proactive"
],
@@ -9097,8 +9097,9 @@
"type": "object"
},
"MultiAgentMode": {
"description": "Controls whether the model should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help.",
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
"enum": [
"none",
"explicitRequestOnly",
"proactive"
],
@@ -663,8 +663,9 @@
]
},
"MultiAgentMode": {
"description": "Controls whether the model should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help.",
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
"enum": [
"none",
"explicitRequestOnly",
"proactive"
],
@@ -663,8 +663,9 @@
]
},
"MultiAgentMode": {
"description": "Controls whether the model should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help.",
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
"enum": [
"none",
"explicitRequestOnly",
"proactive"
],
@@ -109,8 +109,9 @@
"type": "string"
},
"MultiAgentMode": {
"description": "Controls whether the model should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help.",
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
"enum": [
"none",
"explicitRequestOnly",
"proactive"
],
@@ -195,8 +195,9 @@
"type": "string"
},
"MultiAgentMode": {
"description": "Controls whether the model should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help.",
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
"enum": [
"none",
"explicitRequestOnly",
"proactive"
],
@@ -663,8 +663,9 @@
]
},
"MultiAgentMode": {
"description": "Controls whether the model should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help.",
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
"enum": [
"none",
"explicitRequestOnly",
"proactive"
],
@@ -142,8 +142,9 @@
"type": "string"
},
"MultiAgentMode": {
"description": "Controls whether the model should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help.",
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
"enum": [
"none",
"explicitRequestOnly",
"proactive"
],
@@ -3,7 +3,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Controls whether the model should only spawn sub-agents after an explicit
* user request or may delegate proactively when doing so would help.
* Controls whether the model receives multi-agent delegation instructions and,
* when it does, whether it should only spawn sub-agents after an explicit user
* request or may delegate proactively when doing so would help. `none` leaves
* the multi-agent tools available without injecting delegation instructions.
*/
export type MultiAgentMode = "explicitRequestOnly" | "proactive";
export type MultiAgentMode = "none" | "explicitRequestOnly" | "proactive";
@@ -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>,
+4 -4
View File
@@ -137,17 +137,17 @@ Example with notification opt-out:
## API Overview
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Experimental `multiAgentMode` selects the initial thread mode; omission leaves the selected mode unset, while eligible multi-agent v2 turns still default to `explicitRequestOnly`. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`. Experimental `selectedCapabilityRoots` selects environment-owned plugin or standalone-skill roots. Skills found below those roots are listed and read through the owning environment. Stdio MCP servers declared by selected plugins are also started in that environment; HTTP MCP declarations remain inactive.
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Experimental `multiAgentMode` selects the initial thread mode and defaults to `explicitRequestOnly` when omitted; use `none` to keep multi-agent tools available without injecting mode instructions. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`. Experimental `selectedCapabilityRoots` selects environment-owned plugin or standalone-skill roots. Skills found below those roots are listed and read through the owning environment. Stdio MCP servers declared by selected plugins are also started in that environment; HTTP MCP declarations remain inactive.
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`. Multi-agent mode restores the last effective mode from rollout history when available; clients can select another mode on the first `turn/start`.
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`.
- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. `instructionSources` lists loaded instruction files using each source environment's native absolute path syntax, including files loaded from remote environments. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known. Their experimental `multiAgentMode` field, and the corresponding thread setting, report the thread's current selected mode or `null` when no mode was selected. Turn construction separately determines whether that mode is applicable to the selected model and runtime configuration.
- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. `instructionSources` lists loaded instruction files using each source environment's native absolute path syntax, including files loaded from remote environments. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known. Their experimental `multiAgentMode` field, and the corresponding thread setting, report the thread's current mode. Turn construction separately determines whether that mode is applicable to the selected model and runtime configuration.
- `thread/list` — page through stored threads; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Experimental clients can use `parentThreadId` to filter direct spawned children represented by persisted spawn-edge state. Review and Guardian threads are not included because they do not participate in that spawn-edge lifecycle. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. Subagent threads also include `parentThreadId` when the immediate parent is known.
- `thread/loaded/list` — list the thread ids currently loaded in memory.
- `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
- `thread/turns/list` — experimental; page through a stored threads turn history without resuming it; supports cursor-based pagination with `sortDirection`, `itemsView`, `nextCursor`, and `backwardsCursor`.
- `thread/turns/items/list` — experimental; reserved for paging full items for one turn. The API shape is present, but app-server currently returns an unsupported-method JSON-RPC error.
- `thread/metadata/update` — patch stored thread metadata in sqlite; currently supports updating persisted `gitInfo` fields and returns the refreshed `thread`.
- `thread/settings/update` — experimental; queue a partial update to a loaded threads next-turn settings without starting a turn or adding transcript items. Omitted fields leave settings unchanged; `serviceTier: null` clears the tier; `multiAgentMode` selects `explicitRequestOnly` or `proactive` for subsequent turns; `sandboxPolicy` and `permissions` cannot be combined. Returns `{}` when the update is accepted and emits `thread/settings/updated` with the full effective settings only if they actually change. `turn/start` settings overrides emit the same notification when they change the stored settings.
- `thread/settings/update` — experimental; queue a partial update to a loaded threads next-turn settings without starting a turn or adding transcript items. Omitted fields leave settings unchanged; `serviceTier: null` clears the tier; `multiAgentMode` selects `none`, `explicitRequestOnly`, or `proactive` for subsequent turns; `sandboxPolicy` and `permissions` cannot be combined. Returns `{}` when the update is accepted and emits `thread/settings/updated` with the full effective settings only if they actually change. `turn/start` settings overrides emit the same notification when they change the stored settings.
- `thread/memoryMode/set` — experimental; set a threads persisted memory eligibility to `"enabled"` or `"disabled"` for either a loaded thread or a stored rollout; returns `{}` on success.
- `memory/reset` — experimental; clear the current `CODEX_HOME/memories` directory and reset persisted memory stage data in sqlite while preserving existing thread memory modes; returns `{}` on success.
- `thread/goal/set` — create or update the single persisted goal for a materialized thread; returns the current goal and emits `thread/goal/updated`.
@@ -168,7 +168,7 @@ Example with notification opt-out:
- `thread/backgroundTerminals/list` — list running background terminals for a loaded thread (experimental; requires `capabilities.experimentalApi`); returns `data` with the running terminal ids.
- `thread/backgroundTerminals/terminate` — terminate one running background terminal by app-server `processId` (experimental; requires `capabilities.experimentalApi`); returns whether a process was terminated.
- `thread/rollback` — drop the last N turns from the agents in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success.
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. Prefer experimental `permissions` profile selection by id for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". Experimental `multiAgentMode` accepts `explicitRequestOnly` or `proactive`; omission keeps the loaded session's current selected mode, including an unset mode. The requested mode is retained for the loaded session without rejecting unsupported configurations. Eligible multi-agent v2 turns default an unset mode to explicit-request-only, use the selected mode when `features.multi_agent_mode` is enabled, and otherwise use explicit-request-only developer instructions.
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. Prefer experimental `permissions` profile selection by id for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". Experimental `multiAgentMode` accepts `none`, `explicitRequestOnly`, or `proactive`; `none` keeps the tools available without injecting mode instructions, and omission keeps the loaded session's current mode. The requested mode is retained for the loaded session without rejecting unsupported configurations, and eligible multi-agent v2 turns use it directly.
- `thread/inject_items` — append raw Responses API items to a loaded threads model-visible history without starting a user turn; returns `{}` on success.
- `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Review and manual compaction turns reject `turn/steer`.
- `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`.
@@ -117,10 +117,7 @@ async fn thread_settings_update_multi_agent_mode_applies_to_future_turns() -> Re
write_mock_responses_config_toml(
codex_home.path(),
&server.uri(),
&BTreeMap::from([
(Feature::MultiAgentV2, true),
(Feature::MultiAgentMode, true),
]),
&BTreeMap::from([(Feature::MultiAgentV2, true)]),
/*auto_compact_limit*/ 200_000,
/*requires_openai_auth*/ None,
"mock_provider",
@@ -158,7 +155,7 @@ async fn thread_settings_update_multi_agent_mode_applies_to_future_turns() -> Re
assert_eq!(updated.thread_id, thread.id);
assert_eq!(
updated.thread_settings.multi_agent_mode,
Some(MultiAgentMode::Proactive)
MultiAgentMode::Proactive
);
start_text_turn(&mut mcp, thread.id).await?;
@@ -1768,10 +1768,7 @@ async fn turn_start_accepts_multi_agent_mode_v2() -> Result<()> {
codex_home.path(),
&server.uri(),
"never",
&BTreeMap::from([
(Feature::MultiAgentV2, true),
(Feature::MultiAgentMode, true),
]),
&BTreeMap::from([(Feature::MultiAgentV2, true)]),
)?;
let mut mcp = TestAppServer::new(codex_home.path()).await?;
@@ -1845,10 +1842,7 @@ async fn thread_start_multi_agent_mode_initializes_first_turn() -> Result<()> {
codex_home.path(),
&server.uri(),
"never",
&BTreeMap::from([
(Feature::MultiAgentV2, true),
(Feature::MultiAgentMode, true),
]),
&BTreeMap::from([(Feature::MultiAgentV2, true)]),
)?;
let mut mcp = TestAppServer::new(codex_home.path()).await?;
@@ -1871,7 +1865,7 @@ async fn thread_start_multi_agent_mode_initializes_first_turn() -> Result<()> {
multi_agent_mode,
..
} = to_response::<ThreadStartResponse>(thread_resp)?;
assert_eq!(multi_agent_mode, Some(MultiAgentMode::Proactive));
assert_eq!(multi_agent_mode, MultiAgentMode::Proactive);
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
@@ -1911,27 +1905,29 @@ async fn thread_start_multi_agent_mode_initializes_first_turn() -> Result<()> {
}
#[tokio::test]
async fn thread_start_reports_selected_multi_agent_mode() -> Result<()> {
async fn thread_start_reports_multi_agent_mode() -> Result<()> {
skip_if_no_network!(Ok(()));
let cases = [
(
BTreeMap::from([(Feature::MultiAgentV2, true)]),
Some(MultiAgentMode::Proactive),
Some(MultiAgentMode::Proactive),
MultiAgentMode::Proactive,
),
(
BTreeMap::from([(Feature::MultiAgentV2, true)]),
Some(MultiAgentMode::None),
MultiAgentMode::None,
),
(
BTreeMap::new(),
Some(MultiAgentMode::Proactive),
Some(MultiAgentMode::Proactive),
MultiAgentMode::Proactive,
),
(
BTreeMap::from([
(Feature::MultiAgentV2, true),
(Feature::MultiAgentMode, true),
]),
None,
BTreeMap::from([(Feature::MultiAgentV2, true)]),
None,
MultiAgentMode::ExplicitRequestOnly,
),
];
+1
View File
@@ -1725,6 +1725,7 @@
"type": "string"
},
"usage_hint_enabled": {
"description": "Deprecated compatibility field. Its value is ignored.",
"type": "boolean"
},
"usage_hint_text": {
-1
View File
@@ -498,7 +498,6 @@ impl AgentControl {
}
if preserve_reference_context_item
&& multi_agent_version == MultiAgentVersion::V2
&& 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) =
+18 -36
View File
@@ -850,10 +850,7 @@ async fn spawn_thread_subagent_uses_supplied_initial_multi_agent_mode_without_hi
.config_snapshot()
.await;
assert_eq!(
child_snapshot.multi_agent_mode,
Some(MultiAgentMode::Proactive)
);
assert_eq!(child_snapshot.multi_agent_mode, MultiAgentMode::Proactive);
}
#[tokio::test]
@@ -987,7 +984,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
.expect("child thread should be registered");
assert_eq!(
child_thread.config_snapshot().await.multi_agent_mode,
Some(MultiAgentMode::Proactive)
MultiAgentMode::Proactive
);
assert_ne!(child_thread_id, parent_thread_id);
let history = child_thread.codex.session.clone_history().await;
@@ -1025,18 +1022,13 @@ 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
let mut no_hint_child_config = harness.config.clone();
let _ = no_hint_child_config.features.enable(Feature::MultiAgentV2);
no_hint_child_config.multi_agent_v2.subagent_usage_hint_text = None;
let no_hint_child_thread_id = harness
.control
.spawn_agent_with_metadata(
disabled_hint_child_config,
no_hint_child_config,
text_input("child task without hints"),
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
@@ -1052,24 +1044,17 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
},
)
.await
.expect("forked spawn should honor disabled usage hints")
.expect("forked spawn should honor an empty subagent usage hint")
.thread_id;
let disabled_hint_child_thread = harness
let no_hint_child_thread = harness
.manager
.get_thread(disabled_hint_child_thread_id)
.get_thread(no_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;
.expect("no-hint child thread should be registered");
let no_hint_history = no_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"
!history_contains_text(no_hint_history.raw_items(), "Child subagent guidance."),
"full-history forked child should not add empty subagent guidance"
);
let expected = (
@@ -1099,9 +1084,9 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
.expect("child shutdown should submit");
let _ = harness
.control
.shutdown_live_agent(disabled_hint_child_thread_id)
.shutdown_live_agent(no_hint_child_thread_id)
.await
.expect("disabled-hint child shutdown should submit");
.expect("no-hint child shutdown should submit");
let _ = parent_thread
.submit(Op::Shutdown {})
.await
@@ -1406,7 +1391,7 @@ async fn spawn_agent_fork_last_n_turns_keeps_only_recent_turns() {
.expect("child thread should be registered");
assert_eq!(
child_thread.config_snapshot().await.multi_agent_mode,
Some(MultiAgentMode::Proactive)
MultiAgentMode::Proactive
);
let history = child_thread.codex.session.clone_history().await;
@@ -2422,10 +2407,7 @@ async fn resume_thread_subagent_restores_stored_metadata_and_effective_multi_age
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)
);
assert_eq!(resumed_snapshot.multi_agent_mode, MultiAgentMode::Proactive);
let _ = harness
.control
+1 -1
View File
@@ -72,7 +72,7 @@ pub struct ThreadConfigSnapshot {
pub reasoning_summary: Option<ReasoningSummary>,
pub personality: Option<Personality>,
pub collaboration_mode: CollaborationMode,
pub multi_agent_mode: Option<MultiAgentMode>,
pub multi_agent_mode: MultiAgentMode,
pub session_source: SessionSource,
pub forked_from_thread_id: Option<ThreadId>,
pub parent_thread_id: Option<ThreadId>,
-2
View File
@@ -10114,7 +10114,6 @@ max_concurrent_threads_per_session = 5
min_wait_timeout_ms = 2500
max_wait_timeout_ms = 120000
default_wait_timeout_ms = 30000
usage_hint_enabled = false
usage_hint_text = "Custom delegation guidance."
root_agent_usage_hint_text = "Root guidance."
subagent_usage_hint_text = "Subagent guidance."
@@ -10142,7 +10141,6 @@ non_code_mode_only = true
),
(None, Some(4))
);
assert!(!config.multi_agent_v2.usage_hint_enabled);
assert_eq!(
config.multi_agent_v2.usage_hint_text.as_deref(),
Some("Custom delegation guidance.")
-8
View File
@@ -241,8 +241,6 @@ You may also see them addressed as to=/root/..., which indicates your identity i
"#;
const DEFAULT_MULTI_AGENT_V2_SHARED_USAGE_HINT_TEXT: &str = r#"Note that collaboration tools cannot be called from inside `functions.exec`. Call `spawn_agent`, `send_message`, `followup_task`, `wait_agent`, `interrupt_agent`, and `list_agents` only as direct tool calls using the recipient shown in their tool definitions, such as `to=functions.spawn_agent` without a configured namespace or `to=functions.agents.spawn_agent` with `tool_namespace = "agents"`, since they are intentionally absent from the `functions.exec` `tools.*` namespace. Available tools in `functions.exec` are explicitly described with a `tools` namespace in the developer message.
The goal is to correctly solve the problem in as little time as possible. Therefore, if at any point you can parallelize work by delegating tasks to another agent, you should do so to save time.
All agents share the same directory. In detail:
- All agents have access to the same container and filesystem as you.
- All agents use the same current working directory.
@@ -1128,7 +1126,6 @@ pub struct MultiAgentV2Config {
pub min_wait_timeout_ms: i64,
pub max_wait_timeout_ms: i64,
pub default_wait_timeout_ms: i64,
pub usage_hint_enabled: bool,
pub usage_hint_text: Option<String>,
pub root_agent_usage_hint_text: Option<String>,
pub subagent_usage_hint_text: Option<String>,
@@ -1144,7 +1141,6 @@ impl MultiAgentV2Config {
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_usage_hint_text(
DEFAULT_MULTI_AGENT_V2_ROOT_AGENT_USAGE_HINT_TEXT,
@@ -2492,9 +2488,6 @@ fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config
let default_wait_timeout_ms = base
.and_then(|config| config.default_wait_timeout_ms)
.unwrap_or(default.default_wait_timeout_ms);
let usage_hint_enabled = base
.and_then(|config| config.usage_hint_enabled)
.unwrap_or(default.usage_hint_enabled);
let usage_hint_text = base
.and_then(|config| config.usage_hint_text.as_ref())
.cloned()
@@ -2523,7 +2516,6 @@ fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config
min_wait_timeout_ms,
max_wait_timeout_ms,
default_wait_timeout_ms,
usage_hint_enabled,
usage_hint_text,
root_agent_usage_hint_text,
subagent_usage_hint_text,
@@ -4,6 +4,7 @@ use codex_protocol::protocol::MULTI_AGENT_MODE_CLOSE_TAG;
use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG;
const EXPLICIT_REQUEST_ONLY_MULTI_AGENT_MODE_TEXT: &str = "Do not spawn sub-agents unless the user explicitly asks for sub-agents, delegation, or parallel agent work.";
const NO_MULTI_AGENT_MODE_TEXT: &str = "Multi-agent delegation mode instructions are inactive. Any earlier multi-agent mode developer message no longer applies.";
const PROACTIVE_MULTI_AGENT_MODE_TEXT: &str = "Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Use sub-agents when parallel work would materially improve speed or quality. This mode remains active until a later multi-agent mode developer message changes it.";
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -32,6 +33,7 @@ impl ContextualUserFragment for MultiAgentModeInstructions {
fn body(&self) -> String {
match self.multi_agent_mode {
MultiAgentMode::None => NO_MULTI_AGENT_MODE_TEXT.to_string(),
MultiAgentMode::ExplicitRequestOnly => {
EXPLICIT_REQUEST_ONLY_MULTI_AGENT_MODE_TEXT.to_string()
}
+3 -2
View File
@@ -103,10 +103,8 @@ fn build_multi_agent_mode_update_item(
) -> Option<String> {
let effective_multi_agent_mode = crate::session::multi_agents::effective_multi_agent_mode(
next.multi_agent_version,
&next.config.multi_agent_v2,
&next.session_source,
next.multi_agent_mode,
next.config.features.enabled(Feature::MultiAgentMode),
);
let previous = previous?;
if previous.multi_agent_mode == effective_multi_agent_mode {
@@ -114,6 +112,9 @@ fn build_multi_agent_mode_update_item(
}
match effective_multi_agent_mode {
Some(MultiAgentMode::None) => {
Some(MultiAgentModeInstructions::new(MultiAgentMode::None).render())
}
Some(multi_agent_mode) => Some(MultiAgentModeInstructions::new(multi_agent_mode).render()),
None if previous.multi_agent_mode == Some(MultiAgentMode::Proactive) => {
Some(MultiAgentModeInstructions::new(MultiAgentMode::ExplicitRequestOnly).render())
-1
View File
@@ -327,7 +327,6 @@ mod tests {
min_wait_timeout_ms: Some(_),
max_wait_timeout_ms: Some(_),
default_wait_timeout_ms: Some(_),
usage_hint_enabled: Some(_),
hide_spawn_agent_metadata: Some(_),
..
})
+11 -10
View File
@@ -586,7 +586,7 @@ impl Codex {
.await;
let multi_agent_version =
resolve_multi_agent_version(&conversation_history, inherited_multi_agent_version);
let multi_agent_mode = initial_multi_agent_mode;
let multi_agent_mode = initial_multi_agent_mode.unwrap_or_default();
config
.validate_multi_agent_v2_config()
.map_err(|err| CodexErr::InvalidRequest(err.to_string()))?;
@@ -3290,19 +3290,20 @@ impl Session {
{
items.push(usage_hint_message);
}
if let Some(multi_agent_mode) = multi_agents::effective_multi_agent_mode(
match multi_agents::effective_multi_agent_mode(
turn_context.multi_agent_version,
&turn_context.config.multi_agent_v2,
&session_source,
turn_context.multi_agent_mode,
turn_context
.config
.features
.enabled(Feature::MultiAgentMode),
) {
items.push(ContextualUserFragment::into(
MultiAgentModeInstructions::new(multi_agent_mode),
));
Some(
multi_agent_mode
@ (MultiAgentMode::ExplicitRequestOnly | MultiAgentMode::Proactive),
) => {
items.push(ContextualUserFragment::into(
MultiAgentModeInstructions::new(multi_agent_mode),
));
}
Some(MultiAgentMode::None) | None => {}
}
if let Some(contextual_user_message) =
crate::context_manager::updates::build_contextual_user_message(contextual_user_sections)
+12 -23
View File
@@ -14,10 +14,6 @@ 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;
}
configured_usage_hint_text_for_source(multi_agent_v2, session_source)
}
@@ -39,30 +35,23 @@ fn configured_usage_hint_text_for_source<'a>(
}
}
fn multi_agent_mode_is_applicable(
multi_agent_version: MultiAgentVersion,
multi_agent_v2: &MultiAgentV2Config,
session_source: &SessionSource,
) -> bool {
multi_agent_version == MultiAgentVersion::V2
&& multi_agent_v2.usage_hint_enabled
&& configured_usage_hint_text_for_source(multi_agent_v2, session_source).is_some()
}
pub(crate) fn effective_multi_agent_mode(
multi_agent_version: MultiAgentVersion,
multi_agent_v2: &MultiAgentV2Config,
session_source: &SessionSource,
requested_multi_agent_mode: Option<MultiAgentMode>,
multi_agent_mode_enabled: bool,
multi_agent_mode: MultiAgentMode,
) -> Option<MultiAgentMode> {
if !multi_agent_mode_is_applicable(multi_agent_version, multi_agent_v2, session_source) {
if multi_agent_version != MultiAgentVersion::V2 {
return None;
}
Some(if multi_agent_mode_enabled {
requested_multi_agent_mode.unwrap_or_default()
} else {
MultiAgentMode::ExplicitRequestOnly
})
match session_source {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. })
| SessionSource::Cli
| SessionSource::VSCode
| SessionSource::Exec
| SessionSource::Mcp
| SessionSource::Custom(_)
| SessionSource::Unknown => Some(multi_agent_mode),
SessionSource::Internal(_) | SessionSource::SubAgent(_) => None,
}
}
+2 -2
View File
@@ -52,7 +52,7 @@ pub(crate) struct SessionConfiguration {
pub(super) provider: ModelProviderInfo,
pub(super) collaboration_mode: CollaborationMode,
pub(super) multi_agent_mode: Option<MultiAgentMode>,
pub(super) multi_agent_mode: MultiAgentMode,
pub(super) model_reasoning_summary: Option<ReasoningSummaryConfig>,
pub(super) service_tier: Option<String>,
@@ -231,7 +231,7 @@ impl SessionConfiguration {
next_configuration.collaboration_mode = collaboration_mode;
}
if let Some(multi_agent_mode) = updates.multi_agent_mode {
next_configuration.multi_agent_mode = Some(multi_agent_mode);
next_configuration.multi_agent_mode = multi_agent_mode;
}
if let Some(summary) = updates.reasoning_summary {
next_configuration.model_reasoning_summary = Some(summary);
+3 -4
View File
@@ -7895,15 +7895,14 @@ 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() {
async fn build_initial_context_omits_multi_agent_v2_usage_hints_when_hint_is_empty() {
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());
config.multi_agent_v2.root_agent_usage_hint_text = None;
config.multi_agent_v2.subagent_usage_hint_text = None;
},
)
.await;
+1 -3
View File
@@ -125,7 +125,7 @@ pub struct TurnContext {
pub(crate) developer_instructions: Option<String>,
pub(crate) user_instructions: Option<String>,
pub(crate) collaboration_mode: CollaborationMode,
pub(crate) multi_agent_mode: Option<MultiAgentMode>,
pub(crate) multi_agent_mode: MultiAgentMode,
pub(crate) multi_agent_version: MultiAgentVersion,
pub(crate) personality: Option<Personality>,
pub(crate) approval_policy: Constrained<AskForApproval>,
@@ -386,10 +386,8 @@ impl TurnContext {
multi_agent_version: Some(self.multi_agent_version),
multi_agent_mode: super::multi_agents::effective_multi_agent_mode(
self.multi_agent_version,
&self.config.multi_agent_v2,
&self.session_source,
self.multi_agent_mode,
self.config.features.enabled(Feature::MultiAgentMode),
),
realtime_active: Some(self.realtime_active),
effort: self.reasoning_effort.clone(),
+1 -1
View File
@@ -976,7 +976,7 @@ impl ThreadManager {
};
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,
Ok(thread) => Some(thread.config_snapshot().await.multi_agent_mode),
Err(_) => history.get_latest_effective_multi_agent_mode(),
},
None => history.get_latest_effective_multi_agent_mode(),
@@ -24,7 +24,6 @@ pub struct SpawnAgentToolOptions {
pub available_models: Vec<ModelPreset>,
pub agent_type_description: String,
pub hide_agent_type_model_reasoning: bool,
pub include_usage_hint: bool,
pub usage_hint_text: Option<String>,
}
@@ -66,7 +65,6 @@ pub fn create_spawn_agent_tool_v1(options: SpawnAgentToolOptions) -> ToolSpec {
available_models_description.as_deref(),
inherited_model_guidance,
return_value_description,
options.include_usage_hint,
options.usage_hint_text,
),
strict: false,
@@ -99,7 +97,6 @@ pub fn create_spawn_agent_tool_v2(options: SpawnAgentToolOptions) -> ToolSpec {
description: spawn_agent_tool_description_v2(
available_models_description.as_deref(),
inherited_model_guidance,
options.include_usage_hint,
options.usage_hint_text,
),
strict: false,
@@ -648,7 +645,6 @@ fn spawn_agent_tool_description(
available_models_description: Option<&str>,
inherited_model_guidance: Option<&str>,
return_value_description: &str,
include_usage_hint: bool,
usage_hint_text: Option<String>,
) -> String {
let agent_role_guidance = available_models_description.unwrap_or_default();
@@ -660,9 +656,6 @@ fn spawn_agent_tool_description(
Spawn a sub-agent for a well-scoped task. {return_value_description} {inherited_model_guidance}"#
);
if !include_usage_hint {
return tool_description;
}
if let Some(usage_hint_text) = usage_hint_text {
return format!(
r#"
@@ -711,7 +704,6 @@ Do not spawn sub-agents unless the user explicitly asks for sub-agents, delegati
fn spawn_agent_tool_description_v2(
available_models_description: Option<&str>,
inherited_model_guidance: Option<&str>,
include_usage_hint: bool,
usage_hint_text: Option<String>,
) -> String {
let agent_role_guidance = available_models_description.unwrap_or_default();
@@ -731,9 +723,6 @@ The new agent's canonical task name will be provided to it along with the messag
Note that passing `fork_turns="none"` will not pass any surrounding context to the spawned subagent, which may cause the agent to lack the context it needs to complete its task, whereas `fork_turns="all"` will provide the subagent with all surrounding context."#
);
if !include_usage_hint {
return tool_description;
}
if let Some(usage_hint_text) = usage_hint_text {
return format!(
r#"
@@ -45,7 +45,6 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
],
agent_type_description: "role help".to_string(),
hide_agent_type_model_reasoning: false,
include_usage_hint: true,
usage_hint_text: None,
});
@@ -121,7 +120,6 @@ fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() {
available_models: Vec::new(),
agent_type_description: "role help".to_string(),
hide_agent_type_model_reasoning: false,
include_usage_hint: true,
usage_hint_text: None,
});
@@ -178,7 +176,6 @@ fn spawn_agent_tool_caps_visible_model_summaries() {
],
agent_type_description: "role help".to_string(),
hide_agent_type_model_reasoning: false,
include_usage_hint: true,
usage_hint_text: None,
});
@@ -222,7 +219,6 @@ fn spawn_agent_tool_hides_service_tier_with_spawn_metadata() {
available_models: vec![model_preset("visible", /*show_in_picker*/ true)],
agent_type_description: "role help".to_string(),
hide_agent_type_model_reasoning: true,
include_usage_hint: true,
usage_hint_text: None,
});
@@ -1148,11 +1148,7 @@ 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);
turn.multi_agent_mode = MultiAgentMode::Proactive;
set_turn_config(&mut turn, config);
let session = Arc::new(session);
@@ -1191,10 +1187,7 @@ 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_eq!(child_snapshot.multi_agent_mode, MultiAgentMode::Proactive);
assert!(manager.captured_ops().iter().any(|(id, op)| {
*id == child_thread_id
&& matches!(
@@ -52,12 +52,8 @@ async fn handle_spawn_agent(
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
-2
View File
@@ -788,7 +788,6 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mu
.config
.multi_agent_v2
.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(),
}),
tool_namespace,
@@ -832,7 +831,6 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mu
available_models: turn_context.available_models.clone(),
agent_type_description,
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(),
}),
exposure,
+120 -127
View File
@@ -19,6 +19,7 @@ use pretty_assertions::assert_eq;
use serde_json::Value;
const NO_SPAWN_TEXT: &str = "Do not spawn sub-agents unless the user explicitly asks for sub-agents, delegation, or parallel agent work.";
const NO_MODE_TEXT: &str = "Multi-agent delegation mode instructions are inactive.";
const PROACTIVE_TEXT: &str = "Proactive multi-agent delegation is active.";
fn developer_texts(input: &[Value]) -> Vec<&str> {
@@ -66,7 +67,7 @@ async fn multi_agent_mode_is_sticky_and_emits_only_on_change() -> Result<()> {
let server = start_mock_server().await;
let responses = mount_sse_sequence(
&server,
(1..=3)
(1..=5)
.map(|index| {
sse(vec![
ev_response_created(&format!("resp-{index}")),
@@ -82,18 +83,24 @@ async fn multi_agent_mode_is_sticky_and_emits_only_on_change() -> Result<()> {
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
config
.features
.enable(Feature::MultiAgentMode)
.expect("test config should allow feature update");
})
.build(&server)
.await?;
submit_turn(&test.codex, "turn one", /*mode*/ None).await?;
assert_eq!(test.codex.config_snapshot().await.multi_agent_mode, None);
assert_eq!(
test.codex.config_snapshot().await.multi_agent_mode,
MultiAgentMode::ExplicitRequestOnly
);
submit_turn(&test.codex, "turn two", Some(MultiAgentMode::Proactive)).await?;
submit_turn(&test.codex, "turn three", /*mode*/ None).await?;
submit_turn(&test.codex, "turn four", Some(MultiAgentMode::None)).await?;
submit_turn(&test.codex, "turn five", /*mode*/ None).await?;
assert_eq!(
test.codex.config_snapshot().await.multi_agent_mode,
MultiAgentMode::None
);
let requests = responses.requests();
let inputs = requests
@@ -103,6 +110,8 @@ async fn multi_agent_mode_is_sticky_and_emits_only_on_change() -> Result<()> {
let first = developer_texts(&inputs[0]);
let second = developer_texts(&inputs[1]);
let third = developer_texts(&inputs[2]);
let fourth = developer_texts(&inputs[3]);
let fifth = developer_texts(&inputs[4]);
assert_eq!(
(
@@ -128,12 +137,102 @@ async fn multi_agent_mode_is_sticky_and_emits_only_on_change() -> Result<()> {
),
(2, 1, 1)
);
assert_eq!(
(
count_containing(&fourth, MULTI_AGENT_MODE_OPEN_TAG),
count_containing(&fourth, NO_SPAWN_TEXT),
count_containing(&fourth, PROACTIVE_TEXT),
count_containing(&fourth, NO_MODE_TEXT),
),
(3, 1, 1, 1)
);
assert_eq!(
(
count_containing(&fifth, MULTI_AGENT_MODE_OPEN_TAG),
count_containing(&fifth, NO_SPAWN_TEXT),
count_containing(&fifth, PROACTIVE_TEXT),
count_containing(&fifth, NO_MODE_TEXT),
),
(3, 1, 1, 1)
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn multi_agent_mode_feature_uses_explicit_mode_when_disabled() -> Result<()> {
async fn multi_agent_mode_none_omits_instructions_and_survives_resume() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let responses = mount_sse_sequence(
&server,
(1..=2)
.map(|index| {
sse(vec![
ev_response_created(&format!("resp-{index}")),
ev_completed(&format!("resp-{index}")),
])
})
.collect(),
)
.await;
let initial = test_codex()
.with_config(|config| {
config
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
})
.build(&server)
.await?;
let home = initial.home.clone();
let rollout_path = initial
.session_configured
.rollout_path
.clone()
.expect("rollout path");
submit_turn(&initial.codex, "before resume", Some(MultiAgentMode::None)).await?;
assert_eq!(
initial.codex.config_snapshot().await.multi_agent_mode,
MultiAgentMode::None
);
drop(initial);
let mut resume_builder = test_codex().with_config(|config| {
config
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
});
let resumed = resume_builder.resume(&server, home, rollout_path).await?;
submit_turn(&resumed.codex, "after resume", /*mode*/ None).await?;
assert_eq!(
resumed.codex.config_snapshot().await.multi_agent_mode,
MultiAgentMode::None
);
let requests = responses.requests();
assert_eq!(requests.len(), 2);
for request in requests {
let input = request.input();
let texts = developer_texts(&input);
assert_eq!(
(
count_containing(&texts, MULTI_AGENT_MODE_OPEN_TAG),
count_containing(&texts, NO_SPAWN_TEXT),
count_containing(&texts, PROACTIVE_TEXT),
count_containing(&texts, NO_MODE_TEXT),
),
(0, 0, 0, 0)
);
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn multi_agent_mode_applies_without_usage_hint_text() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
@@ -148,21 +247,21 @@ async fn multi_agent_mode_feature_uses_explicit_mode_when_disabled() -> Result<(
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
config.multi_agent_v2.root_agent_usage_hint_text = None;
})
.build(&server)
.await?;
submit_turn(&test.codex, "hello", /*mode*/ None).await?;
submit_turn(&test.codex, "hello", Some(MultiAgentMode::Proactive)).await?;
let input = responses.single_request().input();
let texts = developer_texts(&input);
assert_eq!(
(
count_containing(&texts, MULTI_AGENT_MODE_OPEN_TAG),
count_containing(&texts, NO_SPAWN_TEXT),
count_containing(&texts, PROACTIVE_TEXT),
),
(1, 1, 0)
(1, 1)
);
Ok(())
@@ -175,7 +274,7 @@ async fn resume_compares_against_previous_effective_multi_agent_mode() -> Result
let server = start_mock_server().await;
let responses = mount_sse_sequence(
&server,
(1..=4)
(1..=2)
.map(|index| {
sse(vec![
ev_response_created(&format!("resp-{index}")),
@@ -214,18 +313,14 @@ async fn resume_compares_against_previous_effective_multi_agent_mode() -> Result
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
config
.features
.enable(Feature::MultiAgentMode)
.expect("test config should allow feature update");
});
let resumed = resume_builder.resume(&server, home, rollout_path).await?;
submit_turn(
&resumed.codex,
"after resume",
Some(MultiAgentMode::Proactive),
)
.await?;
submit_turn(&resumed.codex, "after resume", /*mode*/ None).await?;
assert_eq!(
resumed.codex.config_snapshot().await.multi_agent_mode,
MultiAgentMode::Proactive
);
let requests = responses.requests();
let resumed_input = requests[1].input();
@@ -236,108 +331,14 @@ async fn resume_compares_against_previous_effective_multi_agent_mode() -> Result
count_containing(&texts, NO_SPAWN_TEXT),
count_containing(&texts, PROACTIVE_TEXT),
),
(2, 1, 1)
);
let resumed_rollout_path = resumed
.session_configured
.rollout_path
.clone()
.expect("resumed rollout path");
let resumed_home = resumed.home.clone();
drop(resumed);
let mut same_mode_resume_builder = test_codex().with_config(|config| {
config
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
config
.features
.enable(Feature::MultiAgentMode)
.expect("test config should allow feature update");
});
let resumed_same_mode = same_mode_resume_builder
.resume(&server, resumed_home, resumed_rollout_path)
.await?;
submit_turn(
&resumed_same_mode.codex,
"after same-mode resume",
/*mode*/ None,
)
.await?;
assert_eq!(
resumed_same_mode
.codex
.config_snapshot()
.await
.multi_agent_mode,
Some(MultiAgentMode::Proactive)
);
let requests = responses.requests();
let resumed_same_mode_input = requests[2].input();
let texts = developer_texts(&resumed_same_mode_input);
assert_eq!(
(
count_containing(&texts, MULTI_AGENT_MODE_OPEN_TAG),
count_containing(&texts, NO_SPAWN_TEXT),
count_containing(&texts, PROACTIVE_TEXT),
),
(2, 1, 1)
);
let resumed_same_mode_rollout_path = resumed_same_mode
.session_configured
.rollout_path
.clone()
.expect("same-mode resumed rollout path");
let resumed_same_mode_home = resumed_same_mode.home.clone();
drop(resumed_same_mode);
let mut disabled_mode_resume_builder = test_codex().with_config(|config| {
config
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
});
let resumed_disabled_mode = disabled_mode_resume_builder
.resume(
&server,
resumed_same_mode_home,
resumed_same_mode_rollout_path,
)
.await?;
submit_turn(
&resumed_disabled_mode.codex,
"after disabled-mode resume",
/*mode*/ None,
)
.await?;
assert_eq!(
resumed_disabled_mode
.codex
.config_snapshot()
.await
.multi_agent_mode,
Some(MultiAgentMode::Proactive)
);
let requests = responses.requests();
let resumed_disabled_mode_input = requests[3].input();
let texts = developer_texts(&resumed_disabled_mode_input);
assert_eq!(
(
count_containing(&texts, MULTI_AGENT_MODE_OPEN_TAG),
count_containing(&texts, NO_SPAWN_TEXT),
count_containing(&texts, PROACTIVE_TEXT),
),
(3, 2, 1)
(1, 0, 1)
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn explicit_multi_agent_mode_is_retained_without_multi_agent_v2() -> Result<()> {
async fn multi_agent_mode_is_retained_without_multi_agent_v2() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
@@ -346,21 +347,13 @@ async fn explicit_multi_agent_mode_is_retained_without_multi_agent_v2() -> Resul
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let test = test_codex()
.with_config(|config| {
config
.features
.enable(Feature::MultiAgentMode)
.expect("test config should allow feature update");
})
.build(&server)
.await?;
let test = test_codex().build(&server).await?;
submit_turn(&test.codex, "hello", Some(MultiAgentMode::Proactive)).await?;
assert_eq!(
test.codex.config_snapshot().await.multi_agent_mode,
Some(MultiAgentMode::Proactive)
MultiAgentMode::Proactive
);
let input = responses.single_request().input();
let texts = developer_texts(&input);
+1
View File
@@ -46,6 +46,7 @@ pub struct MultiAgentV2ConfigToml {
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 0, max = 3600000))]
pub default_wait_timeout_ms: Option<i64>,
/// Deprecated compatibility field. Its value is ignored.
#[serde(skip_serializing_if = "Option::is_none")]
pub usage_hint_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
+2 -2
View File
@@ -145,7 +145,7 @@ pub enum Feature {
Collab,
/// Enable task-path-based multi-agent routing.
MultiAgentV2,
/// Enable per-turn multi-agent mode selection.
/// Removed compatibility flag retained as a no-op.
MultiAgentMode,
/// Enable CSV-backed agent job tools.
SpawnCsv,
@@ -1034,7 +1034,7 @@ pub const FEATURES: &[FeatureSpec] = &[
FeatureSpec {
id: Feature::MultiAgentMode,
key: "multi_agent_mode",
stage: Stage::UnderDevelopment,
stage: Stage::Removed,
default_enabled: false,
},
FeatureSpec {
-39
View File
@@ -604,45 +604,6 @@ non_code_mode_only = true
);
}
#[test]
fn multi_agent_v2_feature_config_usage_hint_enabled_does_not_enable_feature() {
let features_toml: FeaturesToml = toml::from_str(
r#"
[multi_agent_v2]
usage_hint_enabled = false
"#,
)
.expect("features table should deserialize");
let features = Features::from_sources(
FeatureConfigSource {
features: Some(&features_toml),
..Default::default()
},
FeatureConfigSource::default(),
FeatureOverrides::default(),
);
assert_eq!(features.enabled(Feature::MultiAgentV2), false);
assert_eq!(features_toml.entries(), BTreeMap::new());
assert_eq!(
features_toml.multi_agent_v2,
Some(crate::FeatureToml::Config(crate::MultiAgentV2ConfigToml {
enabled: None,
max_concurrent_threads_per_session: None,
min_wait_timeout_ms: None,
max_wait_timeout_ms: None,
default_wait_timeout_ms: None,
usage_hint_enabled: Some(false),
usage_hint_text: None,
root_agent_usage_hint_text: None,
subagent_usage_hint_text: None,
tool_namespace: None,
hide_spawn_agent_metadata: None,
non_code_mode_only: None,
}))
);
}
#[test]
fn materialize_resolved_enabled_writes_all_features_and_preserves_custom_config() {
let mut features = Features::with_defaults();
+5 -2
View File
@@ -296,8 +296,10 @@ pub enum Personality {
Pragmatic,
}
/// Controls whether the model should only spawn sub-agents after an explicit
/// user request or may delegate proactively when doing so would help.
/// Controls whether the model receives multi-agent delegation instructions and,
/// when it does, whether it should only spawn sub-agents after an explicit user
/// request or may delegate proactively when doing so would help. `none` leaves
/// the multi-agent tools available without injecting delegation instructions.
#[derive(
Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS, Default,
)]
@@ -305,6 +307,7 @@ pub enum Personality {
#[ts(rename_all = "camelCase")]
#[strum(serialize_all = "camelCase")]
pub enum MultiAgentMode {
None,
#[default]
ExplicitRequestOnly,
Proactive,
+1 -1
View File
@@ -2005,7 +2005,7 @@ pub struct ThreadSettingsSnapshot {
pub personality: Option<Personality>,
pub collaboration_mode: CollaborationMode,
#[serde(default)]
pub multi_agent_mode: Option<MultiAgentMode>,
pub multi_agent_mode: MultiAgentMode,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, TS)]