refactor(protocol): isolate legacy item fanout (#30956)

## Stack

1. [#30956](https://github.com/openai/codex/pull/30956) — isolate legacy
item fanout ← **this PR**
2. [#30283](https://github.com/openai/codex/pull/30283) — emit canonical
`TurnItem` lifecycle
3. [#30188](https://github.com/openai/codex/pull/30188) — persist
canonical items for paginated threads

## Description

Move legacy `EventMsg` projection code out of the canonical item and
protocol schema modules into `protocol/src/legacy_events.rs`.

This is a behavior-neutral extraction. It keeps the existing
`HasLegacyEvent` API and the existing legacy projections unchanged,
while giving compatibility fanout a single home.

## Why

Canonical `TurnItem` types and wire event schemas should not own the
implementation details for legacy compatibility projections. Isolating
that code makes the boundary explicit and keeps follow-up canonical
lifecycle work easier to review.
This commit is contained in:
Owen Lin
2026-07-06 12:22:40 -07:00
committed by GitHub
parent 2e20d2ef44
commit 7affe3e3e4
4 changed files with 292 additions and 261 deletions
-189
View File
@@ -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<EventMsg> {
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<EventMsg> {
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<EventMsg> {
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<EventMsg> {
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<EventMsg> {
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)]
+289
View File
@@ -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<EventMsg>;
}
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<EventMsg> {
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<EventMsg> {
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<EventMsg> {
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<EventMsg> {
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<EventMsg> {
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<EventMsg> {
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<EventMsg> {
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<EventMsg> {
Vec::new()
}
}
impl HasLegacyEvent for ReasoningContentDeltaEvent {
fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
Vec::new()
}
}
impl HasLegacyEvent for ReasoningRawContentDeltaEvent {
fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
Vec::new()
}
}
impl HasLegacyEvent for EventMsg {
fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg> {
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(),
}
}
}
+1
View File
@@ -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;
+2 -72
View File
@@ -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<EventMsg> {
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<EventMsg>;
}
impl HasLegacyEvent for ItemCompletedEvent {
fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg> {
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<EventMsg> {
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<EventMsg> {
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<EventMsg> {
Vec::new()
}
}
impl HasLegacyEvent for EventMsg {
fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg> {
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<ReviewOutputEvent>,
@@ -5386,6 +5315,7 @@ mod tests {
let event = serde_json::from_value::<ItemCompletedEvent>(value).unwrap();
assert_eq!(event.completed_at_ms, 0);
}
#[test]
fn rollback_failed_error_does_not_affect_turn_status() {
let event = ErrorEvent {