From aedb8f345a221b22105daad9fdf080988026fbca Mon Sep 17 00:00:00 2001 From: Shijie Rao Date: Wed, 24 Jun 2026 20:13:52 -0700 Subject: [PATCH] [codex] Add Ultra reasoning effort (#29899) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ultra should be one user-facing reasoning selection for work that benefits from both maximum reasoning and proactive multi-agent delegation. Without it, clients must coordinate maximum reasoning with the experimental `multiAgentMode` setting, even though the inference backend still expects its existing `max` effort value. This change makes reasoning effort the source of truth: clients select `ultra`, core derives proactive multi-agent behavior when the turn is eligible for multi-agent V2, and inference requests continue to use the backend-compatible `max` value. - Add `ultra` as a first-class reasoning effort and preserve model-catalog ordering when exposing it to clients. - Convert `ultra` to `max` at the inference request boundary, including Responses HTTP/WebSocket requests, startup prewarm, compaction, and memory summarization. - Derive effective multi-agent mode per turn from effective reasoning effort: - eligible multi-agent V2 + `ultra` → `proactive` - eligible multi-agent V2 + any other effort → `explicitRequestOnly` - V1 or otherwise ineligible sessions → no multi-agent mode instruction - Keep the derived effective mode in turn context history so successive turns can emit a developer-message update only when the effective mode changes. - Remove selected multi-agent mode from core session configuration, turn construction, thread settings, resume/fork restoration, and subagent spawn plumbing. Subagents inherit reasoning effort and derive their own effective mode. - Retain the experimental app-server `multiAgentMode` fields for wire compatibility while marking them deprecated. Request values are accepted but ignored; compatibility response fields report `explicitRequestOnly`. - Display Ultra in the TUI using the order supplied by `model/list`. - `just test -p codex-core ultra_reasoning_uses_max_for_requests` - `just test -p codex-tui model_reasoning_selection_popup` --- .../src/protocol/v2/thread.rs | 14 +- .../src/protocol/v2/turn.rs | 4 +- codex-rs/app-server/README.md | 10 +- .../request_processors/thread_lifecycle.rs | 4 +- .../request_processors/thread_processor.rs | 11 +- .../thread_processor_tests.rs | 1 - .../src/request_processors/thread_summary.rs | 6 +- .../src/request_processors/turn_processor.rs | 8 - codex-rs/app-server/src/thread_state.rs | 4 +- .../tests/suite/v2/thread_settings_update.rs | 106 ------ .../app-server/tests/suite/v2/turn_start.rs | 82 +---- codex-rs/core/src/agent/control.rs | 2 - .../core/src/agent/control/residency_tests.rs | 1 - codex-rs/core/src/agent/control/spawn.rs | 6 - codex-rs/core/src/agent/control_tests.rs | 93 +----- codex-rs/core/src/client.rs | 23 +- codex-rs/core/src/client_tests.rs | 15 + codex-rs/core/src/codex_delegate.rs | 1 - codex-rs/core/src/codex_thread.rs | 5 - codex-rs/core/src/context_manager/updates.rs | 6 +- codex-rs/core/src/session/handlers.rs | 3 - codex-rs/core/src/session/mod.rs | 10 +- codex-rs/core/src/session/multi_agents.rs | 16 +- codex-rs/core/src/session/review.rs | 1 - codex-rs/core/src/session/session.rs | 6 - codex-rs/core/src/session/tests.rs | 8 - .../core/src/session/tests/guardian_tests.rs | 1 - codex-rs/core/src/session/turn_context.rs | 9 +- codex-rs/core/src/thread_manager.rs | 28 -- codex-rs/core/src/thread_manager_tests.rs | 5 - .../src/tools/handlers/multi_agents/spawn.rs | 1 - .../src/tools/handlers/multi_agents_tests.rs | 3 - .../tools/handlers/multi_agents_v2/spawn.rs | 6 - codex-rs/core/tests/common/test_codex.rs | 1 - codex-rs/core/tests/suite/agents_md.rs | 2 - codex-rs/core/tests/suite/multi_agent_mode.rs | 304 +++++------------- .../tests/suite/subagent_notifications.rs | 1 - codex-rs/memories/write/src/runtime.rs | 1 - codex-rs/protocol/src/openai_models.rs | 8 + codex-rs/protocol/src/protocol.rs | 5 - codex-rs/tui/src/chatwidget/model_popups.rs | 1 + 41 files changed, 172 insertions(+), 650 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs index 6a51f71e51..238e87d2af 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs @@ -94,9 +94,7 @@ pub struct ThreadStartParams { pub developer_instructions: Option, #[ts(optional = nullable)] pub personality: Option, - /// Set the initial multi-agent mode for this thread. `none` leaves the - /// multi-agent tools available without injecting mode instructions. - /// Omitted defaults to `explicitRequestOnly`. + /// @deprecated Ignored. Use Ultra reasoning effort for proactive multi-agent behavior. #[experimental("thread/start.multiAgentMode")] #[ts(optional = nullable)] pub multi_agent_mode: Option, @@ -186,7 +184,7 @@ pub struct ThreadStartResponse { #[serde(default)] pub active_permission_profile: Option, pub reasoning_effort: Option, - /// Current multi-agent mode for this thread. + /// @deprecated Always `explicitRequestOnly`. Use `reasoningEffort` for Ultra behavior. #[experimental("thread/start.multiAgentMode")] #[serde(default)] pub multi_agent_mode: MultiAgentMode, @@ -250,7 +248,7 @@ pub struct ThreadSettingsUpdateParams { #[experimental("thread/settings/update.collaborationMode")] #[ts(optional = nullable)] pub collaboration_mode: Option, - /// Select the multi-agent mode for subsequent turns. + /// @deprecated Ignored. Use `effort: "ultra"` for proactive multi-agent behavior. #[experimental("thread/settings/update.multiAgentMode")] #[ts(optional = nullable)] pub multi_agent_mode: Option, @@ -279,7 +277,7 @@ pub struct ThreadSettings { pub effort: Option, pub summary: Option, pub collaboration_mode: CollaborationMode, - /// Current multi-agent mode for this thread. + /// @deprecated Always `explicitRequestOnly`. Use `effort` for Ultra behavior. #[experimental("thread/settings.multiAgentMode")] #[serde(default)] pub multi_agent_mode: MultiAgentMode, @@ -419,7 +417,7 @@ pub struct ThreadResumeResponse { #[serde(default)] pub active_permission_profile: Option, pub reasoning_effort: Option, - /// Current multi-agent mode for this thread. + /// @deprecated Always `explicitRequestOnly`. Use `reasoningEffort` for Ultra behavior. #[experimental("thread/resume.multiAgentMode")] #[serde(default)] pub multi_agent_mode: MultiAgentMode, @@ -578,7 +576,7 @@ pub struct ThreadForkResponse { #[serde(default)] pub active_permission_profile: Option, pub reasoning_effort: Option, - /// Current multi-agent mode for this thread. + /// @deprecated Always `explicitRequestOnly`. Use `reasoningEffort` for Ultra behavior. #[experimental("thread/fork.multiAgentMode")] #[serde(default)] pub multi_agent_mode: MultiAgentMode, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/turn.rs b/codex-rs/app-server-protocol/src/protocol/v2/turn.rs index 1fc35d1515..af99b4b2e7 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/turn.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/turn.rs @@ -151,9 +151,7 @@ pub struct TurnStartParams { #[ts(optional = nullable)] pub collaboration_mode: Option, - /// 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. + /// @deprecated Ignored. Use `effort: "ultra"` for proactive multi-agent behavior. #[experimental("turn/start.multiAgentMode")] #[ts(optional = nullable)] pub multi_agent_mode: Option, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 50f0052094..0a4efd429f 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -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 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/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`. Deprecated experimental `multiAgentMode` is ignored; use Ultra reasoning effort for proactive multi-agent behavior. 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`. - `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 mode. 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 deprecated experimental `multiAgentMode` field, and the corresponding thread setting, always report `explicitRequestOnly`; Ultra reasoning effort is the source of proactive multi-agent behavior. - `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 thread’s 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 thread’s 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/settings/update` — experimental; queue a partial update to a loaded thread’s next-turn settings without starting a turn or adding transcript items. Omitted fields leave settings unchanged; `serviceTier: null` clears the tier; deprecated `multiAgentMode` is ignored, while Ultra reasoning effort enables proactive multi-agent behavior; `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 thread’s 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 agent’s 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 `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. +- `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". Deprecated experimental `multiAgentMode` is ignored; Ultra reasoning effort selects proactive behavior. - `thread/inject_items` — append raw Responses API items to a loaded thread’s 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"`. diff --git a/codex-rs/app-server/src/request_processors/thread_lifecycle.rs b/codex-rs/app-server/src/request_processors/thread_lifecycle.rs index 21adf46134..03031c87f8 100644 --- a/codex-rs/app-server/src/request_processors/thread_lifecycle.rs +++ b/codex-rs/app-server/src/request_processors/thread_lifecycle.rs @@ -1,4 +1,5 @@ use super::*; +use codex_protocol::config_types::MultiAgentMode; pub(super) const THREAD_UNLOADING_DELAY: Duration = Duration::from_secs(30 * 60); @@ -639,7 +640,6 @@ 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; @@ -662,7 +662,7 @@ pub(super) async fn handle_pending_thread_resume_request( sandbox, active_permission_profile, reasoning_effort, - multi_agent_mode, + multi_agent_mode: MultiAgentMode::ExplicitRequestOnly, initial_turns_page, }; outgoing.send_response(request_id, response).await; diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index a3e4b49ce8..5c7b28b342 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -903,7 +903,7 @@ impl ThreadRequestProcessor { mock_experimental_field: _mock_experimental_field, experimental_raw_events, personality, - multi_agent_mode, + multi_agent_mode: _multi_agent_mode, ephemeral, session_start_source, thread_source, @@ -957,7 +957,6 @@ impl ThreadRequestProcessor { supports_openai_form_elicitation, config, typesafe_overrides, - multi_agent_mode, dynamic_tools, selected_capability_roots.unwrap_or_default(), session_start_source, @@ -1032,7 +1031,6 @@ impl ThreadRequestProcessor { supports_openai_form_elicitation: bool, config_overrides: Option>, typesafe_overrides: ConfigOverrides, - multi_agent_mode: Option, dynamic_tools: Option>, selected_capability_roots: Vec, session_start_source: Option, @@ -1156,7 +1154,6 @@ impl ThreadRequestProcessor { thread_source, dynamic_tools, metrics_service_name: service_name, - multi_agent_mode, parent_trace: request_trace, environments, thread_extension_init, @@ -1262,7 +1259,7 @@ impl ThreadRequestProcessor { sandbox, active_permission_profile, reasoning_effort: config_snapshot.reasoning_effort, - multi_agent_mode: config_snapshot.multi_agent_mode, + multi_agent_mode: MultiAgentMode::ExplicitRequestOnly, }; let notif = thread_started_notification(thread); listener_task_context @@ -2793,7 +2790,7 @@ impl ThreadRequestProcessor { sandbox, active_permission_profile, reasoning_effort: session_configured.reasoning_effort, - multi_agent_mode: config_snapshot.multi_agent_mode, + multi_agent_mode: MultiAgentMode::ExplicitRequestOnly, initial_turns_page, }; @@ -3515,7 +3512,7 @@ impl ThreadRequestProcessor { sandbox, active_permission_profile, reasoning_effort: session_configured.reasoning_effort, - multi_agent_mode: config_snapshot.multi_agent_mode, + multi_agent_mode: MultiAgentMode::ExplicitRequestOnly, }; let notif = thread_started_notification(thread); diff --git a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs index 1f5ae12f44..230e1a381d 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs @@ -776,7 +776,6 @@ mod thread_processor_behavior_tests { developer_instructions: None, }, }, - multi_agent_mode: Default::default(), session_source: SessionSource::Cli, forked_from_thread_id: None, parent_thread_id: None, diff --git a/codex-rs/app-server/src/request_processors/thread_summary.rs b/codex-rs/app-server/src/request_processors/thread_summary.rs index bb2c6708cd..ef9a86696a 100644 --- a/codex-rs/app-server/src/request_processors/thread_summary.rs +++ b/codex-rs/app-server/src/request_processors/thread_summary.rs @@ -3,6 +3,7 @@ use super::*; use chrono::DateTime; #[cfg(test)] use chrono::Utc; +use codex_protocol::config_types::MultiAgentMode; #[cfg(test)] pub(crate) async fn read_summary_from_rollout( @@ -205,7 +206,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, + multi_agent_mode: MultiAgentMode::ExplicitRequestOnly, personality: config_snapshot.personality, } } @@ -226,7 +227,6 @@ 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 { @@ -243,7 +243,7 @@ pub(crate) fn thread_settings_from_core_snapshot( effort: reasoning_effort, summary: reasoning_summary, collaboration_mode, - multi_agent_mode, + multi_agent_mode: MultiAgentMode::ExplicitRequestOnly, personality, } } diff --git a/codex-rs/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs index f7b4f7c14e..b339c7798a 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -1,5 +1,4 @@ use super::*; -use codex_protocol::config_types::MultiAgentMode; use codex_protocol::models::ContentItem; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::protocol::AdditionalContextEntry as CoreAdditionalContextEntry; @@ -117,7 +116,6 @@ struct ThreadSettingsBuildParams { effort: Option, summary: Option, collaboration_mode: Option, - multi_agent_mode: Option, personality: Option, } @@ -514,7 +512,6 @@ impl TurnRequestProcessor { effort: params.effort, summary: params.summary, collaboration_mode: params.collaboration_mode, - multi_agent_mode: params.multi_agent_mode, personality: params.personality, }, ) @@ -621,7 +618,6 @@ impl TurnRequestProcessor { effort, summary, collaboration_mode, - multi_agent_mode, personality, } = params; @@ -655,7 +651,6 @@ impl TurnRequestProcessor { || effort.is_some() || summary.is_some() || collaboration_mode.is_some() - || multi_agent_mode.is_some() || personality.is_some(); let runtime_workspace_roots = @@ -732,7 +727,6 @@ impl TurnRequestProcessor { summary, service_tier: service_tier.clone(), collaboration_mode: collaboration_mode.clone(), - multi_agent_mode, personality, }) .await @@ -756,7 +750,6 @@ impl TurnRequestProcessor { summary, service_tier, collaboration_mode, - multi_agent_mode, personality, }) } @@ -787,7 +780,6 @@ impl TurnRequestProcessor { effort: params.effort, summary: params.summary, collaboration_mode: params.collaboration_mode, - multi_agent_mode: params.multi_agent_mode, personality: params.personality, }, ) diff --git a/codex-rs/app-server/src/thread_state.rs b/codex-rs/app-server/src/thread_state.rs index e4e4e42adf..fec786373c 100644 --- a/codex-rs/app-server/src/thread_state.rs +++ b/codex-rs/app-server/src/thread_state.rs @@ -10,6 +10,8 @@ use codex_core::CodexThread; use codex_core::ThreadConfigSnapshot; use codex_file_watcher::WatchRegistration; use codex_protocol::ThreadId; +#[cfg(test)] +use codex_protocol::config_types::MultiAgentMode; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::RolloutItem; use codex_rollout::state_db::StateDbHandle; @@ -241,7 +243,7 @@ mod tests { developer_instructions: None, }, }, - multi_agent_mode: Default::default(), + multi_agent_mode: MultiAgentMode::ExplicitRequestOnly, personality: None, } } diff --git a/codex-rs/app-server/tests/suite/v2/thread_settings_update.rs b/codex-rs/app-server/tests/suite/v2/thread_settings_update.rs index 9ad25ca992..29dbee26d1 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_settings_update.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_settings_update.rs @@ -22,10 +22,7 @@ use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::UserInput as V2UserInput; use codex_core::test_support::all_model_presets; -use codex_features::Feature; -use codex_protocol::config_types::MultiAgentMode; use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE; -use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG; use core_test_support::responses; use pretty_assertions::assert_eq; use serde_json::Value; @@ -97,109 +94,6 @@ async fn thread_settings_update_emits_notification_and_updates_future_turns() -> Ok(()) } -#[tokio::test] -async fn thread_settings_update_multi_agent_mode_applies_to_future_turns() -> Result<()> { - let server = responses::start_mock_server().await; - let response_mock = responses::mount_sse_sequence( - &server, - (1..=2) - .map(|index| { - responses::sse(vec![ - responses::ev_response_created(&format!("resp-{index}")), - responses::ev_assistant_message(&format!("msg-{index}"), "done"), - responses::ev_completed(&format!("resp-{index}")), - ]) - }) - .collect(), - ) - .await; - let codex_home = TempDir::new()?; - write_mock_responses_config_toml( - codex_home.path(), - &server.uri(), - &BTreeMap::from([(Feature::MultiAgentV2, true)]), - /*auto_compact_limit*/ 200_000, - /*requires_openai_auth*/ None, - "mock_provider", - "compact", - )?; - - let mut mcp = TestAppServer::new(codex_home.path()).await?; - timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; - let thread = start_thread(&mut mcp).await?.thread; - - start_text_turn(&mut mcp, thread.id.clone()).await?; - timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_notification_message("turn/completed"), - ) - .await??; - assert_eq!(response_mock.requests().len(), 1); - - send_thread_settings_update( - &mut mcp, - ThreadSettingsUpdateParams { - thread_id: thread.id.clone(), - multi_agent_mode: Some(MultiAgentMode::Proactive), - ..Default::default() - }, - ) - .await?; - assert_eq!( - response_mock.requests().len(), - 1, - "settings-only update should not start a model request" - ); - - let updated = read_thread_settings_updated(&mut mcp).await?; - assert_eq!(updated.thread_id, thread.id); - assert_eq!( - updated.thread_settings.multi_agent_mode, - MultiAgentMode::Proactive - ); - - start_text_turn(&mut mcp, thread.id).await?; - timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_notification_message("turn/completed"), - ) - .await??; - - let requests = response_mock.requests(); - let first_developer_texts = requests[0].message_input_texts("developer"); - let second_developer_texts = requests[1].message_input_texts("developer"); - assert_eq!( - first_developer_texts - .iter() - .filter(|text| text.contains(MULTI_AGENT_MODE_OPEN_TAG)) - .count(), - 1 - ); - assert_eq!( - second_developer_texts - .iter() - .filter(|text| text.contains(MULTI_AGENT_MODE_OPEN_TAG)) - .count(), - 2 - ); - assert_eq!( - second_developer_texts - .iter() - .filter(|text| text.contains("Proactive multi-agent delegation is active.")) - .count(), - 1 - ); - assert_eq!( - second_developer_texts - .iter() - .filter(|text| text - .contains("Do not spawn sub-agents unless the user explicitly asks for sub-agents")) - .count(), - 1 - ); - Ok(()) -} - #[tokio::test] async fn thread_settings_update_cwd_retargets_default_environment() -> Result<()> { let server = responses::start_mock_server().await; diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index dd211a5d89..1f73a3c88c 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -1753,7 +1753,7 @@ async fn turn_start_accepts_personality_override_v2() -> Result<()> { } #[tokio::test] -async fn turn_start_accepts_multi_agent_mode_v2() -> Result<()> { +async fn turn_start_ignores_deprecated_multi_agent_mode() -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -1816,18 +1816,19 @@ async fn turn_start_accepts_multi_agent_mode_v2() -> Result<()> { .single_request() .message_input_texts("developer"); assert!(developer_texts.iter().any(|text| { - text.contains("") - && text.contains("Proactive multi-agent delegation is active.") - })); - assert!(!developer_texts.iter().any(|text| { text.contains("Do not spawn sub-agents unless the user explicitly asks for sub-agents") })); + assert!( + !developer_texts + .iter() + .any(|text| text.contains("Proactive multi-agent delegation is active.")) + ); Ok(()) } #[tokio::test] -async fn thread_start_multi_agent_mode_initializes_first_turn() -> Result<()> { +async fn thread_start_ignores_deprecated_multi_agent_mode() -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -1866,7 +1867,7 @@ async fn thread_start_multi_agent_mode_initializes_first_turn() -> Result<()> { multi_agent_mode, .. } = to_response::(thread_resp)?; - assert_eq!(multi_agent_mode, MultiAgentMode::Proactive); + assert_eq!(multi_agent_mode, MultiAgentMode::ExplicitRequestOnly); let turn_req = mcp .send_turn_start_request(TurnStartParams { @@ -1894,71 +1895,20 @@ async fn thread_start_multi_agent_mode_initializes_first_turn() -> Result<()> { let developer_texts = response_mock .single_request() .message_input_texts("developer"); + assert!(developer_texts.iter().any(|text| { + text.contains(MULTI_AGENT_MODE_OPEN_TAG) + && text + .contains("Do not spawn sub-agents unless the user explicitly asks for sub-agents") + })); assert!( - developer_texts.iter().any(|text| { - text.contains(MULTI_AGENT_MODE_OPEN_TAG) - && text.contains("Proactive multi-agent delegation is active.") - }), - "expected proactive multi-agent mode instructions in developer input, got {developer_texts:?}" + !developer_texts + .iter() + .any(|text| text.contains("Proactive multi-agent delegation is active.")) ); Ok(()) } -#[tokio::test] -async fn thread_start_reports_multi_agent_mode() -> Result<()> { - skip_if_no_network!(Ok(())); - - let cases = [ - ( - BTreeMap::from([(Feature::MultiAgentV2, true)]), - Some(MultiAgentMode::Proactive), - MultiAgentMode::Proactive, - ), - ( - BTreeMap::from([(Feature::MultiAgentV2, true)]), - Some(MultiAgentMode::None), - MultiAgentMode::None, - ), - ( - BTreeMap::new(), - Some(MultiAgentMode::Proactive), - MultiAgentMode::Proactive, - ), - ( - BTreeMap::from([(Feature::MultiAgentV2, true)]), - None, - MultiAgentMode::ExplicitRequestOnly, - ), - ]; - - for (features, requested_multi_agent_mode, expected_multi_agent_mode) in cases { - let server = responses::start_mock_server().await; - let codex_home = TempDir::new()?; - create_config_toml(codex_home.path(), &server.uri(), "never", &features)?; - - let mut mcp = TestAppServer::new(codex_home.path()).await?; - timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; - let thread_req = mcp - .send_thread_start_request(ThreadStartParams { - model: Some("mock-model".to_string()), - multi_agent_mode: requested_multi_agent_mode, - ..Default::default() - }) - .await?; - let thread_resp: JSONRPCResponse = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), - ) - .await??; - let response = to_response::(thread_resp)?; - - assert_eq!(response.multi_agent_mode, expected_multi_agent_mode); - } - - Ok(()) -} - #[tokio::test] async fn turn_start_change_personality_mid_thread_v2() -> Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index 2f8d035435..410c97cbd5 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -19,7 +19,6 @@ 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; @@ -69,7 +68,6 @@ pub(crate) struct SpawnAgentOptions { pub(crate) fork_mode: Option, pub(crate) parent_thread_id: Option, pub(crate) environments: Option>, - pub(crate) initial_multi_agent_mode: Option, } #[derive(Clone, Debug)] diff --git a/codex-rs/core/src/agent/control/residency_tests.rs b/codex-rs/core/src/agent/control/residency_tests.rs index f91a77364a..cf043971e2 100644 --- a/codex-rs/core/src/agent/control/residency_tests.rs +++ b/codex-rs/core/src/agent/control/residency_tests.rs @@ -142,7 +142,6 @@ 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, diff --git a/codex-rs/core/src/agent/control/spawn.rs b/codex-rs/core/src/agent/control/spawn.rs index 05fac6aaf3..cbb8aed0e4 100644 --- a/codex-rs/core/src/agent/control/spawn.rs +++ b/codex-rs/core/src/agent/control/spawn.rs @@ -1,13 +1,11 @@ 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, exec_policy: Option>, - inherited_multi_agent_mode: Option, } fn default_agent_nickname_list() -> Vec<&'static str> { @@ -239,7 +237,6 @@ 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 { @@ -286,7 +283,6 @@ 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(), @@ -392,7 +388,6 @@ 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( @@ -517,7 +512,6 @@ 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(), diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index e37e9b04db..9231cb7b37 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -14,7 +14,6 @@ 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; @@ -817,42 +816,6 @@ 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, MultiAgentMode::Proactive); -} - #[tokio::test] async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { let harness = AgentControlHarness::new().await; @@ -875,15 +838,6 @@ 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; @@ -978,7 +932,6 @@ 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() }, ) @@ -991,10 +944,6 @@ 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, - MultiAgentMode::Proactive - ); assert_ne!(child_thread_id, parent_thread_id); let history = child_thread.codex.session.clone_history().await; let mut expected_final_answer = @@ -1287,15 +1236,6 @@ 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()) @@ -1380,7 +1320,6 @@ 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() }, ) @@ -1393,10 +1332,6 @@ 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, - MultiAgentMode::Proactive - ); let history = child_thread.codex.session.clone_history().await; assert!( @@ -2244,7 +2179,7 @@ async fn spawn_thread_subagent_uses_role_specific_nickname_candidates() { } #[tokio::test] -async fn resume_thread_subagent_restores_stored_metadata_and_effective_multi_agent_mode() { +async fn resume_thread_subagent_restores_stored_metadata() { let (home, mut config) = test_config().await; config .features @@ -2266,7 +2201,7 @@ async fn resume_thread_subagent_restores_stored_metadata_and_effective_multi_age 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"); @@ -2291,18 +2226,6 @@ async fn resume_thread_subagent_restores_stored_metadata_and_effective_multi_age .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 @@ -2313,16 +2236,7 @@ async fn resume_thread_subagent_restores_stored_metadata_and_effective_multi_age .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"); + .expect("flush child rollout"); let mut status_rx = harness .control .subscribe_status(child_thread_id) @@ -2411,7 +2325,6 @@ 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, MultiAgentMode::Proactive); let _ = harness .control diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 8ebea984d6..8cdf281fff 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -162,6 +162,13 @@ pub(crate) struct CompactConversationRequestSettings { pub(crate) service_tier: Option, } +fn reasoning_effort_for_request(effort: ReasoningEffortConfig) -> ReasoningEffortConfig { + match effort { + ReasoningEffortConfig::Ultra => ReasoningEffortConfig::Custom("max".to_string()), + effort => effort, + } +} + /// Session-scoped state shared by all [`ModelClient`] clones. /// /// This is intentionally kept minimal so `ModelClient` does not need to hold a full `Config`. Most @@ -647,11 +654,13 @@ impl ModelClient { let payload = ApiMemorySummarizeInput { model: model_info.slug.clone(), raw_memories, - reasoning: effort.map(|effort| Reasoning { - effort: Some(effort), - summary: None, - context: None, - }), + reasoning: effort + .map(reasoning_effort_for_request) + .map(|effort| Reasoning { + effort: Some(effort), + summary: None, + context: None, + }), }; client @@ -749,7 +758,9 @@ impl ModelClient { ) -> Option { if model_info.supports_reasoning_summaries { Some(Reasoning { - effort: effort.or_else(|| model_info.default_reasoning_level.clone()), + effort: effort + .or_else(|| model_info.default_reasoning_level.clone()) + .map(reasoning_effort_for_request), summary: if summary == ReasoningSummaryConfig::None { None } else { diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs index 9df7d4d01d..5311a55e36 100644 --- a/codex-rs/core/src/client_tests.rs +++ b/codex-rs/core/src/client_tests.rs @@ -31,6 +31,7 @@ use codex_protocol::ThreadId; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ModelInfo; +use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::InternalSessionSource; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; @@ -153,6 +154,20 @@ fn test_session_telemetry() -> SessionTelemetry { ) } +#[test] +fn ultra_reasoning_uses_max_for_requests() { + assert_eq!( + ( + super::reasoning_effort_for_request(ReasoningEffort::Ultra), + super::reasoning_effort_for_request(ReasoningEffort::High), + ), + ( + ReasoningEffort::Custom("max".to_string()), + ReasoningEffort::High, + ) + ); +} + #[derive(Default)] struct TagCollectorVisitor { tags: BTreeMap, diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index 9f5b867f75..dab902ec37 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -122,7 +122,6 @@ 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??; diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index a522cd62aa..23a7523001 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -8,7 +8,6 @@ use codex_otel::SessionTelemetry; use codex_protocol::ThreadId; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::CollaborationMode; -use codex_protocol::config_types::MultiAgentMode; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::WindowsSandboxLevel; @@ -72,7 +71,6 @@ pub struct ThreadConfigSnapshot { pub reasoning_summary: Option, pub personality: Option, pub collaboration_mode: CollaborationMode, - pub multi_agent_mode: MultiAgentMode, pub session_source: SessionSource, pub forked_from_thread_id: Option, pub parent_thread_id: Option, @@ -153,7 +151,6 @@ pub struct CodexThreadSettingsOverrides { pub summary: Option, pub service_tier: Option>, pub collaboration_mode: Option, - pub multi_agent_mode: Option, pub personality: Option, } @@ -374,7 +371,6 @@ impl CodexThread { summary, service_tier, collaboration_mode, - multi_agent_mode, personality, } = overrides; let collaboration_mode = if let Some(collaboration_mode) = collaboration_mode { @@ -398,7 +394,6 @@ impl CodexThread { active_permission_profile, windows_sandbox_level, collaboration_mode: Some(collaboration_mode), - multi_agent_mode, reasoning_summary: summary, service_tier, personality, diff --git a/codex-rs/core/src/context_manager/updates.rs b/codex-rs/core/src/context_manager/updates.rs index 937c4bf059..5e81bc2a7d 100644 --- a/codex-rs/core/src/context_manager/updates.rs +++ b/codex-rs/core/src/context_manager/updates.rs @@ -78,11 +78,7 @@ fn build_multi_agent_mode_update_item( previous: Option<&TurnContextItem>, next: &TurnContext, ) -> Option { - let effective_multi_agent_mode = crate::session::multi_agents::effective_multi_agent_mode( - next.multi_agent_version, - &next.session_source, - next.multi_agent_mode, - ); + let effective_multi_agent_mode = crate::session::multi_agents::effective_multi_agent_mode(next); let previous = previous?; if previous.multi_agent_mode == effective_multi_agent_mode { return None; diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index 39e91d6cf9..5c68893e7f 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -124,7 +124,6 @@ async fn thread_settings_update( summary, service_tier, collaboration_mode, - multi_agent_mode, personality, } = thread_settings; let collaboration_mode = match collaboration_mode { @@ -150,7 +149,6 @@ async fn thread_settings_update( active_permission_profile, windows_sandbox_level, collaboration_mode: Some(collaboration_mode), - multi_agent_mode, reasoning_summary: summary, service_tier, personality, @@ -178,7 +176,6 @@ 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, }, }) } diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index cdf866c391..8467a78e5e 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -454,7 +454,6 @@ pub(crate) struct CodexSpawnArgs { pub(crate) attestation_provider: Option>, pub(crate) external_time_provider: Option>, pub(crate) inherited_multi_agent_version: Option, - pub(crate) initial_multi_agent_mode: Option, } pub(crate) fn resolve_multi_agent_version( @@ -539,7 +538,6 @@ 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(); @@ -594,7 +592,6 @@ 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.unwrap_or_default(); config .validate_multi_agent_v2_config() .map_err(|err| CodexErr::InvalidRequest(err.to_string()))?; @@ -628,7 +625,6 @@ impl Codex { let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, - multi_agent_mode, model_reasoning_summary: config.model_reasoning_summary, service_tier, developer_instructions: config.developer_instructions.clone(), @@ -3330,11 +3326,7 @@ impl Session { { items.push(usage_hint_message); } - match multi_agents::effective_multi_agent_mode( - turn_context.multi_agent_version, - &session_source, - turn_context.multi_agent_mode, - ) { + match multi_agents::effective_multi_agent_mode(turn_context) { Some( multi_agent_mode @ (MultiAgentMode::ExplicitRequestOnly | MultiAgentMode::Proactive), diff --git a/codex-rs/core/src/session/multi_agents.rs b/codex-rs/core/src/session/multi_agents.rs index 82fcfd1282..385f2281ba 100644 --- a/codex-rs/core/src/session/multi_agents.rs +++ b/codex-rs/core/src/session/multi_agents.rs @@ -1,6 +1,7 @@ use crate::config::MultiAgentV2Config; use crate::session::turn_context::TurnContext; use codex_protocol::config_types::MultiAgentMode; +use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::MultiAgentVersion; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; @@ -35,16 +36,17 @@ fn configured_usage_hint_text_for_source<'a>( } } -pub(crate) fn effective_multi_agent_mode( - multi_agent_version: MultiAgentVersion, - session_source: &SessionSource, - multi_agent_mode: MultiAgentMode, -) -> Option { - if multi_agent_version != MultiAgentVersion::V2 { +pub(crate) fn effective_multi_agent_mode(turn_context: &TurnContext) -> Option { + if turn_context.multi_agent_version != MultiAgentVersion::V2 { return None; } - match session_source { + let multi_agent_mode = match turn_context.effective_reasoning_effort() { + Some(ReasoningEffort::Ultra) => MultiAgentMode::Proactive, + _ => MultiAgentMode::ExplicitRequestOnly, + }; + + match &turn_context.session_source { SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. }) | SessionSource::Cli | SessionSource::VSCode diff --git a/codex-rs/core/src/session/review.rs b/codex-rs/core/src/session/review.rs index 8986ffd052..69365fcffb 100644 --- a/codex-rs/core/src/session/review.rs +++ b/codex-rs/core/src/session/review.rs @@ -127,7 +127,6 @@ pub(super) async fn spawn_review_thread( developer_instructions: None, user_instructions: None, collaboration_mode: parent_turn_context.collaboration_mode.clone(), - multi_agent_mode: parent_turn_context.multi_agent_mode, multi_agent_version: MultiAgentVersion::Disabled, personality: parent_turn_context.personality, approval_policy: parent_turn_context.approval_policy.clone(), diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index b4740bea99..eda6fcbc84 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -52,7 +52,6 @@ pub(crate) struct SessionConfiguration { pub(super) provider: ModelProviderInfo, pub(super) collaboration_mode: CollaborationMode, - pub(super) multi_agent_mode: MultiAgentMode, pub(super) model_reasoning_summary: Option, pub(super) service_tier: Option, @@ -193,7 +192,6 @@ impl SessionConfiguration { reasoning_summary: self.model_reasoning_summary, personality: self.personality, collaboration_mode: self.collaboration_mode.clone(), - multi_agent_mode: self.multi_agent_mode, session_source: self.session_source.clone(), forked_from_thread_id: self.forked_from_thread_id, parent_thread_id: self.parent_thread_id, @@ -230,9 +228,6 @@ impl SessionConfiguration { if let Some(collaboration_mode) = updates.collaboration_mode.clone() { next_configuration.collaboration_mode = collaboration_mode; } - if let Some(multi_agent_mode) = updates.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); } @@ -427,7 +422,6 @@ pub(crate) struct SessionSettingsUpdate { pub(crate) active_permission_profile: Option, pub(crate) windows_sandbox_level: Option, pub(crate) collaboration_mode: Option, - pub(crate) multi_agent_mode: Option, pub(crate) reasoning_summary: Option, pub(crate) service_tier: Option>, pub(crate) final_output_json_schema: Option>, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 378dff58c9..e859b1c842 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -3436,7 +3436,6 @@ async fn set_rate_limits_retains_previous_credits() { let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, - multi_agent_mode: Default::default(), model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), loaded_agents_md: None, @@ -3543,7 +3542,6 @@ async fn set_rate_limits_updates_plan_type_when_present() { let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, - multi_agent_mode: Default::default(), model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), loaded_agents_md: None, @@ -4072,7 +4070,6 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, - multi_agent_mode: Default::default(), model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), loaded_agents_md: None, @@ -4939,7 +4936,6 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() { let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, - multi_agent_mode: Default::default(), model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), loaded_agents_md: None, @@ -5052,7 +5048,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, - multi_agent_mode: Default::default(), model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), loaded_agents_md: None, @@ -5298,7 +5293,6 @@ async fn make_session_with_config_and_rx( let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, - multi_agent_mode: Default::default(), model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), loaded_agents_md: None, @@ -5405,7 +5399,6 @@ async fn make_session_with_history_source_and_agent_control_and_rx( let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, - multi_agent_mode: Default::default(), model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), loaded_agents_md: None, @@ -7121,7 +7114,6 @@ where let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, - multi_agent_mode: Default::default(), model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), loaded_agents_md: None, diff --git a/codex-rs/core/src/session/tests/guardian_tests.rs b/codex-rs/core/src/session/tests/guardian_tests.rs index e44b12ef72..08e22dd34c 100644 --- a/codex-rs/core/src/session/tests/guardian_tests.rs +++ b/codex-rs/core/src/session/tests/guardian_tests.rs @@ -738,7 +738,6 @@ 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"); diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index b2c50cf96a..0d65033d23 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -125,7 +125,6 @@ pub struct TurnContext { pub(crate) developer_instructions: Option, pub(crate) user_instructions: Option, pub(crate) collaboration_mode: CollaborationMode, - pub(crate) multi_agent_mode: MultiAgentMode, pub(crate) multi_agent_version: MultiAgentVersion, pub(crate) personality: Option, pub(crate) approval_policy: Constrained, @@ -278,7 +277,6 @@ impl TurnContext { developer_instructions: self.developer_instructions.clone(), user_instructions: self.user_instructions.clone(), collaboration_mode, - multi_agent_mode: self.multi_agent_mode, multi_agent_version: self.multi_agent_version, personality: self.personality, approval_policy: self.approval_policy.clone(), @@ -384,11 +382,7 @@ impl TurnContext { personality: self.personality, collaboration_mode: Some(self.collaboration_mode.clone()), multi_agent_version: Some(self.multi_agent_version), - multi_agent_mode: super::multi_agents::effective_multi_agent_mode( - self.multi_agent_version, - &self.session_source, - self.multi_agent_mode, - ), + multi_agent_mode: super::multi_agents::effective_multi_agent_mode(self), realtime_active: Some(self.realtime_active), effort: self.reasoning_effort.clone(), summary: ReasoningSummaryConfig::Auto, @@ -571,7 +565,6 @@ impl Session { .as_ref() .map(LoadedAgentsMd::render), collaboration_mode: session_configuration.collaboration_mode.clone(), - multi_agent_mode: session_configuration.multi_agent_mode, multi_agent_version, personality: session_configuration.personality, approval_policy: session_configuration.approval_policy.clone(), diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index deba2864b6..55a58289f3 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -36,7 +36,6 @@ 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; @@ -185,7 +184,6 @@ pub struct StartThreadOptions { pub thread_source: Option, pub dynamic_tools: Vec, pub metrics_service_name: Option, - pub multi_agent_mode: Option, pub parent_trace: Option, pub environments: Vec, pub thread_extension_init: ExtensionDataInit, @@ -609,7 +607,6 @@ impl ThreadManager { thread_source: None, dynamic_tools, metrics_service_name: None, - multi_agent_mode: None, parent_trace: None, environments, thread_extension_init: ExtensionDataInit::default(), @@ -649,7 +646,6 @@ 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, @@ -734,7 +730,6 @@ 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, @@ -746,7 +741,6 @@ impl ThreadManager { thread_source, Vec::new(), /*metrics_service_name*/ None, - initial_multi_agent_mode, /*inherited_environments*/ None, /*inherited_exec_policy*/ None, parent_trace, @@ -779,7 +773,6 @@ 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(), @@ -806,7 +799,6 @@ 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, @@ -818,7 +810,6 @@ impl ThreadManager { thread_source, Vec::new(), /*metrics_service_name*/ None, - initial_multi_agent_mode, /*inherited_environments*/ None, /*inherited_exec_policy*/ None, /*parent_trace*/ None, @@ -974,13 +965,6 @@ impl ThreadManager { 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) => Some(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( @@ -1009,7 +993,6 @@ impl ThreadManager { thread_source, Vec::new(), /*metrics_service_name*/ None, - initial_multi_agent_mode, parent_trace, environments, /*thread_extension_init*/ ExtensionDataInit::default(), @@ -1234,7 +1217,6 @@ 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, @@ -1252,7 +1234,6 @@ impl ThreadManagerState { forked_from_thread_id: Option, thread_source: Option, metrics_service_name: Option, - initial_multi_agent_mode: Option, inherited_environments: Option, inherited_exec_policy: Option>, environments: Option>, @@ -1271,7 +1252,6 @@ impl ThreadManagerState { thread_source, Vec::new(), metrics_service_name, - initial_multi_agent_mode, inherited_environments, inherited_exec_policy, /*parent_trace*/ None, @@ -1299,7 +1279,6 @@ 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, @@ -1311,7 +1290,6 @@ impl ThreadManagerState { thread_source, Vec::new(), /*metrics_service_name*/ None, - initial_multi_agent_mode, inherited_environments, inherited_exec_policy, /*parent_trace*/ None, @@ -1333,7 +1311,6 @@ impl ThreadManagerState { thread_source: Option, parent_thread_id: Option, forked_from_thread_id: Option, - initial_multi_agent_mode: Option, inherited_environments: Option, inherited_exec_policy: Option>, environments: Option>, @@ -1352,7 +1329,6 @@ impl ThreadManagerState { thread_source, Vec::new(), /*metrics_service_name*/ None, - initial_multi_agent_mode, inherited_environments, inherited_exec_policy, /*parent_trace*/ None, @@ -1377,7 +1353,6 @@ impl ThreadManagerState { thread_source: Option, dynamic_tools: Vec, metrics_service_name: Option, - initial_multi_agent_mode: Option, parent_trace: Option, environments: Vec, thread_extension_init: ExtensionDataInit, @@ -1395,7 +1370,6 @@ impl ThreadManagerState { thread_source, dynamic_tools, metrics_service_name, - initial_multi_agent_mode, /*inherited_environments*/ None, /*inherited_exec_policy*/ None, parent_trace, @@ -1420,7 +1394,6 @@ impl ThreadManagerState { thread_source: Option, dynamic_tools: Vec, metrics_service_name: Option, - initial_multi_agent_mode: Option, inherited_environments: Option, inherited_exec_policy: Option>, parent_trace: Option, @@ -1500,7 +1473,6 @@ 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 diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index afb103b883..780c85c437 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -322,7 +322,6 @@ 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(), @@ -463,7 +462,6 @@ 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"), @@ -479,7 +477,6 @@ 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"), @@ -572,7 +569,6 @@ 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(), @@ -856,7 +852,6 @@ 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(), diff --git a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs index fcad4bb62f..204385934e 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs @@ -132,7 +132,6 @@ 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 diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index 2561cd7ded..95816a1872 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -24,7 +24,6 @@ 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; @@ -1148,7 +1147,6 @@ 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"); - turn.multi_agent_mode = MultiAgentMode::Proactive; set_turn_config(&mut turn, config); let session = Arc::new(session); @@ -1187,7 +1185,6 @@ 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, MultiAgentMode::Proactive); assert!(manager.captured_ops().iter().any(|(id, op)| { *id == child_thread_id && matches!( diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs index 837257dec9..2186a4a6d6 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs @@ -49,11 +49,6 @@ 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.session_source, - turn.multi_agent_mode, - ); let role_name = args .agent_type .as_deref() @@ -134,7 +129,6 @@ 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, }, ), ) diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index 8784a4d4a0..c7c5cc3863 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -641,7 +641,6 @@ impl TestCodexBuilder { thread_source: None, dynamic_tools: Vec::new(), metrics_service_name: None, - multi_agent_mode: None, parent_trace: None, environments, thread_extension_init: Default::default(), diff --git a/codex-rs/core/tests/suite/agents_md.rs b/codex-rs/core/tests/suite/agents_md.rs index 9094461aae..c227eadae4 100644 --- a/codex-rs/core/tests/suite/agents_md.rs +++ b/codex-rs/core/tests/suite/agents_md.rs @@ -442,7 +442,6 @@ async fn loads_user_instructions_without_a_primary_environment() -> Result<()> { 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(), @@ -650,7 +649,6 @@ async fn multi_environment_thread_loads_every_project_and_keeps_creation_snapsho thread_source: None, dynamic_tools: Vec::new(), metrics_service_name: None, - multi_agent_mode: None, parent_trace: None, environments: vec![ TurnEnvironmentSelection { diff --git a/codex-rs/core/tests/suite/multi_agent_mode.rs b/codex-rs/core/tests/suite/multi_agent_mode.rs index 21ef31c492..451b9cd38c 100644 --- a/codex-rs/core/tests/suite/multi_agent_mode.rs +++ b/codex-rs/core/tests/suite/multi_agent_mode.rs @@ -1,6 +1,9 @@ use anyhow::Result; +use codex_core::config::Config; use codex_features::Feature; -use codex_protocol::config_types::MultiAgentMode; +use codex_protocol::openai_models::ModelInfo; +use codex_protocol::openai_models::ReasoningEffort; +use codex_protocol::openai_models::ReasoningEffortPreset; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG; use codex_protocol::protocol::Op; @@ -19,9 +22,30 @@ 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 add_ultra_reasoning(model_info: &mut ModelInfo) { + model_info.supports_reasoning_summaries = true; + model_info + .supported_reasoning_levels + .push(ReasoningEffortPreset { + effort: ReasoningEffort::Ultra, + description: "Ultra".to_string(), + }); +} + +fn configure_multi_agent_v2(config: &mut Config) { + config + .features + .enable(Feature::MultiAgentV2) + .expect("test config should allow feature update"); +} + +fn configure_ultra(config: &mut Config) { + configure_multi_agent_v2(config); + config.model_reasoning_effort = Some(ReasoningEffort::Ultra); +} + fn developer_texts(input: &[Value]) -> Vec<&str> { input .iter() @@ -39,7 +63,7 @@ fn count_containing(texts: &[&str], target: &str) -> usize { async fn submit_turn( codex: &codex_core::CodexThread, prompt: &str, - mode: Option, + effort: Option, ) -> Result<()> { codex .submit(Op::UserInput { @@ -51,7 +75,7 @@ async fn submit_turn( responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: ThreadSettingsOverrides { - multi_agent_mode: mode, + effort: effort.map(Some), ..Default::default() }, }) @@ -61,214 +85,43 @@ async fn submit_turn( } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn multi_agent_mode_is_sticky_and_emits_only_on_change() -> Result<()> { +async fn ultra_reasoning_uses_max_and_proactive_mode() -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; - let responses = mount_sse_sequence( - &server, - (1..=5) - .map(|index| { - sse(vec![ - ev_response_created(&format!("resp-{index}")), - ev_completed(&format!("resp-{index}")), - ]) - }) - .collect(), - ) - .await; - let test = test_codex() - .with_config(|config| { - config - .features - .enable(Feature::MultiAgentV2) - .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, - 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 - .iter() - .map(core_test_support::responses::ResponsesRequest::input) - .collect::>(); - 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!( - ( - count_containing(&first, MULTI_AGENT_MODE_OPEN_TAG), - count_containing(&first, NO_SPAWN_TEXT), - count_containing(&first, PROACTIVE_TEXT), - ), - (1, 1, 0) - ); - assert_eq!( - ( - count_containing(&second, MULTI_AGENT_MODE_OPEN_TAG), - count_containing(&second, NO_SPAWN_TEXT), - count_containing(&second, PROACTIVE_TEXT), - ), - (2, 1, 1) - ); - assert_eq!( - ( - count_containing(&third, MULTI_AGENT_MODE_OPEN_TAG), - count_containing(&third, NO_SPAWN_TEXT), - count_containing(&third, PROACTIVE_TEXT), - ), - (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_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; - let responses = mount_sse_once( + let response = mount_sse_once( &server, sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]), ) .await; let test = test_codex() - .with_config(|config| { - config - .features - .enable(Feature::MultiAgentV2) - .expect("test config should allow feature update"); - config.multi_agent_v2.root_agent_usage_hint_text = None; - }) + .with_model_info_override("gpt-5.4", add_ultra_reasoning) + .with_config(configure_ultra) .build(&server) .await?; - submit_turn(&test.codex, "hello", Some(MultiAgentMode::Proactive)).await?; + submit_turn(&test.codex, "hello", /*effort*/ None).await?; - let input = responses.single_request().input(); + let request = response.single_request(); + assert_eq!( + request.body_json()["reasoning"]["effort"].as_str(), + Some("max") + ); + 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), ), - (1, 1) + (0, 1) ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn resume_compares_against_previous_effective_multi_agent_mode() -> Result<()> { +async fn leaving_ultra_after_cold_resume_emits_explicit_mode() -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; @@ -285,12 +138,8 @@ async fn resume_compares_against_previous_effective_multi_agent_mode() -> Result ) .await; let initial = test_codex() - .with_config(|config| { - config - .features - .enable(Feature::MultiAgentV2) - .expect("test config should allow feature update"); - }) + .with_model_info_override("gpt-5.4", add_ultra_reasoning) + .with_config(configure_ultra) .build(&server) .await?; let home = initial.home.clone(); @@ -300,29 +149,27 @@ async fn resume_compares_against_previous_effective_multi_agent_mode() -> Result .clone() .expect("rollout path"); - submit_turn( - &initial.codex, - "before resume", - Some(MultiAgentMode::Proactive), - ) - .await?; + submit_turn(&initial.codex, "before resume", /*effort*/ None).await?; drop(initial); - let mut resume_builder = test_codex().with_config(|config| { - config - .features - .enable(Feature::MultiAgentV2) - .expect("test config should allow feature update"); - }); + let mut resume_builder = test_codex() + .with_model_info_override("gpt-5.4", add_ultra_reasoning) + .with_config(configure_ultra); 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::Proactive - ); + submit_turn(&resumed.codex, "after resume", Some(ReasoningEffort::High)).await?; let requests = responses.requests(); + assert_eq!( + ( + requests[0].body_json()["reasoning"]["effort"] + .as_str() + .map(str::to_string), + requests[1].body_json()["reasoning"]["effort"] + .as_str() + .map(str::to_string), + ), + (Some("max".to_string()), Some("high".to_string())) + ); let resumed_input = requests[1].input(); let texts = developer_texts(&resumed_input); assert_eq!( @@ -331,39 +178,40 @@ async fn resume_compares_against_previous_effective_multi_agent_mode() -> Result count_containing(&texts, NO_SPAWN_TEXT), count_containing(&texts, PROACTIVE_TEXT), ), - (1, 0, 1) + (2, 1, 1) ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn multi_agent_mode_is_retained_without_multi_agent_v2() -> Result<()> { +async fn ultra_on_multi_agent_v1_uses_max_without_mode_instructions() -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; - let responses = mount_sse_once( + let response = mount_sse_once( &server, sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]), ) .await; - let test = test_codex().build(&server).await?; + let test = test_codex() + .with_model_info_override("gpt-5.4", add_ultra_reasoning) + .with_config(|config| { + config.model_reasoning_effort = Some(ReasoningEffort::Ultra); + }) + .build(&server) + .await?; - submit_turn(&test.codex, "hello", Some(MultiAgentMode::Proactive)).await?; + submit_turn(&test.codex, "hello", /*effort*/ None).await?; + let request = response.single_request(); assert_eq!( - test.codex.config_snapshot().await.multi_agent_mode, - MultiAgentMode::Proactive + request.body_json()["reasoning"]["effort"].as_str(), + Some("max") ); - let input = responses.single_request().input(); + let input = request.input(); let texts = developer_texts(&input); - assert_eq!( - ( - count_containing(&texts, MULTI_AGENT_MODE_OPEN_TAG), - count_containing(&texts, PROACTIVE_TEXT), - ), - (0, 0) - ); + assert_eq!(count_containing(&texts, MULTI_AGENT_MODE_OPEN_TAG), 0); Ok(()) } diff --git a/codex-rs/core/tests/suite/subagent_notifications.rs b/codex-rs/core/tests/suite/subagent_notifications.rs index c49e474965..ba7e6e02c5 100644 --- a/codex-rs/core/tests/suite/subagent_notifications.rs +++ b/codex-rs/core/tests/suite/subagent_notifications.rs @@ -752,7 +752,6 @@ async fn subagent_stop_replaces_stop_and_skips_internal_subagents() -> Result<() 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(), diff --git a/codex-rs/memories/write/src/runtime.rs b/codex-rs/memories/write/src/runtime.rs index 0835e15520..6cc856ddb4 100644 --- a/codex-rs/memories/write/src/runtime.rs +++ b/codex-rs/memories/write/src/runtime.rs @@ -311,7 +311,6 @@ impl MemoryStartupContext { thread_source: Some(ThreadSource::MemoryConsolidation), dynamic_tools: Vec::new(), metrics_service_name: None, - multi_agent_mode: None, parent_trace: None, environments, thread_extension_init: Default::default(), diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index 5b95fee29d..f697a8107f 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -45,6 +45,7 @@ pub enum ReasoningEffort { Medium, High, XHigh, + Ultra, /// A model-defined effort value that this client does not know yet. Custom(String), } @@ -59,6 +60,7 @@ impl ReasoningEffort { Self::Medium => "medium", Self::High => "high", Self::XHigh => "xhigh", + Self::Ultra => "ultra", Self::Custom(effort) => effort, } } @@ -123,6 +125,7 @@ impl FromStr for ReasoningEffort { "medium" => Ok(Self::Medium), "high" => Ok(Self::High), "xhigh" => Ok(Self::XHigh), + "ultra" => Ok(Self::Ultra), "" => Err("reasoning_effort must not be empty".to_string()), effort => Ok(Self::Custom(effort.to_string())), } @@ -701,20 +704,25 @@ mod tests { let deserialized = from_str::(r#""max""#) .expect("custom reasoning effort should deserialize"); let serialized = to_string(&custom).expect("custom reasoning effort should serialize"); + let serialized_ultra = to_string(&ReasoningEffort::Ultra).expect("Ultra should serialize"); assert_eq!( ( "high".parse(), + "ultra".parse(), "max".parse(), deserialized, serialized, + serialized_ultra, custom.to_string(), ), ( Ok(ReasoningEffort::High), + Ok(ReasoningEffort::Ultra), Ok(custom.clone()), custom, r#""max""#.to_string(), + r#""ultra""#.to_string(), "max".to_string(), ) ); diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index e1fe605462..ead89809b1 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -486,9 +486,6 @@ pub struct ThreadSettingsOverrides { /// Takes precedence over model, effort, and developer instructions if set. pub collaboration_mode: Option, - /// Updated multi-agent mode for this turn and subsequent turns. - pub multi_agent_mode: Option, - /// Updated personality preference. pub personality: Option, } @@ -2019,8 +2016,6 @@ pub struct ThreadSettingsSnapshot { #[serde(skip_serializing_if = "Option::is_none")] pub personality: Option, pub collaboration_mode: CollaborationMode, - #[serde(default)] - pub multi_agent_mode: MultiAgentMode, } #[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, TS)] diff --git a/codex-rs/tui/src/chatwidget/model_popups.rs b/codex-rs/tui/src/chatwidget/model_popups.rs index 90fd6c2cc3..3042ff6c71 100644 --- a/codex-rs/tui/src/chatwidget/model_popups.rs +++ b/codex-rs/tui/src/chatwidget/model_popups.rs @@ -505,6 +505,7 @@ impl ChatWidget { ReasoningEffortConfig::Medium => "Medium".to_string(), ReasoningEffortConfig::High => "High".to_string(), ReasoningEffortConfig::XHigh => "Extra high".to_string(), + ReasoningEffortConfig::Ultra => "Ultra".to_string(), ReasoningEffortConfig::Custom(value) => value.clone(), } }