app-server: treat null mode developer instructions as built-in defaults (#10983)

## Summary
- make `turn/start` normalize
`collaborationMode.settings.developer_instructions: null` to the
built-in instructions for the selected mode
- prevent app-server clients from accidentally clearing mode-switch
developer instructions by sending `null`
- document this behavior in the v2 protocol and app-server docs

## What changed
- `codex-rs/app-server/src/codex_message_processor.rs`
  - added a small `normalize_turn_start_collaboration_mode` helper
  - in `turn_start`, apply normalization before `OverrideTurnContext`
- `codex-rs/app-server/tests/suite/v2/turn_start.rs`
- extended `turn_start_accepts_collaboration_mode_override_v2` to assert
the outgoing request includes default-mode instruction text when the
client sends `developer_instructions: null`
- `codex-rs/app-server-protocol/src/protocol/v2.rs`
- clarified `TurnStartParams.collaboration_mode` docs:
`settings.developer_instructions: null` means use built-in mode
instructions
- regenerated schema fixture:
- `codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts`
- docs:
  - `codex-rs/app-server/README.md`
  - `codex-rs/docs/codex_mcp_interface.md`
This commit is contained in:
Charley Cunningham
2026-02-07 12:59:41 -08:00
committed by GitHub
Unverified
parent 739908a12c
commit e6662d6387
6 changed files with 39 additions and 3 deletions
@@ -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};
@@ -2089,6 +2089,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<CollaborationMode>,
+1 -1
View File
@@ -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 agents in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success.
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications.
- `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 Codexs 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.
@@ -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<CoreInputItem> = 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;
@@ -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(())
}
+2
View File
@@ -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: