mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
@@ -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()],
|
||||
}
|
||||
|
||||
@@ -1841,6 +1841,7 @@ impl HasLegacyEvent for ItemStartedEvent {
|
||||
call_id: item.id.clone(),
|
||||
})]
|
||||
}
|
||||
TurnItem::FileChange(item) => vec![item.as_legacy_begin_event(self.turn_id.clone())],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -1859,7 +1860,13 @@ pub trait HasLegacyEvent {
|
||||
|
||||
impl HasLegacyEvent for ItemCompletedEvent {
|
||||
fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg> {
|
||||
self.item.as_legacy_events(show_raw_agent_reasoning)
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3928,6 +3935,7 @@ pub struct CollabResumeEndEvent {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::items::FileChangeItem;
|
||||
use crate::items::ImageGenerationItem;
|
||||
use crate::items::UserMessageItem;
|
||||
use crate::items::WebSearchItem;
|
||||
@@ -4630,6 +4638,41 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn item_started_event_from_file_change_emits_patch_begin_event() {
|
||||
let event = ItemStartedEvent {
|
||||
thread_id: ThreadId::new(),
|
||||
turn_id: "turn-1".into(),
|
||||
item: TurnItem::FileChange(FileChangeItem {
|
||||
id: "patch-1".into(),
|
||||
changes: [(
|
||||
PathBuf::from("new.txt"),
|
||||
FileChange::Add {
|
||||
content: "hello".into(),
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
status: None,
|
||||
auto_approved: Some(true),
|
||||
stdout: None,
|
||||
stderr: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false);
|
||||
assert_eq!(legacy_events.len(), 1);
|
||||
match &legacy_events[0] {
|
||||
EventMsg::PatchApplyBegin(event) => {
|
||||
assert_eq!(event.call_id, "patch-1");
|
||||
assert_eq!(event.turn_id, "turn-1");
|
||||
assert!(event.auto_approved);
|
||||
assert!(event.changes.contains_key(&PathBuf::from("new.txt")));
|
||||
}
|
||||
_ => panic!("expected PatchApplyBegin event"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn item_completed_event_from_image_generation_emits_end_event() {
|
||||
let event = ItemCompletedEvent {
|
||||
@@ -4661,6 +4704,43 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn item_completed_event_from_file_change_emits_patch_end_event() {
|
||||
let event = ItemCompletedEvent {
|
||||
thread_id: ThreadId::new(),
|
||||
turn_id: "turn-1".into(),
|
||||
item: TurnItem::FileChange(FileChangeItem {
|
||||
id: "patch-1".into(),
|
||||
changes: [(
|
||||
PathBuf::from("new.txt"),
|
||||
FileChange::Add {
|
||||
content: "hello".into(),
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
status: Some(PatchApplyStatus::Completed),
|
||||
auto_approved: None,
|
||||
stdout: Some("Done!".into()),
|
||||
stderr: Some(String::new()),
|
||||
}),
|
||||
};
|
||||
|
||||
let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false);
|
||||
assert_eq!(legacy_events.len(), 1);
|
||||
match &legacy_events[0] {
|
||||
EventMsg::PatchApplyEnd(event) => {
|
||||
assert_eq!(event.call_id, "patch-1");
|
||||
assert_eq!(event.turn_id, "turn-1");
|
||||
assert_eq!(event.stdout, "Done!");
|
||||
assert!(event.success);
|
||||
assert_eq!(event.status, PatchApplyStatus::Completed);
|
||||
assert!(event.changes.contains_key(&PathBuf::from("new.txt")));
|
||||
}
|
||||
_ => panic!("expected PatchApplyEnd event"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rollback_failed_error_does_not_affect_turn_status() {
|
||||
let event = ErrorEvent {
|
||||
|
||||
Reference in New Issue
Block a user