Move apply-patch file changes into turn items (#20540)

## Why

Apply-patch file changes are now part of the core turn item stream, so
v2 clients can consume the same first-class item lifecycle path used by
other turn items instead of relying on app-server-specific remapping
from legacy patch events.

## What changed

- Added a core `TurnItem::FileChange` carrying apply-patch changes and
completion metadata.
- Updated the apply-patch tool emitter to send `ItemStarted` /
`ItemCompleted` with the new `FileChange` item while preserving legacy
`PatchApplyBegin` / `PatchApplyEnd` fan-out.
- Updated app-server v2 conversion to render the new core item directly
and stopped `event_mapping` from remapping old patch begin/end events
into item notifications.
- Kept thread history reconstruction based on the existing old
apply-patch events for rollout compatibility.

## Verification

- `cargo test -p codex-protocol -p codex-app-server-protocol`
- `cargo test -p codex-core --test all
apply_patch_tool_executes_and_emits_patch_events`
- `cargo test -p codex-app-server bespoke_event_handling`
This commit is contained in:
pakrym-oai
2026-05-01 08:47:18 -07:00
committed by GitHub
parent 0b04d1b3cc
commit f476338f93
10 changed files with 240 additions and 165 deletions
+54
View File
@@ -8,7 +8,11 @@ use crate::protocol::AgentReasoningEvent;
use crate::protocol::AgentReasoningRawContentEvent;
use crate::protocol::ContextCompactedEvent;
use crate::protocol::EventMsg;
use crate::protocol::FileChange;
use crate::protocol::ImageGenerationEndEvent;
use crate::protocol::PatchApplyBeginEvent;
use crate::protocol::PatchApplyEndEvent;
use crate::protocol::PatchApplyStatus;
use crate::protocol::UserMessageEvent;
use crate::protocol::WebSearchEndEvent;
use crate::user_input::ByteRange;
@@ -20,6 +24,8 @@ use quick_xml::se::to_string as to_xml_string;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::path::PathBuf;
use ts_rs::TS;
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
@@ -33,6 +39,7 @@ pub enum TurnItem {
Reasoning(ReasoningItem),
WebSearch(WebSearchItem),
ImageGeneration(ImageGenerationItem),
FileChange(FileChangeItem),
ContextCompaction(ContextCompactionItem),
}
@@ -127,6 +134,24 @@ pub struct ImageGenerationItem {
pub saved_path: Option<AbsolutePathBuf>,
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]
pub struct FileChangeItem {
pub id: String,
pub changes: HashMap<PathBuf, FileChange>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub status: Option<PatchApplyStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub auto_approved: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub stdout: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub stderr: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
pub struct ContextCompactionItem {
pub id: String,
@@ -381,6 +406,30 @@ impl ImageGenerationItem {
}
}
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 TurnItem {
pub fn id(&self) -> String {
match self {
@@ -391,6 +440,7 @@ impl TurnItem {
TurnItem::Reasoning(item) => item.id.clone(),
TurnItem::WebSearch(item) => item.id.clone(),
TurnItem::ImageGeneration(item) => item.id.clone(),
TurnItem::FileChange(item) => item.id.clone(),
TurnItem::ContextCompaction(item) => item.id.clone(),
}
}
@@ -403,6 +453,10 @@ impl TurnItem {
TurnItem::Plan(_) => Vec::new(),
TurnItem::WebSearch(item) => vec![item.as_legacy_event()],
TurnItem::ImageGeneration(item) => vec![item.as_legacy_event()],
TurnItem::FileChange(item) => item
.as_legacy_end_event(String::new())
.into_iter()
.collect(),
TurnItem::Reasoning(item) => item.as_legacy_events(show_raw_agent_reasoning),
TurnItem::ContextCompaction(item) => vec![item.as_legacy_event()],
}