diff --git a/codex-rs/protocol/src/items.rs b/codex-rs/protocol/src/items.rs index c31957344637..820c3b774b25 100644 --- a/codex-rs/protocol/src/items.rs +++ b/codex-rs/protocol/src/items.rs @@ -10,26 +10,12 @@ use crate::models::ResponseItem; use crate::models::WebSearchAction; use crate::openai_models::ReasoningEffort as ReasoningEffortConfig; use crate::parse_command::ParsedCommand; -use crate::protocol::AgentMessageEvent; -use crate::protocol::AgentReasoningEvent; -use crate::protocol::AgentReasoningRawContentEvent; use crate::protocol::AgentStatus; use crate::protocol::CollabAgentRef; -use crate::protocol::ContextCompactedEvent; -use crate::protocol::EventMsg; use crate::protocol::ExecCommandSource; use crate::protocol::FileChange; -use crate::protocol::ImageGenerationEndEvent; -use crate::protocol::McpInvocation; -use crate::protocol::McpToolCallBeginEvent; -use crate::protocol::McpToolCallEndEvent; -use crate::protocol::PatchApplyBeginEvent; -use crate::protocol::PatchApplyEndEvent; use crate::protocol::PatchApplyStatus; use crate::protocol::SubAgentActivityKind; -use crate::protocol::UserMessageEvent; -use crate::protocol::ViewImageToolCallEvent; -use crate::protocol::WebSearchEndEvent; use crate::user_input::ByteRange; use crate::user_input::TextElement; use crate::user_input::UserInput; @@ -387,10 +373,6 @@ impl ContextCompactionItem { id: uuid::Uuid::new_v4().to_string(), } } - - pub fn as_legacy_event(&self) -> EventMsg { - EventMsg::ContextCompacted(ContextCompactedEvent {}) - } } impl Default for ContextCompactionItem { @@ -408,20 +390,6 @@ impl UserMessageItem { } } - pub fn as_legacy_event(&self) -> EventMsg { - // Legacy user-message events flatten only text inputs into `message` and - // rebase text element ranges onto that concatenated text. - EventMsg::UserMessage(UserMessageEvent { - client_id: self.client_id.clone(), - message: self.message(), - images: Some(self.image_urls()), - image_details: self.image_details(), - local_images: self.local_image_paths(), - local_image_details: self.local_image_details(), - text_elements: self.text_elements(), - }) - } - pub fn message(&self) -> String { self.content .iter() @@ -609,134 +577,6 @@ impl AgentMessageItem { memory_citation: None, } } - - pub fn as_legacy_events(&self) -> Vec { - self.content - .iter() - .map(|c| match c { - AgentMessageContent::Text { text } => EventMsg::AgentMessage(AgentMessageEvent { - message: text.clone(), - phase: self.phase.clone(), - memory_citation: self.memory_citation.clone(), - }), - }) - .collect() - } -} - -impl ReasoningItem { - pub fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec { - let mut events = Vec::new(); - for summary in &self.summary_text { - events.push(EventMsg::AgentReasoning(AgentReasoningEvent { - text: summary.clone(), - })); - } - - if show_raw_agent_reasoning { - for entry in &self.raw_content { - events.push(EventMsg::AgentReasoningRawContent( - AgentReasoningRawContentEvent { - text: entry.clone(), - }, - )); - } - } - - events - } -} - -impl WebSearchItem { - pub fn as_legacy_event(&self) -> EventMsg { - EventMsg::WebSearchEnd(WebSearchEndEvent { - call_id: self.id.clone(), - query: self.query.clone(), - action: self.action.clone(), - }) - } -} - -impl ImageGenerationItem { - pub fn as_legacy_event(&self) -> EventMsg { - EventMsg::ImageGenerationEnd(ImageGenerationEndEvent { - call_id: self.id.clone(), - status: self.status.clone(), - revised_prompt: self.revised_prompt.clone(), - result: self.result.clone(), - saved_path: self.saved_path.clone(), - }) - } -} - -impl FileChangeItem { - pub fn as_legacy_begin_event(&self, turn_id: String) -> EventMsg { - EventMsg::PatchApplyBegin(PatchApplyBeginEvent { - call_id: self.id.clone(), - turn_id, - auto_approved: self.auto_approved.unwrap_or(false), - changes: self.changes.clone(), - }) - } - - pub fn as_legacy_end_event(&self, turn_id: String) -> Option { - let status = self.status.clone()?; - Some(EventMsg::PatchApplyEnd(PatchApplyEndEvent { - call_id: self.id.clone(), - turn_id, - stdout: self.stdout.clone().unwrap_or_default(), - stderr: self.stderr.clone().unwrap_or_default(), - success: status == PatchApplyStatus::Completed, - changes: self.changes.clone(), - status, - })) - } -} - -impl McpToolCallItem { - pub fn as_legacy_begin_event(&self) -> EventMsg { - EventMsg::McpToolCallBegin(McpToolCallBeginEvent { - call_id: self.id.clone(), - invocation: McpInvocation { - server: self.server.clone(), - tool: self.tool.clone(), - arguments: (!self.arguments.is_null()).then(|| self.arguments.clone()), - }, - connector_id: self.connector_id.clone(), - mcp_app_resource_uri: self.mcp_app_resource_uri.clone(), - link_id: self.link_id.clone(), - app_name: self.app_name.clone(), - template_id: self.template_id.clone(), - action_name: self.action_name.clone(), - plugin_id: self.plugin_id.clone(), - }) - } - - pub fn as_legacy_end_event(&self) -> Option { - let result = match (&self.result, &self.error) { - (Some(result), _) => Ok(result.clone()), - (None, Some(error)) => Err(error.message.clone()), - (None, None) => return None, - }; - - Some(EventMsg::McpToolCallEnd(McpToolCallEndEvent { - call_id: self.id.clone(), - invocation: McpInvocation { - server: self.server.clone(), - tool: self.tool.clone(), - arguments: (!self.arguments.is_null()).then(|| self.arguments.clone()), - }, - mcp_app_resource_uri: self.mcp_app_resource_uri.clone(), - connector_id: self.connector_id.clone(), - link_id: self.link_id.clone(), - app_name: self.app_name.clone(), - template_id: self.template_id.clone(), - action_name: self.action_name.clone(), - plugin_id: self.plugin_id.clone(), - duration: self.duration?, - result, - })) - } } impl TurnItem { @@ -760,35 +600,6 @@ impl TurnItem { TurnItem::ContextCompaction(item) => item.id.clone(), } } - - pub fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec { - match self { - TurnItem::UserMessage(item) => vec![item.as_legacy_event()], - TurnItem::HookPrompt(_) => Vec::new(), - TurnItem::AgentMessage(item) => item.as_legacy_events(), - TurnItem::Plan(_) => Vec::new(), - TurnItem::CommandExecution(_) - | TurnItem::DynamicToolCall(_) - | TurnItem::CollabAgentToolCall(_) => Vec::new(), - TurnItem::SubAgentActivity(_) => Vec::new(), - TurnItem::WebSearch(item) => vec![item.as_legacy_event()], - TurnItem::ImageView(item) => { - vec![EventMsg::ViewImageToolCall(ViewImageToolCallEvent { - call_id: item.id.clone(), - path: item.path.clone(), - })] - } - TurnItem::Sleep(_) => Vec::new(), - TurnItem::ImageGeneration(item) => vec![item.as_legacy_event()], - TurnItem::FileChange(item) => item - .as_legacy_end_event(String::new()) - .into_iter() - .collect(), - TurnItem::McpToolCall(item) => item.as_legacy_end_event().into_iter().collect(), - TurnItem::Reasoning(item) => item.as_legacy_events(show_raw_agent_reasoning), - TurnItem::ContextCompaction(item) => vec![item.as_legacy_event()], - } - } } #[cfg(test)] diff --git a/codex-rs/protocol/src/legacy_events.rs b/codex-rs/protocol/src/legacy_events.rs new file mode 100644 index 000000000000..f4a511d36757 --- /dev/null +++ b/codex-rs/protocol/src/legacy_events.rs @@ -0,0 +1,289 @@ +use crate::items::AgentMessageContent; +use crate::items::AgentMessageItem; +use crate::items::ContextCompactionItem; +use crate::items::FileChangeItem; +use crate::items::ImageGenerationItem; +use crate::items::McpToolCallItem; +use crate::items::ReasoningItem; +use crate::items::TurnItem; +use crate::items::UserMessageItem; +use crate::items::WebSearchItem; +use crate::protocol::AgentMessageContentDeltaEvent; +use crate::protocol::AgentMessageEvent; +use crate::protocol::AgentReasoningEvent; +use crate::protocol::AgentReasoningRawContentEvent; +use crate::protocol::ContextCompactedEvent; +use crate::protocol::EventMsg; +use crate::protocol::ImageGenerationBeginEvent; +use crate::protocol::ImageGenerationEndEvent; +use crate::protocol::ItemCompletedEvent; +use crate::protocol::ItemStartedEvent; +use crate::protocol::McpInvocation; +use crate::protocol::McpToolCallBeginEvent; +use crate::protocol::McpToolCallEndEvent; +use crate::protocol::PatchApplyBeginEvent; +use crate::protocol::PatchApplyEndEvent; +use crate::protocol::PatchApplyStatus; +use crate::protocol::ReasoningContentDeltaEvent; +use crate::protocol::ReasoningRawContentDeltaEvent; +use crate::protocol::UserMessageEvent; +use crate::protocol::ViewImageToolCallEvent; +use crate::protocol::WebSearchBeginEvent; +use crate::protocol::WebSearchEndEvent; + +/// Converts canonical item lifecycle events back into the legacy raw event stream used by +/// compatibility consumers that have not migrated to `TurnItem`. +pub trait HasLegacyEvent { + fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec; +} + +impl ContextCompactionItem { + pub fn as_legacy_event(&self) -> EventMsg { + EventMsg::ContextCompacted(ContextCompactedEvent {}) + } +} + +impl UserMessageItem { + pub fn as_legacy_event(&self) -> EventMsg { + // Legacy user-message events flatten only text inputs into `message` and + // rebase text element ranges onto that concatenated text. + EventMsg::UserMessage(UserMessageEvent { + client_id: self.client_id.clone(), + message: self.message(), + images: Some(self.image_urls()), + image_details: self.image_details(), + local_images: self.local_image_paths(), + local_image_details: self.local_image_details(), + text_elements: self.text_elements(), + }) + } +} + +impl AgentMessageItem { + pub fn as_legacy_events(&self) -> Vec { + self.content + .iter() + .map(|c| match c { + AgentMessageContent::Text { text } => EventMsg::AgentMessage(AgentMessageEvent { + message: text.clone(), + phase: self.phase.clone(), + memory_citation: self.memory_citation.clone(), + }), + }) + .collect() + } +} + +impl ReasoningItem { + pub fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec { + let mut events = Vec::new(); + for summary in &self.summary_text { + events.push(EventMsg::AgentReasoning(AgentReasoningEvent { + text: summary.clone(), + })); + } + + if show_raw_agent_reasoning { + for entry in &self.raw_content { + events.push(EventMsg::AgentReasoningRawContent( + AgentReasoningRawContentEvent { + text: entry.clone(), + }, + )); + } + } + + events + } +} + +impl WebSearchItem { + pub fn as_legacy_event(&self) -> EventMsg { + EventMsg::WebSearchEnd(WebSearchEndEvent { + call_id: self.id.clone(), + query: self.query.clone(), + action: self.action.clone(), + }) + } +} + +impl ImageGenerationItem { + pub fn as_legacy_event(&self) -> EventMsg { + EventMsg::ImageGenerationEnd(ImageGenerationEndEvent { + call_id: self.id.clone(), + status: self.status.clone(), + revised_prompt: self.revised_prompt.clone(), + result: self.result.clone(), + saved_path: self.saved_path.clone(), + }) + } +} + +impl FileChangeItem { + pub fn as_legacy_begin_event(&self, turn_id: String) -> EventMsg { + EventMsg::PatchApplyBegin(PatchApplyBeginEvent { + call_id: self.id.clone(), + turn_id, + auto_approved: self.auto_approved.unwrap_or(false), + changes: self.changes.clone(), + }) + } + + pub fn as_legacy_end_event(&self, turn_id: String) -> Option { + let status = self.status.clone()?; + Some(EventMsg::PatchApplyEnd(PatchApplyEndEvent { + call_id: self.id.clone(), + turn_id, + stdout: self.stdout.clone().unwrap_or_default(), + stderr: self.stderr.clone().unwrap_or_default(), + success: status == PatchApplyStatus::Completed, + changes: self.changes.clone(), + status, + })) + } +} + +impl McpToolCallItem { + pub fn as_legacy_begin_event(&self) -> EventMsg { + EventMsg::McpToolCallBegin(McpToolCallBeginEvent { + call_id: self.id.clone(), + invocation: McpInvocation { + server: self.server.clone(), + tool: self.tool.clone(), + arguments: (!self.arguments.is_null()).then(|| self.arguments.clone()), + }, + connector_id: self.connector_id.clone(), + mcp_app_resource_uri: self.mcp_app_resource_uri.clone(), + link_id: self.link_id.clone(), + app_name: self.app_name.clone(), + template_id: self.template_id.clone(), + action_name: self.action_name.clone(), + plugin_id: self.plugin_id.clone(), + }) + } + + pub fn as_legacy_end_event(&self) -> Option { + let result = match (&self.result, &self.error) { + (Some(result), _) => Ok(result.clone()), + (None, Some(error)) => Err(error.message.clone()), + (None, None) => return None, + }; + + Some(EventMsg::McpToolCallEnd(McpToolCallEndEvent { + call_id: self.id.clone(), + invocation: McpInvocation { + server: self.server.clone(), + tool: self.tool.clone(), + arguments: (!self.arguments.is_null()).then(|| self.arguments.clone()), + }, + mcp_app_resource_uri: self.mcp_app_resource_uri.clone(), + connector_id: self.connector_id.clone(), + link_id: self.link_id.clone(), + app_name: self.app_name.clone(), + template_id: self.template_id.clone(), + action_name: self.action_name.clone(), + plugin_id: self.plugin_id.clone(), + duration: self.duration?, + result, + })) + } +} + +impl TurnItem { + pub fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec { + match self { + TurnItem::UserMessage(item) => vec![item.as_legacy_event()], + TurnItem::HookPrompt(_) => Vec::new(), + TurnItem::AgentMessage(item) => item.as_legacy_events(), + TurnItem::Plan(_) => Vec::new(), + TurnItem::CommandExecution(_) + | TurnItem::DynamicToolCall(_) + | TurnItem::CollabAgentToolCall(_) => Vec::new(), + TurnItem::SubAgentActivity(_) => Vec::new(), + TurnItem::WebSearch(item) => vec![item.as_legacy_event()], + TurnItem::ImageView(item) => { + vec![EventMsg::ViewImageToolCall(ViewImageToolCallEvent { + call_id: item.id.clone(), + path: item.path.clone(), + })] + } + TurnItem::Sleep(_) => Vec::new(), + TurnItem::ImageGeneration(item) => vec![item.as_legacy_event()], + TurnItem::FileChange(item) => item + .as_legacy_end_event(String::new()) + .into_iter() + .collect(), + TurnItem::McpToolCall(item) => item.as_legacy_end_event().into_iter().collect(), + TurnItem::Reasoning(item) => item.as_legacy_events(show_raw_agent_reasoning), + TurnItem::ContextCompaction(item) => vec![item.as_legacy_event()], + } + } +} + +impl HasLegacyEvent for ItemStartedEvent { + fn as_legacy_events(&self, _: bool) -> Vec { + match &self.item { + TurnItem::WebSearch(item) => vec![EventMsg::WebSearchBegin(WebSearchBeginEvent { + call_id: item.id.clone(), + })], + TurnItem::ImageView(_) => Vec::new(), + TurnItem::ImageGeneration(item) => { + vec![EventMsg::ImageGenerationBegin(ImageGenerationBeginEvent { + call_id: item.id.clone(), + })] + } + TurnItem::FileChange(item) => vec![item.as_legacy_begin_event(self.turn_id.clone())], + TurnItem::McpToolCall(item) => vec![item.as_legacy_begin_event()], + _ => Vec::new(), + } + } +} + +impl HasLegacyEvent for ItemCompletedEvent { + fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec { + match &self.item { + TurnItem::FileChange(item) => item + .as_legacy_end_event(self.turn_id.clone()) + .into_iter() + .collect(), + _ => self.item.as_legacy_events(show_raw_agent_reasoning), + } + } +} + +impl HasLegacyEvent for AgentMessageContentDeltaEvent { + fn as_legacy_events(&self, _: bool) -> Vec { + Vec::new() + } +} + +impl HasLegacyEvent for ReasoningContentDeltaEvent { + fn as_legacy_events(&self, _: bool) -> Vec { + Vec::new() + } +} + +impl HasLegacyEvent for ReasoningRawContentDeltaEvent { + fn as_legacy_events(&self, _: bool) -> Vec { + Vec::new() + } +} + +impl HasLegacyEvent for EventMsg { + fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec { + match self { + EventMsg::ItemStarted(event) => event.as_legacy_events(show_raw_agent_reasoning), + EventMsg::ItemCompleted(event) => event.as_legacy_events(show_raw_agent_reasoning), + EventMsg::AgentMessageContentDelta(event) => { + event.as_legacy_events(show_raw_agent_reasoning) + } + EventMsg::ReasoningContentDelta(event) => { + event.as_legacy_events(show_raw_agent_reasoning) + } + EventMsg::ReasoningRawContentDelta(event) => { + event.as_legacy_events(show_raw_agent_reasoning) + } + _ => Vec::new(), + } + } +} diff --git a/codex-rs/protocol/src/lib.rs b/codex-rs/protocol/src/lib.rs index c50e95d37f97..636c64ed7bfa 100644 --- a/codex-rs/protocol/src/lib.rs +++ b/codex-rs/protocol/src/lib.rs @@ -16,6 +16,7 @@ pub mod dynamic_tools; pub mod error; pub mod exec_output; pub mod items; +mod legacy_events; pub mod mcp; pub mod mcp_approval_meta; pub mod memory_citation; diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 96a793bcd38f..9daa466ef89a 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -84,6 +84,7 @@ pub use crate::approvals::NetworkApprovalContext; pub use crate::approvals::NetworkApprovalProtocol; pub use crate::approvals::NetworkPolicyAmendment; pub use crate::approvals::NetworkPolicyRuleAction; +pub use crate::legacy_events::HasLegacyEvent; pub use crate::permissions::FileSystemAccessMode; pub use crate::permissions::FileSystemPath; pub use crate::permissions::FileSystemSandboxEntry; @@ -1794,25 +1795,6 @@ pub struct ItemStartedEvent { pub started_at_ms: i64, } -impl HasLegacyEvent for ItemStartedEvent { - fn as_legacy_events(&self, _: bool) -> Vec { - match &self.item { - TurnItem::WebSearch(item) => vec![EventMsg::WebSearchBegin(WebSearchBeginEvent { - call_id: item.id.clone(), - })], - TurnItem::ImageView(_) => Vec::new(), - TurnItem::ImageGeneration(item) => { - vec![EventMsg::ImageGenerationBegin(ImageGenerationBeginEvent { - call_id: item.id.clone(), - })] - } - TurnItem::FileChange(item) => vec![item.as_legacy_begin_event(self.turn_id.clone())], - TurnItem::McpToolCall(item) => vec![item.as_legacy_begin_event()], - _ => Vec::new(), - } - } -} - #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)] pub struct ItemCompletedEvent { pub thread_id: ThreadId, @@ -1829,22 +1811,6 @@ const fn default_item_completed_at_ms() -> i64 { 0 } -pub trait HasLegacyEvent { - fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec; -} - -impl HasLegacyEvent for ItemCompletedEvent { - fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec { - match &self.item { - TurnItem::FileChange(item) => item - .as_legacy_end_event(self.turn_id.clone()) - .into_iter() - .collect(), - _ => self.item.as_legacy_events(show_raw_agent_reasoning), - } - } -} - #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)] pub struct AgentMessageContentDeltaEvent { pub thread_id: String, @@ -1853,12 +1819,6 @@ pub struct AgentMessageContentDeltaEvent { pub delta: String, } -impl HasLegacyEvent for AgentMessageContentDeltaEvent { - fn as_legacy_events(&self, _: bool) -> Vec { - Vec::new() - } -} - #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)] pub struct PlanDeltaEvent { pub thread_id: String, @@ -1878,12 +1838,6 @@ pub struct ReasoningContentDeltaEvent { pub summary_index: i64, } -impl HasLegacyEvent for ReasoningContentDeltaEvent { - fn as_legacy_events(&self, _: bool) -> Vec { - Vec::new() - } -} - #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)] pub struct ReasoningRawContentDeltaEvent { pub thread_id: String, @@ -1895,31 +1849,6 @@ pub struct ReasoningRawContentDeltaEvent { pub content_index: i64, } -impl HasLegacyEvent for ReasoningRawContentDeltaEvent { - fn as_legacy_events(&self, _: bool) -> Vec { - Vec::new() - } -} - -impl HasLegacyEvent for EventMsg { - fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec { - match self { - EventMsg::ItemStarted(event) => event.as_legacy_events(show_raw_agent_reasoning), - EventMsg::ItemCompleted(event) => event.as_legacy_events(show_raw_agent_reasoning), - EventMsg::AgentMessageContentDelta(event) => { - event.as_legacy_events(show_raw_agent_reasoning) - } - EventMsg::ReasoningContentDelta(event) => { - event.as_legacy_events(show_raw_agent_reasoning) - } - EventMsg::ReasoningRawContentDelta(event) => { - event.as_legacy_events(show_raw_agent_reasoning) - } - _ => Vec::new(), - } - } -} - #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] pub struct ExitedReviewModeEvent { pub review_output: Option, @@ -5386,6 +5315,7 @@ mod tests { let event = serde_json::from_value::(value).unwrap(); assert_eq!(event.completed_at_ms, 0); } + #[test] fn rollback_failed_error_does_not_affect_turn_status() { let event = ErrorEvent {