Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8150,14 +8150,8 @@
"type": "object"
},
"CollaborationModeMask": {
"description": "A mask for collaboration mode settings, allowing partial updates. All fields except `name` are optional, enabling selective updates.",
"description": "EXPERIMENTAL - collaboration mode preset metadata for clients.",
"properties": {
"developer_instructions": {
"type": [
"string",
"null"
]
},
"mode": {
"anyOf": [
{
Expand Down

This file was deleted.

1 change: 0 additions & 1 deletion codex-rs/app-server-protocol/schema/typescript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ export type { CollabResumeEndEvent } from "./CollabResumeEndEvent";
export type { CollabWaitingBeginEvent } from "./CollabWaitingBeginEvent";
export type { CollabWaitingEndEvent } from "./CollabWaitingEndEvent";
export type { CollaborationMode } from "./CollaborationMode";
export type { CollaborationModeMask } from "./CollaborationModeMask";
export type { ContentItem } from "./ContentItem";
export type { ContextCompactedEvent } from "./ContextCompactedEvent";
export type { ContextCompactionItem } from "./ContextCompactionItem";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!

// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ModeKind } from "../ModeKind";
import type { ReasoningEffort } from "../ReasoningEffort";

/**
* EXPERIMENTAL - collaboration mode preset metadata for clients.
*/
export type CollaborationModeMask = { name: string, mode: ModeKind | null, model: string | null, reasoning_effort: ReasoningEffort | null | null, };
1 change: 1 addition & 0 deletions codex-rs/app-server-protocol/schema/typescript/v2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type { CollabAgentState } from "./CollabAgentState";
export type { CollabAgentStatus } from "./CollabAgentStatus";
export type { CollabAgentTool } from "./CollabAgentTool";
export type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus";
export type { CollaborationModeMask } from "./CollaborationModeMask";
export type { CommandAction } from "./CommandAction";
export type { CommandExecParams } from "./CommandExecParams";
export type { CommandExecResponse } from "./CommandExecResponse";
Expand Down
27 changes: 26 additions & 1 deletion codex-rs/app-server-protocol/src/protocol/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ use codex_protocol::approvals::NetworkApprovalProtocol as CoreNetworkApprovalPro
use codex_protocol::approvals::NetworkPolicyAmendment as CoreNetworkPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyRuleAction as CoreNetworkPolicyRuleAction;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::CollaborationModeMask;
use codex_protocol::config_types::CollaborationModeMask as CoreCollaborationModeMask;
use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::SandboxMode as CoreSandboxMode;
Expand Down Expand Up @@ -1409,6 +1410,30 @@ pub struct ModelListResponse {
#[ts(export_to = "v2/")]
pub struct CollaborationModeListParams {}

/// EXPERIMENTAL - collaboration mode preset metadata for clients.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CollaborationModeMask {
pub name: String,
pub mode: Option<ModeKind>,
pub model: Option<String>,
#[serde(rename = "reasoning_effort")]
#[ts(rename = "reasoning_effort")]
pub reasoning_effort: Option<Option<ReasoningEffort>>,
}

impl From<CoreCollaborationModeMask> for CollaborationModeMask {
fn from(value: CoreCollaborationModeMask) -> Self {
Self {
name: value.name,
mode: value.mode,
model: value.model,
reasoning_effort: value.reasoning_effort,
}
}
}

/// EXPERIMENTAL - collaboration mode presets response.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ Example with notification opt-out:
- `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation).
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options and optional `upgrade` model ids.
- `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. For non-beta flags, `displayName`/`description`/`announcement` are `null`.
- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination).
- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly.
- `skills/list` — list skills for one or more `cwd` values (optional `forceReload`).
- `skills/remote/list` — list public remote skills (**under development; do not call from production clients yet**).
- `skills/remote/export` — download a remote skill by `hazelnutId` into `skills` under `codex_home` (**under development; do not call from production clients yet**).
Expand Down
20 changes: 15 additions & 5 deletions codex-rs/app-server/src/codex_message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ use codex_core::find_thread_path_by_id_str;
use codex_core::git_info::git_diff_to_remote;
use codex_core::mcp::collect_mcp_snapshot;
use codex_core::mcp::group_tools_by_server;
use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig;
use codex_core::parse_cursor;
use codex_core::read_head_for_summary;
use codex_core::read_session_meta_line;
Expand Down Expand Up @@ -479,11 +480,13 @@ impl CodexMessageProcessor {
fn normalize_turn_start_collaboration_mode(
&self,
mut collaboration_mode: CollaborationMode,
collaboration_modes_config: CollaborationModesConfig,
) -> CollaborationMode {
if collaboration_mode.settings.developer_instructions.is_none()
&& let Some(instructions) = self
.thread_manager
.list_collaboration_modes()
.get_models_manager()
.list_collaboration_modes_for_config(collaboration_modes_config)
.into_iter()
.find(|preset| preset.mode == Some(collaboration_mode.mode))
.and_then(|preset| preset.developer_instructions.flatten())
Expand Down Expand Up @@ -3911,7 +3914,11 @@ impl CodexMessageProcessor {
params: CollaborationModeListParams,
) {
let CollaborationModeListParams {} = params;
let items = thread_manager.list_collaboration_modes();
let items = thread_manager
.list_collaboration_modes()
.into_iter()
.map(Into::into)
.collect();
let response = CollaborationModeListResponse { data: items };
outgoing.send_response(request_id, response).await;
}
Expand Down Expand Up @@ -5570,9 +5577,12 @@ impl CodexMessageProcessor {
}
};

let collaboration_mode = params
.collaboration_mode
.map(|mode| self.normalize_turn_start_collaboration_mode(mode));
let collaboration_modes_config = CollaborationModesConfig {
default_mode_request_user_input: thread.enabled(Feature::DefaultModeRequestUserInput),
};
let collaboration_mode = params.collaboration_mode.map(|mode| {
self.normalize_turn_start_collaboration_mode(mode, collaboration_modes_config)
});

// Map v2 input items to core input items.
let mapped_items: Vec<CoreInputItem> = params
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/app-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ use codex_core::default_client::USER_AGENT_SUFFIX;
use codex_core::default_client::get_codex_user_agent;
use codex_core::default_client::set_default_client_residency_requirement;
use codex_core::default_client::set_default_originator;
use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig;
use codex_feedback::CodexFeedback;
use codex_protocol::ThreadId;
use codex_protocol::protocol::SessionSource;
Expand Down Expand Up @@ -182,6 +183,11 @@ impl MessageProcessor {
auth_manager.clone(),
SessionSource::VSCode,
config.model_catalog.clone(),
CollaborationModesConfig {
default_mode_request_user_input: config
.features
.enabled(codex_core::features::Feature::DefaultModeRequestUserInput),
},
));
let cloud_requirements = Arc::new(RwLock::new(cloud_requirements));
let codex_message_processor = CodexMessageProcessor::new(CodexMessageProcessorArgs {
Expand Down
36 changes: 11 additions & 25 deletions codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@ use app_test_support::McpProcess;
use app_test_support::to_response;
use codex_app_server_protocol::CollaborationModeListParams;
use codex_app_server_protocol::CollaborationModeListResponse;
use codex_app_server_protocol::CollaborationModeMask;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_core::test_support::builtin_collaboration_mode_presets;
use codex_protocol::config_types::CollaborationModeMask;
use codex_protocol::config_types::ModeKind;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use tokio::time::timeout;
Expand All @@ -33,7 +32,7 @@ async fn list_collaboration_modes_returns_presets() -> Result<()> {
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;

let request_id = mcp
.send_list_collaboration_modes_request(CollaborationModeListParams {})
.send_list_collaboration_modes_request(CollaborationModeListParams::default())
.await?;

let response: JSONRPCResponse = timeout(
Expand All @@ -45,28 +44,15 @@ async fn list_collaboration_modes_returns_presets() -> Result<()> {
let CollaborationModeListResponse { data: items } =
to_response::<CollaborationModeListResponse>(response)?;

let expected = vec![plan_preset(), default_preset()];
let expected: Vec<CollaborationModeMask> = builtin_collaboration_mode_presets()
.into_iter()
.map(|preset| CollaborationModeMask {
name: preset.name,
mode: preset.mode,
model: preset.model,
reasoning_effort: preset.reasoning_effort,
})
.collect();
assert_eq!(expected, items);
Ok(())
}

/// Builds the plan preset that the list response is expected to return.
///
/// If the defaults change in the app server, this helper should be updated alongside the
/// contract, or the test will fail in ways that imply a regression in the API.
fn plan_preset() -> CollaborationModeMask {
let presets = builtin_collaboration_mode_presets();
presets
.into_iter()
.find(|p| p.mode == Some(ModeKind::Plan))
.unwrap()
}

/// Builds the default preset that the list response is expected to return.
fn default_preset() -> CollaborationModeMask {
let presets = builtin_collaboration_mode_presets();
presets
.into_iter()
.find(|p| p.mode == Some(ModeKind::Default))
.unwrap()
}
3 changes: 0 additions & 3 deletions codex-rs/app-server/tests/suite/v2/request_user_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,6 @@ sandbox_mode = "read-only"

model_provider = "mock_provider"

[features]
collaboration_modes = true

[model_providers.mock_provider]
name = "Mock provider for test"
base_url = "{server_uri}/v1"
Expand Down
91 changes: 89 additions & 2 deletions codex-rs/app-server/tests/suite/v2/turn_start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ use codex_protocol::openai_models::ReasoningEffort;
use core_test_support::responses;
use core_test_support::skip_if_no_network;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::Path;
use tempfile::TempDir;
use tokio::time::timeout;
Expand Down Expand Up @@ -352,7 +354,7 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> {
codex_home.path(),
&server.uri(),
"never",
&BTreeMap::default(),
&BTreeMap::from([(Feature::DefaultModeRequestUserInput, true)]),
)?;

let mut mcp = McpProcess::new(codex_home.path()).await?;
Expand Down Expand Up @@ -412,7 +414,92 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> {
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."));
assert!(payload_text.contains("The `request_user_input` tool is available in Default mode."));

Ok(())
}

#[tokio::test]
async fn turn_start_uses_thread_feature_overrides_for_collaboration_mode_instructions_v2()
-> 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::default(),
)?;

let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;

let thread_req = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.2-codex".to_string()),
config: Some(HashMap::from([(
"features.default_mode_request_user_input".to_string(),
json!(true),
)])),
..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, .. } = to_response::<ThreadStartResponse>(thread_resp)?;

let collaboration_mode = CollaborationMode {
mode: ModeKind::Default,
settings: Settings {
model: "mock-model-collab".to_string(),
reasoning_effort: Some(ReasoningEffort::High),
developer_instructions: None,
},
};

let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
}],
model: Some("mock-model-override".to_string()),
effort: Some(ReasoningEffort::Low),
summary: Some(ReasoningSummary::Auto),
output_schema: None,
collaboration_mode: Some(collaboration_mode),
..Default::default()
})
.await?;
let turn_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
)
.await??;
let _turn: TurnStartResponse = to_response::<TurnStartResponse>(turn_resp)?;

timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;

let request = response_mock.single_request();
let payload_text = request.body_json().to_string();
assert!(payload_text.contains("The `request_user_input` tool is available in Default mode."));

Ok(())
}
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,9 @@
"connectors": {
"type": "boolean"
},
"default_mode_request_user_input": {
"type": "boolean"
},
"elevated_windows_sandbox": {
"type": "boolean"
},
Expand Down Expand Up @@ -1621,6 +1624,9 @@
"connectors": {
"type": "boolean"
},
"default_mode_request_user_input": {
"type": "boolean"
},
"elevated_windows_sandbox": {
"type": "boolean"
},
Expand Down
Loading
Loading