From 6a0aad7247780a27e39ed040fb4ab728d41e1ff7 Mon Sep 17 00:00:00 2001 From: shijie-openai Date: Wed, 17 Jun 2026 14:22:36 -0700 Subject: [PATCH] Expose thread-level multi-agent mode --- .../analytics/src/analytics_client_tests.rs | 2 + codex-rs/analytics/src/client_tests.rs | 3 + .../schema/json/ServerNotification.json | 8 ++ .../schema/json/v2/ThreadForkResponse.json | 8 ++ .../schema/json/v2/ThreadResumeResponse.json | 8 ++ .../v2/ThreadSettingsUpdatedNotification.json | 8 ++ .../schema/json/v2/ThreadStartParams.json | 8 ++ .../schema/json/v2/ThreadStartResponse.json | 8 ++ .../schema/typescript/v2/ThreadSettings.ts | 2 +- .../src/protocol/common.rs | 5 +- .../src/protocol/v2/tests.rs | 30 ++++ .../src/protocol/v2/thread.rs | 25 +++- codex-rs/app-server/README.md | 8 +- .../request_processors/thread_lifecycle.rs | 2 + .../request_processors/thread_processor.rs | 8 ++ .../src/request_processors/thread_summary.rs | 4 +- codex-rs/app-server/src/thread_state.rs | 1 + .../app-server/tests/suite/v2/skills_list.rs | 1 + .../app-server/tests/suite/v2/turn_start.rs | 135 ++++++++++++++++++ codex-rs/core/src/codex_delegate.rs | 1 + codex-rs/core/src/session/handlers.rs | 1 + codex-rs/core/src/session/mod.rs | 5 +- .../core/src/session/tests/guardian_tests.rs | 1 + codex-rs/core/src/thread_manager.rs | 59 ++++++-- codex-rs/core/src/thread_manager_tests.rs | 5 + codex-rs/core/tests/suite/agents_md.rs | 2 + .../tests/suite/subagent_notifications.rs | 1 + codex-rs/exec/src/lib_tests.rs | 1 + codex-rs/memories/write/src/runtime.rs | 1 + codex-rs/protocol/src/protocol.rs | 2 + .../tui/src/app/app_server_event_targets.rs | 1 + codex-rs/tui/src/app/tests.rs | 1 + codex-rs/tui/src/app_server_session.rs | 1 + .../tui/src/chatwidget/tests/app_server.rs | 1 + 34 files changed, 338 insertions(+), 19 deletions(-) diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 7c634ce370ad..f01c120f479b 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -216,6 +216,7 @@ fn sample_thread_start_response( sandbox: AppServerSandboxPolicy::DangerFullAccess, active_permission_profile: None, reasoning_effort: None, + multi_agent_mode: Default::default(), }) } @@ -280,6 +281,7 @@ fn sample_thread_resume_response_with_source( sandbox: AppServerSandboxPolicy::DangerFullAccess, active_permission_profile: None, reasoning_effort: None, + multi_agent_mode: Default::default(), initial_turns_page: None, }) } diff --git a/codex-rs/analytics/src/client_tests.rs b/codex-rs/analytics/src/client_tests.rs index 1835932e8ef2..58490162ed04 100644 --- a/codex-rs/analytics/src/client_tests.rs +++ b/codex-rs/analytics/src/client_tests.rs @@ -313,6 +313,7 @@ fn sample_thread_start_response() -> ClientResponsePayload { sandbox: AppServerSandboxPolicy::DangerFullAccess, active_permission_profile: None, reasoning_effort: None, + multi_agent_mode: Default::default(), }) } @@ -330,6 +331,7 @@ fn sample_thread_resume_response() -> ClientResponsePayload { sandbox: AppServerSandboxPolicy::DangerFullAccess, active_permission_profile: None, reasoning_effort: None, + multi_agent_mode: Default::default(), initial_turns_page: None, }) } @@ -348,6 +350,7 @@ fn sample_thread_fork_response() -> ClientResponsePayload { sandbox: AppServerSandboxPolicy::DangerFullAccess, active_permission_profile: None, reasoning_effort: None, + multi_agent_mode: Default::default(), }) } diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index d64befa0a08b..e5eb1c66de69 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -2588,6 +2588,14 @@ ], "type": "object" }, + "MultiAgentMode": { + "description": "Controls whether the model should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help.", + "enum": [ + "explicitRequestOnly", + "proactive" + ], + "type": "string" + }, "NetworkAccess": { "enum": [ "restricted", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 4a666672b66e..95a26d977abe 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -636,6 +636,14 @@ } ] }, + "MultiAgentMode": { + "description": "Controls whether the model should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help.", + "enum": [ + "explicitRequestOnly", + "proactive" + ], + "type": "string" + }, "NetworkAccess": { "enum": [ "restricted", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 032d1dd74ef2..9fd001afa4f8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -636,6 +636,14 @@ } ] }, + "MultiAgentMode": { + "description": "Controls whether the model should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help.", + "enum": [ + "explicitRequestOnly", + "proactive" + ], + "type": "string" + }, "NetworkAccess": { "enum": [ "restricted", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadSettingsUpdatedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadSettingsUpdatedNotification.json index fbcaee3ee8b9..67a0f5c5d955 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadSettingsUpdatedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadSettingsUpdatedNotification.json @@ -108,6 +108,14 @@ ], "type": "string" }, + "MultiAgentMode": { + "description": "Controls whether the model should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help.", + "enum": [ + "explicitRequestOnly", + "proactive" + ], + "type": "string" + }, "NetworkAccess": { "enum": [ "restricted", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json index d4291f6dc1a4..eebb5da9602c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json @@ -194,6 +194,14 @@ "LegacyAppPathString": { "type": "string" }, + "MultiAgentMode": { + "description": "Controls whether the model should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help.", + "enum": [ + "explicitRequestOnly", + "proactive" + ], + "type": "string" + }, "Personality": { "enum": [ "none", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index 33373b9f7208..c02e6a8ac194 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -636,6 +636,14 @@ } ] }, + "MultiAgentMode": { + "description": "Controls whether the model should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help.", + "enum": [ + "explicitRequestOnly", + "proactive" + ], + "type": "string" + }, "NetworkAccess": { "enum": [ "restricted", diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSettings.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSettings.ts index bcfd0ad86ce3..b034ea80bb35 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSettings.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSettings.ts @@ -11,4 +11,4 @@ import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; import type { SandboxPolicy } from "./SandboxPolicy"; -export type ThreadSettings = { cwd: AbsolutePathBuf, approvalPolicy: AskForApproval, approvalsReviewer: ApprovalsReviewer, sandboxPolicy: SandboxPolicy, activePermissionProfile: ActivePermissionProfile | null, model: string, modelProvider: string, serviceTier: string | null, effort: ReasoningEffort | null, summary: ReasoningSummary | null, collaborationMode: CollaborationMode, personality: Personality | null, }; +export type ThreadSettings = {cwd: AbsolutePathBuf, approvalPolicy: AskForApproval, approvalsReviewer: ApprovalsReviewer, sandboxPolicy: SandboxPolicy, activePermissionProfile: ActivePermissionProfile | null, model: string, modelProvider: string, serviceTier: string | null, effort: ReasoningEffort | null, summary: ReasoningSummary | null, collaborationMode: CollaborationMode, personality: Personality | null}; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 7611c27ff1fc..dd88df4d4923 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -2524,6 +2524,7 @@ mod tests { sandbox: v2::SandboxPolicy::DangerFullAccess, active_permission_profile: None, reasoning_effort: None, + multi_agent_mode: None, }, }; @@ -2570,7 +2571,8 @@ mod tests { "type": "dangerFullAccess" }, "activePermissionProfile": null, - "reasoningEffort": null + "reasoningEffort": null, + "multiAgentMode": null } }), serde_json::to_value(&response)?, @@ -3475,6 +3477,7 @@ mod tests { developer_instructions: None, }, }, + multi_agent_mode: Default::default(), personality: None, }, }); diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 33d1134cb9ee..0982da1703f5 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -197,6 +197,7 @@ fn thread_resume_response_round_trips_initial_turns_page() { sandbox: SandboxPolicy::DangerFullAccess, active_permission_profile: None, reasoning_effort: None, + multi_agent_mode: Default::default(), initial_turns_page: Some(TurnsPage { data: Vec::new(), next_cursor: Some("cursor_next".to_string()), @@ -3606,6 +3607,14 @@ fn thread_lifecycle_responses_default_missing_optional_fields() { assert_eq!(resume.active_permission_profile, None); assert_eq!(resume.initial_turns_page, None); assert_eq!(fork.active_permission_profile, None); + assert_eq!( + ( + start.multi_agent_mode, + resume.multi_agent_mode, + fork.multi_agent_mode, + ), + (None, None, None,) + ); } #[test] @@ -3674,6 +3683,27 @@ fn turn_start_params_round_trip_multi_agent_mode() { ); } +#[test] +fn thread_start_params_round_trip_multi_agent_mode() { + let params: ThreadStartParams = serde_json::from_value(json!({ + "multiAgentMode": "proactive" + })) + .expect("params should deserialize"); + + assert_eq!( + params.multi_agent_mode, + Some(codex_protocol::config_types::MultiAgentMode::Proactive) + ); + assert_eq!( + crate::experimental_api::ExperimentalApi::experimental_reason(¶ms), + Some("thread/start.multiAgentMode") + ); + assert_eq!( + serde_json::to_value(params).expect("params should serialize")["multiAgentMode"], + "proactive" + ); +} + #[test] fn thread_settings_update_params_preserve_explicit_null_service_tier() { let params: ThreadSettingsUpdateParams = serde_json::from_value(json!({ 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 0252c4b048ae..1ec2263e2bc9 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs @@ -14,6 +14,7 @@ use codex_experimental_api_macros::ExperimentalApi; pub use codex_protocol::capabilities::CapabilityRootLocation; pub use codex_protocol::capabilities::SelectedCapabilityRoot; use codex_protocol::config_types::CollaborationMode; +use codex_protocol::config_types::MultiAgentMode; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; pub use codex_protocol::dynamic_tools::DynamicToolFunctionSpec; @@ -91,6 +92,12 @@ pub struct ThreadStartParams { pub developer_instructions: Option, #[ts(optional = nullable)] pub personality: Option, + /// Set the initial multi-agent mode for this thread. + /// Omitted leaves the thread without a selected mode. Eligible multi-agent + /// v2 turns still default to `explicitRequestOnly`. + #[experimental("thread/start.multiAgentMode")] + #[ts(optional = nullable)] + pub multi_agent_mode: Option, #[ts(optional = nullable)] pub ephemeral: Option, #[ts(optional = nullable)] @@ -177,6 +184,10 @@ pub struct ThreadStartResponse { #[serde(default)] pub active_permission_profile: Option, pub reasoning_effort: Option, + /// Current selected multi-agent mode for this thread, if one was selected. + #[experimental("thread/start.multiAgentMode")] + #[serde(default)] + pub multi_agent_mode: Option, } #[derive( @@ -240,7 +251,7 @@ pub struct ThreadSettingsUpdateParams { #[ts(export_to = "v2/")] pub struct ThreadSettingsUpdateResponse {} -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct ThreadSettings { @@ -255,6 +266,10 @@ pub struct ThreadSettings { pub effort: Option, pub summary: Option, pub collaboration_mode: CollaborationMode, + /// Current selected multi-agent mode for this thread, if one was selected. + #[experimental("thread/settings.multiAgentMode")] + #[serde(default)] + pub multi_agent_mode: Option, pub personality: Option, } @@ -391,6 +406,10 @@ pub struct ThreadResumeResponse { #[serde(default)] pub active_permission_profile: Option, pub reasoning_effort: Option, + /// Current selected multi-agent mode for this thread, if one was selected. + #[experimental("thread/resume.multiAgentMode")] + #[serde(default)] + pub multi_agent_mode: Option, /// `thread/turns/list` page returned when requested by `initialTurnsPage`. #[experimental("thread/resume.initialTurnsPage")] #[serde(default)] @@ -539,6 +558,10 @@ pub struct ThreadForkResponse { #[serde(default)] pub active_permission_profile: Option, pub reasoning_effort: Option, + /// Current selected multi-agent mode for this thread, if one was selected. + #[experimental("thread/fork.multiAgentMode")] + #[serde(default)] + pub multi_agent_mode: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index aaae6d4387ca..5a50925e52bf 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -130,10 +130,10 @@ 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 `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/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Experimental `multiAgentMode` selects the initial thread mode; omission leaves the selected mode unset, while eligible multi-agent v2 turns still default to `explicitRequestOnly`. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`. Experimental `selectedCapabilityRoots` selects environment-owned plugin or standalone-skill roots. Skills found below those roots are listed and read through the owning environment. Stdio MCP servers declared by selected plugins are also started in that environment; HTTP MCP declarations remain inactive. +- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`. Multi-agent mode restores the last effective mode from rollout history when available; clients can select another mode on the first `turn/start`. - `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`. -- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. 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. +- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known. Their experimental `multiAgentMode` field, and the corresponding thread setting, report the thread's current selected mode or `null` when no mode was selected. Turn construction separately determines whether that mode is applicable to the selected model and runtime configuration. - `thread/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. @@ -161,7 +161,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 `explicitRequestOnly` or `proactive`; omission keeps the loaded session's current mode. The requested mode is retained for the loaded session without rejecting unsupported configurations. Eligible multi-agent v2 turns use the requested mode when `features.multi_agent_mode` is enabled and otherwise use explicit-request-only developer instructions. +- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. Prefer experimental `permissions` profile selection by id for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". Experimental `multiAgentMode` accepts `explicitRequestOnly` or `proactive`; omission keeps the loaded session's current selected mode, including an unset mode. The requested mode is retained for the loaded session without rejecting unsupported configurations. Eligible multi-agent v2 turns default an unset mode to explicit-request-only, use the selected mode when `features.multi_agent_mode` is enabled, and otherwise use explicit-request-only developer instructions. - `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 fd9e93e1894c..21adf4613476 100644 --- a/codex-rs/app-server/src/request_processors/thread_lifecycle.rs +++ b/codex-rs/app-server/src/request_processors/thread_lifecycle.rs @@ -639,6 +639,7 @@ pub(super) async fn handle_pending_thread_resume_request( active_permission_profile, workspace_roots, reasoning_effort, + multi_agent_mode, .. } = config_snapshot; let instruction_sources = pending.instruction_sources; @@ -661,6 +662,7 @@ pub(super) async fn handle_pending_thread_resume_request( sandbox, active_permission_profile, reasoning_effort, + multi_agent_mode, initial_turns_page, }; outgoing.send_response(request_id, response).await; 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 a5299f2c72d9..82df9567a698 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -2,6 +2,7 @@ use super::*; use crate::error_code::method_not_found; use codex_app_server_protocol::SelectedCapabilityRoot; use codex_extension_api::ExtensionDataInit; +use codex_protocol::config_types::MultiAgentMode; use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS; use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; @@ -895,6 +896,7 @@ impl ThreadRequestProcessor { mock_experimental_field: _mock_experimental_field, experimental_raw_events, personality, + multi_agent_mode, ephemeral, session_start_source, thread_source, @@ -947,6 +949,7 @@ impl ThreadRequestProcessor { app_server_client_version, config, typesafe_overrides, + multi_agent_mode, dynamic_tools, selected_capability_roots.unwrap_or_default(), session_start_source, @@ -1020,6 +1023,7 @@ impl ThreadRequestProcessor { app_server_client_version: Option, config_overrides: Option>, typesafe_overrides: ConfigOverrides, + multi_agent_mode: Option, dynamic_tools: Option>, selected_capability_roots: Vec, session_start_source: Option, @@ -1143,6 +1147,7 @@ impl ThreadRequestProcessor { thread_source, dynamic_tools, metrics_service_name: service_name, + multi_agent_mode, parent_trace: request_trace, environments, thread_extension_init, @@ -1247,6 +1252,7 @@ impl ThreadRequestProcessor { sandbox, active_permission_profile, reasoning_effort: config_snapshot.reasoning_effort, + multi_agent_mode: config_snapshot.multi_agent_mode, }; let notif = thread_started_notification(thread); listener_task_context @@ -2767,6 +2773,7 @@ impl ThreadRequestProcessor { sandbox, active_permission_profile, reasoning_effort: session_configured.reasoning_effort, + multi_agent_mode: config_snapshot.multi_agent_mode, initial_turns_page, }; @@ -3486,6 +3493,7 @@ impl ThreadRequestProcessor { sandbox, active_permission_profile, reasoning_effort: session_configured.reasoning_effort, + multi_agent_mode: config_snapshot.multi_agent_mode, }; let notif = thread_started_notification(thread); 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 0fb1320055c1..40dffd94a21e 100644 --- a/codex-rs/app-server/src/request_processors/thread_summary.rs +++ b/codex-rs/app-server/src/request_processors/thread_summary.rs @@ -1,5 +1,4 @@ use super::*; - #[cfg(test)] use chrono::DateTime; #[cfg(test)] @@ -206,6 +205,7 @@ pub(crate) fn thread_settings_from_config_snapshot( effort: config_snapshot.reasoning_effort.clone(), summary: config_snapshot.reasoning_summary, collaboration_mode: config_snapshot.collaboration_mode.clone(), + multi_agent_mode: config_snapshot.multi_agent_mode, personality: config_snapshot.personality, } } @@ -226,6 +226,7 @@ pub(crate) fn thread_settings_from_core_snapshot( reasoning_summary, personality, collaboration_mode, + multi_agent_mode, } = snapshot; let sandbox_policy = thread_response_sandbox_policy(&permission_profile, cwd.as_path()); ThreadSettings { @@ -242,6 +243,7 @@ pub(crate) fn thread_settings_from_core_snapshot( effort: reasoning_effort, summary: reasoning_summary, collaboration_mode, + multi_agent_mode, personality, } } diff --git a/codex-rs/app-server/src/thread_state.rs b/codex-rs/app-server/src/thread_state.rs index 6d2b48a4c88b..19a0845ce77d 100644 --- a/codex-rs/app-server/src/thread_state.rs +++ b/codex-rs/app-server/src/thread_state.rs @@ -240,6 +240,7 @@ mod tests { developer_instructions: None, }, }, + multi_agent_mode: Default::default(), personality: None, } } diff --git a/codex-rs/app-server/tests/suite/v2/skills_list.rs b/codex-rs/app-server/tests/suite/v2/skills_list.rs index 491c0608507a..ef7ee6c1d127 100644 --- a/codex-rs/app-server/tests/suite/v2/skills_list.rs +++ b/codex-rs/app-server/tests/suite/v2/skills_list.rs @@ -789,6 +789,7 @@ async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<( base_instructions: None, developer_instructions: None, personality: None, + multi_agent_mode: None, ephemeral: None, session_start_source: None, thread_source: None, 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 d657db7ea40a..0fee18f750ef 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -76,6 +76,7 @@ use codex_protocol::config_types::Settings; use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS; use codex_protocol::models::ImageDetail; use codex_protocol::openai_models::ReasoningEffort; +use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG; use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS; use codex_utils_absolute_path::test_support::PathExt; use core_test_support::responses; @@ -1827,6 +1828,140 @@ async fn turn_start_accepts_multi_agent_mode_v2() -> Result<()> { Ok(()) } +#[tokio::test] +async fn thread_start_multi_agent_mode_initializes_first_turn() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let body = responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_assistant_message("msg-1", "Done"), + responses::ev_completed("resp-1"), + ]); + let response_mock = responses::mount_sse_once(&server, body).await; + + let codex_home = TempDir::new()?; + create_config_toml( + codex_home.path(), + &server.uri(), + "never", + &BTreeMap::from([ + (Feature::MultiAgentV2, true), + (Feature::MultiAgentMode, true), + ]), + )?; + + 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: Some(MultiAgentMode::Proactive), + ..Default::default() + }) + .await?; + let thread_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), + ) + .await??; + let ThreadStartResponse { + thread, + multi_agent_mode, + .. + } = to_response::(thread_resp)?; + assert_eq!(multi_agent_mode, Some(MultiAgentMode::Proactive)); + + let turn_req = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread.id, + input: vec![V2UserInput::Text { + text: "Hello".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + let turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), + ) + .await??; + let _: TurnStartResponse = to_response(turn_resp)?; + + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + 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("Proactive multi-agent delegation is active.") + }), + "expected proactive multi-agent mode instructions in developer input, got {developer_texts:?}" + ); + + Ok(()) +} + +#[tokio::test] +async fn thread_start_reports_selected_multi_agent_mode() -> Result<()> { + skip_if_no_network!(Ok(())); + + let cases = [ + ( + BTreeMap::from([(Feature::MultiAgentV2, true)]), + Some(MultiAgentMode::Proactive), + Some(MultiAgentMode::Proactive), + ), + ( + BTreeMap::new(), + Some(MultiAgentMode::Proactive), + Some(MultiAgentMode::Proactive), + ), + ( + BTreeMap::from([ + (Feature::MultiAgentV2, true), + (Feature::MultiAgentMode, true), + ]), + None, + None, + ), + ]; + + 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/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index d31dc138b57f..bf11050f6957 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -117,6 +117,7 @@ pub(crate) async fn run_codex_thread_interactive( thread_store: Arc::clone(&parent_session.services.thread_store), attestation_provider: parent_session.services.attestation_provider.clone(), inherited_multi_agent_version: Some(MultiAgentVersion::Disabled), + initial_multi_agent_mode: None, })) .or_cancel(&cancel_token) .await??; diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index 6ed2e3119c1d..f88ff434ea00 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -178,6 +178,7 @@ 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 bd98cde02b11..251c67cedc42 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -440,6 +440,7 @@ pub(crate) struct CodexSpawnArgs { pub(crate) thread_store: Arc, pub(crate) attestation_provider: Option>, pub(crate) inherited_multi_agent_version: Option, + pub(crate) initial_multi_agent_mode: Option, } pub(crate) fn resolve_multi_agent_version( @@ -522,6 +523,7 @@ impl Codex { thread_store, attestation_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(); @@ -576,7 +578,8 @@ impl Codex { .await; let multi_agent_version = resolve_multi_agent_version(&conversation_history, inherited_multi_agent_version); - let multi_agent_mode = conversation_history.get_multi_agent_mode(); + let multi_agent_mode = + initial_multi_agent_mode.or_else(|| conversation_history.get_multi_agent_mode()); config .validate_multi_agent_v2_config() .map_err(|err| CodexErr::InvalidRequest(err.to_string()))?; diff --git a/codex-rs/core/src/session/tests/guardian_tests.rs b/codex-rs/core/src/session/tests/guardian_tests.rs index e958b39e1f7d..6836ae95598e 100644 --- a/codex-rs/core/src/session/tests/guardian_tests.rs +++ b/codex-rs/core/src/session/tests/guardian_tests.rs @@ -736,6 +736,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() { thread_store, attestation_provider: None, inherited_multi_agent_version: None, + initial_multi_agent_mode: None, }) .await .expect("spawn guardian subagent"); diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index 2adb713a0e51..47d358204dd7 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -35,6 +35,7 @@ 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; @@ -183,6 +184,7 @@ 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, @@ -601,6 +603,7 @@ impl ThreadManager { thread_source: None, dynamic_tools, metrics_service_name: None, + multi_agent_mode: None, parent_trace: None, environments, thread_extension_init: ExtensionDataInit::default(), @@ -638,6 +641,7 @@ 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, @@ -728,6 +732,7 @@ impl ThreadManager { thread_source, Vec::new(), /*metrics_service_name*/ None, + /*initial_multi_agent_mode*/ None, /*inherited_environments*/ None, /*inherited_exec_policy*/ None, parent_trace, @@ -791,6 +796,7 @@ impl ThreadManager { thread_source, Vec::new(), /*metrics_service_name*/ None, + /*initial_multi_agent_mode*/ None, /*inherited_environments*/ None, /*inherited_exec_policy*/ None, /*parent_trace*/ None, @@ -1096,16 +1102,12 @@ impl ThreadManagerState { parent_thread_id: Option, forked_from_thread_id: Option, ) -> Option { - let inherited_thread_id = match session_source { - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { - parent_thread_id, .. - })) => Some(*parent_thread_id), - _ => match initial_history { - InitialHistory::Resumed(resumed) => Some(resumed.conversation_id), - InitialHistory::Forked(_) => forked_from_thread_id.or(parent_thread_id), - InitialHistory::New | InitialHistory::Cleared => parent_thread_id, - }, - }; + let inherited_thread_id = Self::inherited_thread_id_for_spawn( + initial_history, + session_source, + parent_thread_id, + forked_from_thread_id, + ); let inherited_multi_agent_version = match inherited_thread_id { Some(thread_id) => self .get_thread(thread_id) @@ -1117,6 +1119,24 @@ impl ThreadManagerState { resolve_multi_agent_version(initial_history, inherited_multi_agent_version) } + fn inherited_thread_id_for_spawn( + initial_history: &InitialHistory, + session_source: Option<&SessionSource>, + parent_thread_id: Option, + forked_from_thread_id: Option, + ) -> Option { + match session_source { + Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, .. + })) => Some(*parent_thread_id), + _ => match initial_history { + InitialHistory::Resumed(resumed) => Some(resumed.conversation_id), + InitialHistory::Forked(_) => forked_from_thread_id.or(parent_thread_id), + InitialHistory::New | InitialHistory::Cleared => parent_thread_id, + }, + } + } + /// Resolves the provider snapshot for a newly spawned runtime. /// /// Loads a fresh provider snapshot for: @@ -1216,6 +1236,7 @@ impl ThreadManagerState { thread_source, Vec::new(), metrics_service_name, + /*initial_multi_agent_mode*/ None, inherited_environments, inherited_exec_policy, /*parent_trace*/ None, @@ -1253,6 +1274,7 @@ impl ThreadManagerState { thread_source, Vec::new(), /*metrics_service_name*/ None, + /*initial_multi_agent_mode*/ None, inherited_environments, inherited_exec_policy, /*parent_trace*/ None, @@ -1291,6 +1313,7 @@ impl ThreadManagerState { thread_source, Vec::new(), /*metrics_service_name*/ None, + /*initial_multi_agent_mode*/ None, inherited_environments, inherited_exec_policy, /*parent_trace*/ None, @@ -1330,6 +1353,7 @@ impl ThreadManagerState { thread_source, dynamic_tools, metrics_service_name, + /*initial_multi_agent_mode*/ None, /*inherited_environments*/ None, /*inherited_exec_policy*/ None, parent_trace, @@ -1353,6 +1377,7 @@ 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, @@ -1397,6 +1422,19 @@ impl ThreadManagerState { forked_from_thread_id, ) .await; + let inherited_multi_agent_mode = match Self::inherited_thread_id_for_spawn( + &initial_history, + Some(&session_source), + parent_thread_id, + forked_from_thread_id, + ) { + Some(thread_id) => match self.get_thread(thread_id).await { + Ok(thread) => thread.config_snapshot().await.multi_agent_mode, + Err(_) => None, + }, + None => None, + }; + let initial_multi_agent_mode = initial_multi_agent_mode.or(inherited_multi_agent_mode); let CodexSpawnOk { codex, thread_id, .. } = Box::pin(Codex::spawn(CodexSpawnArgs { @@ -1429,6 +1467,7 @@ impl ThreadManagerState { thread_store: Arc::clone(&self.thread_store), attestation_provider: self.attestation_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 cec54b5e4446..b1948cf11264 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -322,6 +322,7 @@ 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(), @@ -460,6 +461,7 @@ 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"), @@ -474,6 +476,7 @@ 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"), @@ -564,6 +567,7 @@ 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(), @@ -839,6 +843,7 @@ 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/tests/suite/agents_md.rs b/codex-rs/core/tests/suite/agents_md.rs index a4ab404065dc..4a29dd2e6f70 100644 --- a/codex-rs/core/tests/suite/agents_md.rs +++ b/codex-rs/core/tests/suite/agents_md.rs @@ -445,6 +445,7 @@ 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(), @@ -652,6 +653,7 @@ 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/subagent_notifications.rs b/codex-rs/core/tests/suite/subagent_notifications.rs index 764380be7190..b33fd0b7c038 100644 --- a/codex-rs/core/tests/suite/subagent_notifications.rs +++ b/codex-rs/core/tests/suite/subagent_notifications.rs @@ -751,6 +751,7 @@ 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/exec/src/lib_tests.rs b/codex-rs/exec/src/lib_tests.rs index 6b27d01116d5..7c7881b7416d 100644 --- a/codex-rs/exec/src/lib_tests.rs +++ b/codex-rs/exec/src/lib_tests.rs @@ -788,5 +788,6 @@ fn sample_thread_start_response() -> ThreadStartResponse { }, active_permission_profile: None, reasoning_effort: None, + multi_agent_mode: Default::default(), } } diff --git a/codex-rs/memories/write/src/runtime.rs b/codex-rs/memories/write/src/runtime.rs index 8dae6ca61704..40a9b429f26f 100644 --- a/codex-rs/memories/write/src/runtime.rs +++ b/codex-rs/memories/write/src/runtime.rs @@ -310,6 +310,7 @@ 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/protocol.rs b/codex-rs/protocol/src/protocol.rs index 168d018e0952..047a765faa6e 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1995,6 +1995,8 @@ pub struct ThreadSettingsSnapshot { #[serde(skip_serializing_if = "Option::is_none")] pub personality: Option, pub collaboration_mode: CollaborationMode, + #[serde(default)] + pub multi_agent_mode: Option, } #[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, TS)] diff --git a/codex-rs/tui/src/app/app_server_event_targets.rs b/codex-rs/tui/src/app/app_server_event_targets.rs index 90f688f7ed1b..52cf61067cb5 100644 --- a/codex-rs/tui/src/app/app_server_event_targets.rs +++ b/codex-rs/tui/src/app/app_server_event_targets.rs @@ -227,6 +227,7 @@ mod tests { developer_instructions: None, }, }, + multi_agent_mode: Default::default(), personality: None, } } diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 87538251aa3a..1b8d4f2133c2 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -6033,6 +6033,7 @@ async fn inactive_thread_settings_notification_updates_cached_collaboration_mode effort: collaboration_mode.settings.reasoning_effort.clone(), summary: None, collaboration_mode: collaboration_mode.clone(), + multi_agent_mode: Default::default(), personality: Some(Personality::Pragmatic), }, }; diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index ecbf92c33df4..a09211139a0b 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -2373,6 +2373,7 @@ mod tests { .into(), active_permission_profile: None, reasoning_effort: None, + multi_agent_mode: Default::default(), initial_turns_page: None, }; diff --git a/codex-rs/tui/src/chatwidget/tests/app_server.rs b/codex-rs/tui/src/chatwidget/tests/app_server.rs index f4fcc85e438d..448925e39441 100644 --- a/codex-rs/tui/src/chatwidget/tests/app_server.rs +++ b/codex-rs/tui/src/chatwidget/tests/app_server.rs @@ -30,6 +30,7 @@ fn thread_settings_for_test( developer_instructions: None, }, }, + multi_agent_mode: Default::default(), personality: Some(Personality::Pragmatic), }, }