diff --git a/codex-rs/codex-api/src/requests/chat.rs b/codex-rs/codex-api/src/requests/chat.rs index 60a591e5c2..13408fe20d 100644 --- a/codex-rs/codex-api/src/requests/chat.rs +++ b/codex-rs/codex-api/src/requests/chat.rs @@ -69,7 +69,9 @@ impl<'a> ChatRequestBuilder<'a> { last_emitted_role = Some("assistant") } ResponseItem::FunctionCallOutput { .. } => last_emitted_role = Some("tool"), - ResponseItem::Reasoning { .. } | ResponseItem::Other => {} + ResponseItem::Reasoning { .. } + | ResponseItem::CollaborationModeUpdate { .. } + | ResponseItem::Other => {} ResponseItem::CustomToolCall { .. } => {} ResponseItem::CustomToolCallOutput { .. } => {} ResponseItem::WebSearchCall { .. } => {} @@ -284,6 +286,7 @@ impl<'a> ChatRequestBuilder<'a> { } ResponseItem::Reasoning { .. } | ResponseItem::WebSearchCall { .. } + | ResponseItem::CollaborationModeUpdate { .. } | ResponseItem::Other | ResponseItem::Compaction { .. } => { continue; diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 2744fd7f35..0279fa8502 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -42,7 +42,6 @@ use async_channel::Receiver; use async_channel::Sender; use codex_protocol::ThreadId; use codex_protocol::approvals::ExecPolicyAmendment; -use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; use codex_protocol::config_types::WebSearchMode; use codex_protocol::dynamic_tools::DynamicToolResponse; @@ -206,6 +205,8 @@ use crate::windows_sandbox::WindowsSandboxLevelExt; use codex_async_utils::OrCancelExt; use codex_otel::OtelManager; use codex_protocol::config_types::CollaborationMode; +use codex_protocol::config_types::CollaborationModeMask; +use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; use codex_protocol::config_types::WindowsSandboxLevel; @@ -1400,6 +1401,26 @@ impl Session { } } + fn collaboration_mode_mask_from_mode(mode: &CollaborationMode) -> CollaborationModeMask { + CollaborationModeMask { + name: Self::collaboration_mode_name(mode.mode).to_string(), + mode: Some(mode.mode), + model: Some(mode.model().to_string()), + reasoning_effort: Some(mode.reasoning_effort()), + developer_instructions: Some(mode.settings.developer_instructions.clone()), + } + } + + fn collaboration_mode_name(kind: ModeKind) -> &'static str { + match kind { + ModeKind::Plan => "Plan", + ModeKind::Code => "Code", + ModeKind::PairProgramming => "Pair Programming", + ModeKind::Execute => "Execute", + ModeKind::Custom => "Custom", + } + } + fn build_settings_update_items( &self, previous_context: Option<&Arc>, @@ -1416,9 +1437,17 @@ impl Session { { update_items.push(permissions_item); } - if let Some(collaboration_mode_item) = - self.build_collaboration_mode_update_item(previous_context, current_context) - { + if let Some(next_mode) = next_collaboration_mode { + if previous_collaboration_mode != next_mode { + update_items.push(ResponseItem::CollaborationModeUpdate { + mask: Self::collaboration_mode_mask_from_mode(next_mode), + }); + } + } + if let Some(collaboration_mode_item) = self.build_collaboration_mode_update_item( + previous_collaboration_mode, + next_collaboration_mode, + ) { update_items.push(collaboration_mode_item); } if let Some(personality_item) = @@ -1924,6 +1953,9 @@ impl Session { state.session_configuration.base_instructions.clone(), ) }; + items.push(ResponseItem::CollaborationModeUpdate { + mask: Self::collaboration_mode_mask_from_mode(&collaboration_mode), + }); if let Some(collab_instructions) = DeveloperInstructions::from_collaboration_mode(&collaboration_mode) { @@ -3117,6 +3149,9 @@ mod handlers { fn last_collaboration_mask(items: &[ResponseItem]) -> Option { items.iter().rev().find_map(|item| { + if let ResponseItem::CollaborationModeUpdate { mask } = item { + return Some(mask.clone()); + } let ResponseItem::Message { role, content, .. } = item else { return None; }; diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index a29f7df7e0..1ee297bcf2 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -59,7 +59,9 @@ impl ContextManager { for item in items { let item_ref = item.deref(); let is_ghost_snapshot = matches!(item_ref, ResponseItem::GhostSnapshot { .. }); - if !is_api_message(item_ref) && !is_ghost_snapshot { + let is_collaboration_mode_update = + matches!(item_ref, ResponseItem::CollaborationModeUpdate { .. }); + if !is_api_message(item_ref) && !is_ghost_snapshot && !is_collaboration_mode_update { continue; } @@ -72,8 +74,12 @@ impl ContextManager { /// normalization and drop un-suited items. pub(crate) fn for_prompt(mut self) -> Vec { self.normalize_history(); - self.items - .retain(|item| !matches!(item, ResponseItem::GhostSnapshot { .. })); + self.items.retain(|item| { + !matches!( + item, + ResponseItem::GhostSnapshot { .. } | ResponseItem::CollaborationModeUpdate { .. } + ) + }); self.items } @@ -302,6 +308,7 @@ impl ContextManager { | ResponseItem::CustomToolCall { .. } | ResponseItem::Compaction { .. } | ResponseItem::GhostSnapshot { .. } + | ResponseItem::CollaborationModeUpdate { .. } | ResponseItem::Other => item.clone(), } } @@ -321,6 +328,7 @@ fn is_api_message(message: &ResponseItem) -> bool { | ResponseItem::WebSearchCall { .. } | ResponseItem::Compaction { .. } => true, ResponseItem::GhostSnapshot { .. } => false, + ResponseItem::CollaborationModeUpdate { .. } => false, ResponseItem::Other => false, } } @@ -335,7 +343,7 @@ fn estimate_reasoning_length(encoded_len: usize) -> usize { fn estimate_item_token_count(item: &ResponseItem) -> i64 { match item { - ResponseItem::GhostSnapshot { .. } => 0, + ResponseItem::GhostSnapshot { .. } | ResponseItem::CollaborationModeUpdate { .. } => 0, ResponseItem::Reasoning { encrypted_content: Some(content), .. diff --git a/codex-rs/core/src/rollout/policy.rs b/codex-rs/core/src/rollout/policy.rs index 5796bd1961..31b9cbcb61 100644 --- a/codex-rs/core/src/rollout/policy.rs +++ b/codex-rs/core/src/rollout/policy.rs @@ -27,6 +27,7 @@ pub(crate) fn should_persist_response_item(item: &ResponseItem) -> bool { | ResponseItem::CustomToolCall { .. } | ResponseItem::CustomToolCallOutput { .. } | ResponseItem::WebSearchCall { .. } + | ResponseItem::CollaborationModeUpdate { .. } | ResponseItem::GhostSnapshot { .. } | ResponseItem::Compaction { .. } => true, ResponseItem::Other => false, diff --git a/codex-rs/otel/src/traces/otel_manager.rs b/codex-rs/otel/src/traces/otel_manager.rs index c585ec67d5..445831adac 100644 --- a/codex-rs/otel/src/traces/otel_manager.rs +++ b/codex-rs/otel/src/traces/otel_manager.rs @@ -679,6 +679,7 @@ impl OtelManager { ResponseItem::CustomToolCall { .. } => "custom_tool_call".into(), ResponseItem::CustomToolCallOutput { .. } => "custom_tool_call_output".into(), ResponseItem::WebSearchCall { .. } => "web_search_call".into(), + ResponseItem::CollaborationModeUpdate { .. } => "collaboration_mode_update".into(), ResponseItem::GhostSnapshot { .. } => "ghost_snapshot".into(), ResponseItem::Compaction { .. } => "compaction".into(), ResponseItem::Other => "other".into(), diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 74cc9a53f1..bbae6f9af2 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -9,6 +9,7 @@ use serde::ser::Serializer; use ts_rs::TS; use crate::config_types::CollaborationMode; +use crate::config_types::CollaborationModeMask; use crate::config_types::SandboxMode; use crate::protocol::AskForApproval; use crate::protocol::COLLABORATION_MODE_CLOSE_TAG; @@ -98,6 +99,9 @@ pub enum ResponseItem { #[ts(optional)] phase: Option, }, + CollaborationModeUpdate { + mask: CollaborationModeMask, + }, Reasoning { #[serde(default, skip_serializing)] #[ts(skip)]