From df1199fddb0c41441b7cd5a1f48bc48514a617dd 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 ## Why 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. ## What changed - 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`. ## Validation - `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 | 94 +----- 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(+), 651 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 994cdd512..c330c7eef 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 1fc35d151..af99b4b2e 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 71557bb73..246b2bb0c 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 using environment-native absolute paths. 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 using environment-native absolute paths. 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` for direct spawned children or `ancestorThreadId` for spawned descendants at any depth; the two filters are mutually exclusive. 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/items/list` — experimental; page through persisted thread items without resuming the thread. Pass `turnId` to restrict results to one turn, or omit it to page items across the thread. The active thread store must support item pagination. - `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 21adf4613..03031c87f 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 4703518f7..7242e816b 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 @@ -2856,7 +2853,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, }; @@ -3576,7 +3573,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 3f7cbc9d0..0d447011b 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 3b668e0a1..2bacaf644 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 e0d79a0c3..bb074e457 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; @@ -118,7 +117,6 @@ struct ThreadSettingsBuildParams { effort: Option, summary: Option, collaboration_mode: Option, - multi_agent_mode: Option, personality: Option, } @@ -515,7 +513,6 @@ impl TurnRequestProcessor { effort: params.effort, summary: params.summary, collaboration_mode: params.collaboration_mode, - multi_agent_mode: params.multi_agent_mode, personality: params.personality, }, ) @@ -622,7 +619,6 @@ impl TurnRequestProcessor { effort, summary, collaboration_mode, - multi_agent_mode, personality, } = params; @@ -656,7 +652,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 = @@ -733,7 +728,6 @@ impl TurnRequestProcessor { summary, service_tier: service_tier.clone(), collaboration_mode: collaboration_mode.clone(), - multi_agent_mode, personality, }) .await @@ -757,7 +751,6 @@ impl TurnRequestProcessor { summary, service_tier, collaboration_mode, - multi_agent_mode, personality, }) } @@ -788,7 +781,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 e4e4e42ad..fec786373 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 9ad25ca99..29dbee26d 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 4ff6417ce..a52829858 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -1754,7 +1754,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; @@ -1817,18 +1817,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; @@ -1867,7 +1868,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 { @@ -1895,71 +1896,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_with_auto_env(codex_home.path()).await?; - timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; - let thread_req = mcp - .send_thread_start_request_with_auto_env(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 9b010f7cd..49bbab0f8 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; @@ -68,7 +67,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 f91a77364..cf043971e 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 b427f14c8..6919d8c62 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> { @@ -241,7 +239,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 { @@ -288,7 +285,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(), @@ -394,7 +390,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( @@ -519,7 +514,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 460584371..e00a8624f 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -18,7 +18,6 @@ use codex_login::AuthManager; 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; @@ -889,42 +888,6 @@ async fn ephemeral_spawn_does_not_persist_agent_graph_edge() { ); } -#[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; @@ -947,15 +910,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; @@ -1050,7 +1004,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() }, ) @@ -1063,10 +1016,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 = @@ -1359,15 +1308,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()) @@ -1452,7 +1392,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() }, ) @@ -1465,10 +1404,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!( @@ -2283,7 +2218,6 @@ async fn spawn_thread_subagents_persist_parent_originator_across_new_and_truncat thread_source: None, dynamic_tools: Vec::new(), metrics_service_name: Some("codex_work_desktop".to_string()), - multi_agent_mode: None, parent_trace: None, environments: Vec::new(), thread_extension_init: ExtensionDataInit::default(), @@ -2393,7 +2327,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, config) = test_config().await; let thread_store = Arc::new(InMemoryThreadStore::default()); let manager = ThreadManager::new( @@ -2418,7 +2352,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"); @@ -2443,18 +2377,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 @@ -2465,16 +2387,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) @@ -2567,7 +2480,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 649f52e54..a4b2c491f 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -163,6 +163,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, + } +} + fn session_telemetry_for_request( session_telemetry: &SessionTelemetry, request: &ResponsesApiRequest, @@ -665,11 +672,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 @@ -768,7 +777,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 78dc324c4..26b3fa158 100644 --- a/codex-rs/core/src/client_tests.rs +++ b/codex-rs/core/src/client_tests.rs @@ -31,6 +31,7 @@ use codex_protocol::auth::AuthMode; 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; @@ -154,6 +155,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 640e1f8df..6fbd195fc 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -123,7 +123,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 dccfd4a76..0cd1ca25b 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, @@ -154,7 +152,6 @@ pub struct CodexThreadSettingsOverrides { pub summary: Option, pub service_tier: Option>, pub collaboration_mode: Option, - pub multi_agent_mode: Option, pub personality: Option, } @@ -375,7 +372,6 @@ impl CodexThread { summary, service_tier, collaboration_mode, - multi_agent_mode, personality, } = overrides; let collaboration_mode = if let Some(collaboration_mode) = collaboration_mode { @@ -399,7 +395,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 937c4bf05..5e81bc2a7 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 39e91d6cf..5c68893e7 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 46906583c..6e0d70bc1 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -453,7 +453,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(), @@ -3402,11 +3398,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 82fcfd128..385f2281b 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 c55afbf14..f2482a3ce 100644 --- a/codex-rs/core/src/session/review.rs +++ b/codex-rs/core/src/session/review.rs @@ -128,7 +128,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 af64e0bef..216ebd1f9 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, @@ -195,7 +194,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, @@ -233,9 +231,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); } @@ -430,7 +425,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 05f733248..c6772373d 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -3651,7 +3651,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, @@ -3759,7 +3758,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, @@ -4291,7 +4289,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, @@ -5163,7 +5160,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, @@ -5293,7 +5289,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, @@ -5541,7 +5536,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, @@ -5649,7 +5643,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, @@ -7374,7 +7367,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 17e349e66..38a7ec039 100644 --- a/codex-rs/core/src/session/tests/guardian_tests.rs +++ b/codex-rs/core/src/session/tests/guardian_tests.rs @@ -750,7 +750,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 da526d491..9799f68cc 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -126,7 +126,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, @@ -276,7 +275,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(), @@ -375,11 +373,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, @@ -563,7 +557,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 4846bfd89..98e13c5d6 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -40,7 +40,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; @@ -188,7 +187,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, @@ -639,7 +637,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(), @@ -679,7 +676,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, @@ -764,7 +760,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, @@ -776,7 +771,6 @@ impl ThreadManager { thread_source, Vec::new(), /*metrics_service_name*/ None, - initial_multi_agent_mode, /*inherited_environments*/ None, /*inherited_exec_policy*/ None, parent_trace, @@ -809,7 +803,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(), @@ -836,7 +829,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, @@ -848,7 +840,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, @@ -1004,13 +995,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( @@ -1039,7 +1023,6 @@ impl ThreadManager { thread_source, Vec::new(), /*metrics_service_name*/ None, - initial_multi_agent_mode, parent_trace, environments, /*thread_extension_init*/ ExtensionDataInit::default(), @@ -1322,7 +1305,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, @@ -1340,7 +1322,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>, @@ -1359,7 +1340,6 @@ impl ThreadManagerState { thread_source, Vec::new(), metrics_service_name, - initial_multi_agent_mode, inherited_environments, inherited_exec_policy, /*parent_trace*/ None, @@ -1387,7 +1367,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, @@ -1399,7 +1378,6 @@ impl ThreadManagerState { thread_source, Vec::new(), /*metrics_service_name*/ None, - initial_multi_agent_mode, inherited_environments, inherited_exec_policy, /*parent_trace*/ None, @@ -1421,7 +1399,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>, @@ -1440,7 +1417,6 @@ impl ThreadManagerState { thread_source, Vec::new(), /*metrics_service_name*/ None, - initial_multi_agent_mode, inherited_environments, inherited_exec_policy, /*parent_trace*/ None, @@ -1465,7 +1441,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, @@ -1483,7 +1458,6 @@ impl ThreadManagerState { thread_source, dynamic_tools, metrics_service_name, - initial_multi_agent_mode, /*inherited_environments*/ None, /*inherited_exec_policy*/ None, parent_trace, @@ -1508,7 +1482,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, @@ -1598,7 +1571,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 b1f5c4e24..a886dbd97 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -404,7 +404,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(), @@ -545,7 +544,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"), @@ -561,7 +559,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"), @@ -654,7 +651,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(), @@ -938,7 +934,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 fcad4bb62..204385934 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 ccfb465b7..8acb0a8c8 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -26,7 +26,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; @@ -1152,7 +1151,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); @@ -1191,7 +1189,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 837257dec..2186a4a6d 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 6a09c0f65..eb4fa3b39 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -663,7 +663,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 2e222c7a3..fcc675b86 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(), @@ -648,7 +647,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 21ef31c49..451b9cd38 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 53f9a97bc..aad8fc494 100644 --- a/codex-rs/core/tests/suite/subagent_notifications.rs +++ b/codex-rs/core/tests/suite/subagent_notifications.rs @@ -766,7 +766,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 bd86d8313..3d703e317 100644 --- a/codex-rs/memories/write/src/runtime.rs +++ b/codex-rs/memories/write/src/runtime.rs @@ -334,7 +334,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 5b95fee29..f697a8107 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 2d413b78e..1affdf0d2 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -489,9 +489,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, } @@ -2023,8 +2020,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 90fd6c2cc..3042ff6c7 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(), } }