diff --git a/codex-rs/codex-api/src/requests/chat.rs b/codex-rs/codex-api/src/requests/chat.rs index 5c16a5fb58..95fd97f742 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 ae6d3654f5..1e58f00f27 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -40,7 +40,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; @@ -203,6 +202,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; @@ -1365,6 +1366,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>, @@ -1383,6 +1404,13 @@ impl Session { { update_items.push(permissions_item); } + 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, @@ -1891,6 +1919,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) { @@ -3120,6 +3151,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 1638a243cc..d9a1e8c022 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 } @@ -94,7 +100,8 @@ impl ContextManager { let items_tokens = self.items.iter().fold(0i64, |acc, item| { acc + match item { - ResponseItem::GhostSnapshot { .. } => 0, + ResponseItem::GhostSnapshot { .. } + | ResponseItem::CollaborationModeUpdate { .. } => 0, ResponseItem::Reasoning { encrypted_content: Some(content), .. @@ -301,6 +308,7 @@ impl ContextManager { | ResponseItem::CustomToolCall { .. } | ResponseItem::Compaction { .. } | ResponseItem::GhostSnapshot { .. } + | ResponseItem::CollaborationModeUpdate { .. } | ResponseItem::Other => item.clone(), } } @@ -320,6 +328,7 @@ fn is_api_message(message: &ResponseItem) -> bool { | ResponseItem::WebSearchCall { .. } | ResponseItem::Compaction { .. } => true, ResponseItem::GhostSnapshot { .. } => false, + ResponseItem::CollaborationModeUpdate { .. } => false, ResponseItem::Other => false, } } 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 9a7744ee60..75542e44ec 100644 --- a/codex-rs/otel/src/traces/otel_manager.rs +++ b/codex-rs/otel/src/traces/otel_manager.rs @@ -545,6 +545,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 8e67fc40fc..73cc0eb4ed 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -11,6 +11,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; @@ -86,6 +87,9 @@ pub enum ResponseItem { #[ts(optional)] end_turn: Option, }, + CollaborationModeUpdate { + mask: CollaborationModeMask, + }, Reasoning { #[serde(default, skip_serializing)] #[ts(skip)]