diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts index ec390486410..d595035ddaf 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts @@ -37,5 +37,8 @@ personality?: Personality | null, /** outputSchema?: JsonValue | null, /** * EXPERIMENTAL - Set a pre-set collaboration mode. * Takes precedence over model, reasoning_effort, and developer instructions if set. + * + * For `collaboration_mode.settings.developer_instructions`, `null` means + * "use the built-in instructions for the selected mode". */ collaborationMode?: CollaborationMode | null}; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 630e0395786..738edd224ff 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -2069,6 +2069,9 @@ pub struct TurnStartParams { /// EXPERIMENTAL - Set a pre-set collaboration mode. /// Takes precedence over model, reasoning_effort, and developer instructions if set. + /// + /// For `collaboration_mode.settings.developer_instructions`, `null` means + /// "use the built-in instructions for the selected mode". #[experimental("turn/start.collaborationMode")] #[ts(optional = nullable)] pub collaboration_mode: Option, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index a6d49f2e4cd..6fc03fdd4b4 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -94,7 +94,7 @@ Example (from OpenAI's official VSCode extension): - `thread/unarchive` — move an archived rollout file back into the sessions directory; returns the restored `thread` on success. - `thread/compact/start` — trigger conversation history compaction for a thread; returns `{}` immediately while progress streams through standard turn/item notifications. - `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. +- `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. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". - `turn/steer` — add user input to an already in-flight turn without starting a new turn; returns the active `turnId` that accepted the input. - `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"`. - `review/start` — kick off Codex’s automated reviewer for a thread; responds like `turn/start` and emits `item/started`/`item/completed` notifications with `enteredReviewMode` and `exitedReviewMode` items, plus a final assistant `agentMessage` containing the review. diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index f96daebc194..29fbd3d74d0 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -206,6 +206,7 @@ use codex_login::ServerOptions as LoginServerOptions; use codex_login::ShutdownHandle; use codex_login::run_login_server; use codex_protocol::ThreadId; +use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::Personality; use codex_protocol::config_types::WindowsSandboxLevel; @@ -397,6 +398,27 @@ impl CodexMessageProcessor { .unwrap_or_default() } + /// If a client sends `developer_instructions: null` during a mode switch, + /// use the built-in instructions for that mode. + fn normalize_turn_start_collaboration_mode( + &self, + mut collaboration_mode: CollaborationMode, + ) -> CollaborationMode { + if collaboration_mode.settings.developer_instructions.is_none() + && let Some(instructions) = self + .thread_manager + .list_collaboration_modes() + .into_iter() + .find(|preset| preset.mode == Some(collaboration_mode.mode)) + .and_then(|preset| preset.developer_instructions.flatten()) + .filter(|instructions| !instructions.is_empty()) + { + collaboration_mode.settings.developer_instructions = Some(instructions); + } + + collaboration_mode + } + fn review_request_from_target( target: ApiReviewTarget, ) -> Result<(ReviewRequest, String), JSONRPCErrorError> { @@ -4554,6 +4576,10 @@ impl CodexMessageProcessor { } }; + let collaboration_mode = params + .collaboration_mode + .map(|mode| self.normalize_turn_start_collaboration_mode(mode)); + // Map v2 input items to core input items. let mapped_items: Vec = params .input @@ -4567,7 +4593,7 @@ impl CodexMessageProcessor { || params.model.is_some() || params.effort.is_some() || params.summary.is_some() - || params.collaboration_mode.is_some() + || collaboration_mode.is_some() || params.personality.is_some(); // If any overrides are provided, update the session turn context first. @@ -4581,7 +4607,7 @@ impl CodexMessageProcessor { model: params.model, effort: params.effort.map(Some), summary: params.summary, - collaboration_mode: params.collaboration_mode, + collaboration_mode, personality: params.personality, }) .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 395561f69a1..27fc335cb44 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -408,6 +408,8 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> { let request = response_mock.single_request(); let payload = request.body_json(); assert_eq!(payload["model"].as_str(), Some("mock-model-collab")); + let payload_text = payload.to_string(); + assert!(payload_text.contains("The `request_user_input` tool is unavailable in Default mode.")); Ok(()) } diff --git a/codex-rs/docs/codex_mcp_interface.md b/codex-rs/docs/codex_mcp_interface.md index 8b1b5f70457..da415d9deb2 100644 --- a/codex-rs/docs/codex_mcp_interface.md +++ b/codex-rs/docs/codex_mcp_interface.md @@ -112,6 +112,8 @@ Fetch the built-in collaboration mode presets with `collaborationMode/list`. Thi - `data` – ordered list of collaboration mode masks (partial settings to apply on top of the base mode) - For tri-state fields like `reasoning_effort` and `developer_instructions`, omit the field to keep the current value, set it to `null` to clear it, or set a concrete value to update it. +When sending `turn/start` with `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". + ## Event stream While a conversation runs, the server sends notifications: