Compare commits

...

1 Commits

Author SHA1 Message Date
pakrym-oai
c6d25e9ee5 Migrate file changes to turn items 2026-04-30 16:47:28 -07:00
14 changed files with 216 additions and 219 deletions

View File

@@ -1,7 +1,6 @@
use crate::protocol::common::ServerNotification;
use crate::protocol::item_builders::build_command_execution_begin_item;
use crate::protocol::item_builders::build_command_execution_end_item;
use crate::protocol::item_builders::build_file_change_begin_item;
use crate::protocol::item_builders::convert_patch_changes;
use crate::protocol::v2::AgentMessageDeltaNotification;
use crate::protocol::v2::CollabAgentState;
@@ -450,13 +449,6 @@ pub fn item_event_to_server_notification(
item: item_completed_event.item.into(),
})
}
EventMsg::PatchApplyBegin(patch_begin_event) => {
ServerNotification::ItemStarted(ItemStartedNotification {
thread_id,
turn_id,
item: build_file_change_begin_item(&patch_begin_event),
})
}
EventMsg::PatchApplyUpdated(event) => {
ServerNotification::FileChangePatchUpdated(FileChangePatchUpdatedNotification {
thread_id,

View File

@@ -356,6 +356,7 @@ impl ThreadHistoryBuilder {
| codex_protocol::items::TurnItem::HookPrompt(_)
| codex_protocol::items::TurnItem::AgentMessage(_)
| codex_protocol::items::TurnItem::Reasoning(_)
| codex_protocol::items::TurnItem::FileChange(_)
| codex_protocol::items::TurnItem::WebSearch(_)
| codex_protocol::items::TurnItem::ImageGeneration(_)
| codex_protocol::items::TurnItem::ContextCompaction(_) => {}
@@ -377,6 +378,7 @@ impl ThreadHistoryBuilder {
| codex_protocol::items::TurnItem::HookPrompt(_)
| codex_protocol::items::TurnItem::AgentMessage(_)
| codex_protocol::items::TurnItem::Reasoning(_)
| codex_protocol::items::TurnItem::FileChange(_)
| codex_protocol::items::TurnItem::WebSearch(_)
| codex_protocol::items::TurnItem::ImageGeneration(_)
| codex_protocol::items::TurnItem::ContextCompaction(_) => {}

View File

@@ -30,6 +30,7 @@ use codex_protocol::config_types::Verbosity;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::config_types::WebSearchToolConfig;
use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent;
use codex_protocol::items::FileChangeStatus as CoreFileChangeStatus;
use codex_protocol::items::TurnItem as CoreTurnItem;
use codex_protocol::mcp::CallToolResult as CoreMcpCallToolResult;
use codex_protocol::mcp::Resource as McpResource;
@@ -6412,6 +6413,13 @@ impl From<CoreTurnItem> for ThreadItem {
summary: reasoning.summary_text,
content: reasoning.raw_content,
},
CoreTurnItem::FileChange(file_change) => ThreadItem::FileChange {
id: file_change.id,
changes: crate::protocol::item_builders::convert_patch_changes(
&file_change.changes,
),
status: PatchApplyStatus::from(file_change.status),
},
CoreTurnItem::WebSearch(search) => ThreadItem::WebSearch {
id: search.id,
query: search.query,
@@ -6533,6 +6541,17 @@ impl From<&CorePatchApplyStatus> for PatchApplyStatus {
}
}
impl From<CoreFileChangeStatus> for PatchApplyStatus {
fn from(value: CoreFileChangeStatus) -> Self {
match value {
CoreFileChangeStatus::InProgress => PatchApplyStatus::InProgress,
CoreFileChangeStatus::Completed => PatchApplyStatus::Completed,
CoreFileChangeStatus::Failed => PatchApplyStatus::Failed,
CoreFileChangeStatus::Declined => PatchApplyStatus::Declined,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -8033,11 +8052,14 @@ mod tests {
use super::*;
use codex_protocol::items::AgentMessageContent;
use codex_protocol::items::AgentMessageItem;
use codex_protocol::items::FileChangeItem;
use codex_protocol::items::FileChangeStatus;
use codex_protocol::items::ReasoningItem;
use codex_protocol::items::TurnItem;
use codex_protocol::items::UserMessageItem;
use codex_protocol::items::WebSearchItem;
use codex_protocol::models::WebSearchAction as CoreWebSearchAction;
use codex_protocol::protocol::FileChange;
use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess;
use codex_protocol::user_input::UserInput as CoreUserInput;
use codex_utils_absolute_path::test_support::PathBufExt;
@@ -10293,6 +10315,48 @@ mod tests {
}
);
let file_change_item = TurnItem::FileChange(FileChangeItem {
id: "patch-1".to_string(),
changes: HashMap::from([
(
PathBuf::from("z.txt"),
FileChange::Delete {
content: "bye".to_string(),
},
),
(
PathBuf::from("a.txt"),
FileChange::Add {
content: "hello".to_string(),
},
),
]),
status: FileChangeStatus::Completed,
stdout: Some("Success.".to_string()),
stderr: Some(String::new()),
success: Some(true),
});
assert_eq!(
ThreadItem::from(file_change_item),
ThreadItem::FileChange {
id: "patch-1".to_string(),
changes: vec![
FileUpdateChange {
path: "a.txt".to_string(),
kind: PatchChangeKind::Add,
diff: "hello".to_string(),
},
FileUpdateChange {
path: "z.txt".to_string(),
kind: PatchChangeKind::Delete,
diff: "bye".to_string(),
},
],
status: PatchApplyStatus::Completed,
}
);
let search_item = TurnItem::WebSearch(WebSearchItem {
id: "search-1".to_string(),
query: "docs".to_string(),

View File

@@ -29,7 +29,6 @@ use codex_app_server_protocol::ExecPolicyAmendment as V2ExecPolicyAmendment;
use codex_app_server_protocol::FileChangeApprovalDecision;
use codex_app_server_protocol::FileChangeRequestApprovalParams;
use codex_app_server_protocol::FileChangeRequestApprovalResponse;
use codex_app_server_protocol::FileUpdateChange;
use codex_app_server_protocol::GrantedPermissionProfile as V2GrantedPermissionProfile;
use codex_app_server_protocol::GuardianWarningNotification;
use codex_app_server_protocol::HookCompletedNotification;
@@ -46,7 +45,6 @@ use codex_app_server_protocol::ModelVerificationNotification;
use codex_app_server_protocol::NetworkApprovalContext as V2NetworkApprovalContext;
use codex_app_server_protocol::NetworkPolicyAmendment as V2NetworkPolicyAmendment;
use codex_app_server_protocol::NetworkPolicyRuleAction as V2NetworkPolicyRuleAction;
use codex_app_server_protocol::PatchApplyStatus;
use codex_app_server_protocol::PermissionsRequestApprovalParams;
use codex_app_server_protocol::PermissionsRequestApprovalResponse;
use codex_app_server_protocol::RawResponseItemCompletedNotification;
@@ -82,11 +80,8 @@ use codex_app_server_protocol::TurnPlanUpdatedNotification;
use codex_app_server_protocol::TurnStartedNotification;
use codex_app_server_protocol::TurnStatus;
use codex_app_server_protocol::WarningNotification;
use codex_app_server_protocol::build_file_change_approval_request_item;
use codex_app_server_protocol::build_file_change_end_item;
use codex_app_server_protocol::build_item_from_guardian_event;
use codex_app_server_protocol::build_turns_from_rollout_items;
use codex_app_server_protocol::convert_patch_changes;
use codex_app_server_protocol::guardian_auto_approval_review_notification;
use codex_app_server_protocol::item_event_to_server_notification;
use codex_core::CodexThread;
@@ -524,29 +519,7 @@ pub(crate) async fn apply_bespoke_event_handling(
let permission_guard = thread_watch_manager
.note_permission_requested(&conversation_id.to_string())
.await;
// Until we migrate the core to be aware of a first class FileChangeItem
// and emit the corresponding EventMsg, we repurpose the call_id as the item_id.
let item_id = event.call_id.clone();
let patch_changes = convert_patch_changes(&event.changes);
let first_start = {
let mut state = thread_state.lock().await;
state
.turn_summary
.file_change_started
.insert(item_id.clone())
};
if first_start {
let item = build_file_change_approval_request_item(&event);
let notification = ItemStartedNotification {
thread_id: conversation_id.to_string(),
turn_id: event_turn_id.clone(),
item,
};
outgoing
.send_server_notification(ServerNotification::ItemStarted(notification))
.await;
}
let params = FileChangeRequestApprovalParams {
thread_id: conversation_id.to_string(),
turn_id: event.turn_id.clone(),
@@ -559,14 +532,10 @@ pub(crate) async fn apply_bespoke_event_handling(
.await;
tokio::spawn(async move {
on_file_change_request_approval_response(
event_turn_id,
conversation_id,
item_id,
patch_changes,
pending_request_id,
rx,
conversation,
outgoing,
thread_state.clone(),
permission_guard,
)
@@ -1104,41 +1073,7 @@ pub(crate) async fn apply_bespoke_event_handling(
)
.await;
}
EventMsg::PatchApplyBegin(patch_begin_event) => {
// Until we migrate the core to be aware of a first class FileChangeItem
// and emit the corresponding EventMsg, we repurpose the call_id as the item_id.
let item_id = patch_begin_event.call_id.clone();
let first_start = {
let mut state = thread_state.lock().await;
state
.turn_summary
.file_change_started
.insert(item_id.clone())
};
if first_start {
let notification = item_event_to_server_notification(
EventMsg::PatchApplyBegin(patch_begin_event),
&conversation_id.to_string(),
&event_turn_id,
);
outgoing.send_server_notification(notification).await;
}
}
EventMsg::PatchApplyEnd(patch_end_event) => {
// Until we migrate the core to be aware of a first class FileChangeItem
// and emit the corresponding EventMsg, we repurpose the call_id as the item_id.
let item_id = patch_end_event.call_id.clone();
complete_file_change_item(
conversation_id,
item_id,
build_file_change_end_item(&patch_end_event),
event_turn_id.clone(),
&outgoing,
&thread_state,
)
.await;
}
EventMsg::PatchApplyBegin(_) | EventMsg::PatchApplyEnd(_) => {}
EventMsg::ExecCommandBegin(exec_command_begin_event) => {
if matches!(
exec_command_begin_event.source,
@@ -1425,31 +1360,6 @@ async fn emit_turn_completed_with_status(
.await;
}
async fn complete_file_change_item(
conversation_id: ThreadId,
item_id: String,
item: ThreadItem,
turn_id: String,
outgoing: &ThreadScopedOutgoingMessageSender,
thread_state: &Arc<Mutex<ThreadState>>,
) {
thread_state
.lock()
.await
.turn_summary
.file_change_started
.remove(&item_id);
let notification = ItemCompletedNotification {
thread_id: conversation_id.to_string(),
turn_id,
item,
};
outgoing
.send_server_notification(ServerNotification::ItemCompleted(notification))
.await;
}
#[allow(clippy::too_many_arguments)]
async fn start_command_execution_item(
conversation_id: &ThreadId,
@@ -2002,38 +1912,27 @@ fn render_review_output_text(output: &ReviewOutputEvent) -> String {
}
}
fn map_file_change_approval_decision(
decision: FileChangeApprovalDecision,
) -> (ReviewDecision, Option<PatchApplyStatus>) {
fn map_file_change_approval_decision(decision: FileChangeApprovalDecision) -> ReviewDecision {
match decision {
FileChangeApprovalDecision::Accept => (ReviewDecision::Approved, None),
FileChangeApprovalDecision::AcceptForSession => (ReviewDecision::ApprovedForSession, None),
FileChangeApprovalDecision::Decline => {
(ReviewDecision::Denied, Some(PatchApplyStatus::Declined))
}
FileChangeApprovalDecision::Cancel => {
(ReviewDecision::Abort, Some(PatchApplyStatus::Declined))
}
FileChangeApprovalDecision::Accept => ReviewDecision::Approved,
FileChangeApprovalDecision::AcceptForSession => ReviewDecision::ApprovedForSession,
FileChangeApprovalDecision::Decline => ReviewDecision::Denied,
FileChangeApprovalDecision::Cancel => ReviewDecision::Abort,
}
}
#[allow(clippy::too_many_arguments)]
async fn on_file_change_request_approval_response(
event_turn_id: String,
conversation_id: ThreadId,
item_id: String,
changes: Vec<FileUpdateChange>,
pending_request_id: RequestId,
receiver: oneshot::Receiver<ClientRequestResult>,
codex: Arc<CodexThread>,
outgoing: ThreadScopedOutgoingMessageSender,
thread_state: Arc<Mutex<ThreadState>>,
permission_guard: ThreadWatchActiveGuard,
) {
let response = receiver.await;
resolve_server_request_on_thread_listener(&thread_state, pending_request_id).await;
drop(permission_guard);
let (decision, completion_status) = match response {
let decision = match response {
Ok(Ok(value)) => {
let response = serde_json::from_value::<FileChangeRequestApprovalResponse>(value)
.unwrap_or_else(|err| {
@@ -2043,39 +1942,19 @@ async fn on_file_change_request_approval_response(
}
});
let (decision, completion_status) =
map_file_change_approval_decision(response.decision);
// Allow EventMsg::PatchApplyEnd to emit ItemCompleted for accepted patches.
// Only short-circuit on declines/cancels/failures.
(decision, completion_status)
map_file_change_approval_decision(response.decision)
}
Ok(Err(err)) if is_turn_transition_server_request_error(&err) => return,
Ok(Err(err)) => {
error!("request failed with client error: {err:?}");
(ReviewDecision::Denied, Some(PatchApplyStatus::Failed))
ReviewDecision::Denied
}
Err(err) => {
error!("request failed: {err:?}");
(ReviewDecision::Denied, Some(PatchApplyStatus::Failed))
ReviewDecision::Denied
}
};
if let Some(status) = completion_status {
complete_file_change_item(
conversation_id,
item_id.clone(),
ThreadItem::FileChange {
id: item_id.clone(),
changes,
status,
},
event_turn_id.clone(),
&outgoing,
&thread_state,
)
.await;
}
if let Err(err) = codex
.submit(Op::PatchApproval {
id: item_id,
@@ -2891,10 +2770,9 @@ mod tests {
#[test]
fn file_change_accept_for_session_maps_to_approved_for_session() {
let (decision, completion_status) =
let decision =
map_file_change_approval_decision(FileChangeApprovalDecision::AcceptForSession);
assert_eq!(decision, ReviewDecision::ApprovedForSession);
assert_eq!(completion_status, None);
}
#[test]

View File

@@ -61,7 +61,6 @@ pub(crate) enum ThreadListenerCommand {
#[derive(Default, Clone)]
pub(crate) struct TurnSummary {
pub(crate) started_at: Option<i64>,
pub(crate) file_change_started: HashSet<String>,
pub(crate) command_execution_started: HashSet<String>,
pub(crate) last_error: Option<TurnError>,
}

View File

@@ -2197,8 +2197,8 @@ async fn turn_start_file_change_approval_v2() -> Result<()> {
)
.await?;
let mut saw_resolved = false;
let mut completed_file_change: Option<ThreadItem> = None;
while completed_file_change.is_none() {
let mut completed_file_changes = Vec::new();
loop {
let message = timeout(DEFAULT_READ_TIMEOUT, mcp.read_next_message()).await??;
let JSONRPCMessage::Notification(notification) = message else {
continue;
@@ -2221,14 +2221,21 @@ async fn turn_start_file_change_approval_v2() -> Result<()> {
)?;
if let ThreadItem::FileChange { .. } = completed.item {
assert!(saw_resolved, "serverRequest/resolved should arrive first");
completed_file_change = Some(completed.item);
completed_file_changes.push(completed.item);
}
}
"turn/completed" => break,
_ => {}
}
}
let completed_file_change =
completed_file_change.expect("file change completion should be observed");
assert_eq!(
completed_file_changes.len(),
1,
"file change should complete exactly once"
);
let completed_file_change = completed_file_changes
.pop()
.expect("file change completion should be observed");
let ThreadItem::FileChange { ref id, status, .. } = completed_file_change else {
unreachable!("loop ensures we break on file change items");
};
@@ -2238,12 +2245,6 @@ async fn turn_start_file_change_approval_v2() -> Result<()> {
let readme_contents = std::fs::read_to_string(expected_readme_path)?;
assert_eq!(readme_contents, "new line\n");
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
Ok(())
}

View File

@@ -26,7 +26,6 @@ pub(crate) enum InternalApplyPatchInvocation {
#[derive(Debug)]
pub(crate) struct ApplyPatchRuntimeInvocation {
pub(crate) action: ApplyPatchAction,
pub(crate) auto_approved: bool,
pub(crate) exec_approval_requirement: ExecApprovalRequirement,
}
@@ -43,24 +42,21 @@ pub(crate) async fn apply_patch(
&turn_context.cwd,
turn_context.windows_sandbox_level,
) {
SafetyCheck::AutoApprove {
user_explicitly_approved,
..
} => InternalApplyPatchInvocation::DelegateToRuntime(ApplyPatchRuntimeInvocation {
action,
auto_approved: !user_explicitly_approved,
exec_approval_requirement: ExecApprovalRequirement::Skip {
bypass_sandbox: false,
proposed_execpolicy_amendment: None,
},
}),
SafetyCheck::AutoApprove { .. } => {
InternalApplyPatchInvocation::DelegateToRuntime(ApplyPatchRuntimeInvocation {
action,
exec_approval_requirement: ExecApprovalRequirement::Skip {
bypass_sandbox: false,
proposed_execpolicy_amendment: None,
},
})
}
SafetyCheck::AskUser => {
// Delegate the approval prompt (including cached approvals) to the
// tool runtime, consistent with how shell/unified_exec approvals
// are orchestrator-driven.
InternalApplyPatchInvocation::DelegateToRuntime(ApplyPatchRuntimeInvocation {
action,
auto_approved: false,
exec_approval_requirement: ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: None,

View File

@@ -6,6 +6,9 @@ use crate::tools::sandboxing::ToolError;
use codex_protocol::error::CodexErr;
use codex_protocol::error::SandboxErr;
use codex_protocol::exec_output::ExecToolCallOutput;
use codex_protocol::items::FileChangeItem;
use codex_protocol::items::FileChangeStatus;
use codex_protocol::items::TurnItem;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecCommandBeginEvent;
@@ -13,8 +16,6 @@ use codex_protocol::protocol::ExecCommandEndEvent;
use codex_protocol::protocol::ExecCommandSource;
use codex_protocol::protocol::ExecCommandStatus;
use codex_protocol::protocol::FileChange;
use codex_protocol::protocol::PatchApplyBeginEvent;
use codex_protocol::protocol::PatchApplyEndEvent;
use codex_protocol::protocol::PatchApplyStatus;
use codex_protocol::protocol::TurnDiffEvent;
use codex_shell_command::parse_command::parse_command;
@@ -97,7 +98,6 @@ pub(crate) enum ToolEmitter {
},
ApplyPatch {
changes: HashMap<PathBuf, FileChange>,
auto_approved: bool,
},
UnifiedExec {
command: Vec<String>,
@@ -125,11 +125,8 @@ impl ToolEmitter {
}
}
pub fn apply_patch(changes: HashMap<PathBuf, FileChange>, auto_approved: bool) -> Self {
Self::ApplyPatch {
changes,
auto_approved,
}
pub fn apply_patch(changes: HashMap<PathBuf, FileChange>) -> Self {
Self::ApplyPatch { changes }
}
pub fn unified_exec(
@@ -171,28 +168,20 @@ impl ToolEmitter {
.await;
}
(
Self::ApplyPatch {
changes,
auto_approved,
},
ToolEventStage::Begin,
) => {
(Self::ApplyPatch { changes, .. }, ToolEventStage::Begin) => {
if let Some(tracker) = ctx.turn_diff_tracker {
let mut guard = tracker.lock().await;
guard.on_patch_begin(changes);
}
ctx.session
.send_event(
ctx.turn,
EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
call_id: ctx.call_id.to_string(),
turn_id: ctx.turn.sub_id.clone(),
auto_approved: *auto_approved,
changes: changes.clone(),
}),
)
.await;
let item = TurnItem::FileChange(FileChangeItem {
id: ctx.call_id.to_string(),
changes: changes.clone(),
status: FileChangeStatus::InProgress,
stdout: None,
stderr: None,
success: None,
});
ctx.session.emit_turn_item_started(ctx.turn, &item).await;
}
(Self::ApplyPatch { changes, .. }, ToolEventStage::Success(output)) => {
emit_patch_end(
@@ -499,20 +488,15 @@ async fn emit_patch_end(
success: bool,
status: PatchApplyStatus,
) {
ctx.session
.send_event(
ctx.turn,
EventMsg::PatchApplyEnd(PatchApplyEndEvent {
call_id: ctx.call_id.to_string(),
turn_id: ctx.turn.sub_id.clone(),
stdout,
stderr,
success,
changes,
status,
}),
)
.await;
let item = TurnItem::FileChange(FileChangeItem {
id: ctx.call_id.to_string(),
changes: changes.clone(),
status: status.clone().into(),
stdout: Some(stdout),
stderr: Some(stderr),
success: Some(success),
});
ctx.session.emit_turn_item_completed(ctx.turn, item).await;
if let Some(tracker) = ctx.turn_diff_tracker {
let unified_diff = {

View File

@@ -392,8 +392,7 @@ impl ToolHandler for ApplyPatchHandler {
}
InternalApplyPatchInvocation::DelegateToRuntime(apply) => {
let changes = convert_apply_patch_to_protocol(&apply.action);
let emitter =
ToolEmitter::apply_patch(changes.clone(), apply.auto_approved);
let emitter = ToolEmitter::apply_patch(changes.clone());
let event_ctx = ToolEventCtx::new(
session.as_ref(),
turn.as_ref(),
@@ -501,7 +500,7 @@ pub(crate) async fn intercept_apply_patch(
}
InternalApplyPatchInvocation::DelegateToRuntime(apply) => {
let changes = convert_apply_patch_to_protocol(&apply.action);
let emitter = ToolEmitter::apply_patch(changes.clone(), apply.auto_approved);
let emitter = ToolEmitter::apply_patch(changes.clone());
let event_ctx = ToolEventCtx::new(
session.as_ref(),
turn.as_ref(),

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)]
@@ -31,6 +37,7 @@ pub enum TurnItem {
AgentMessage(AgentMessageItem),
Plan(PlanItem),
Reasoning(ReasoningItem),
FileChange(FileChangeItem),
WebSearch(WebSearchItem),
ImageGeneration(ImageGenerationItem),
ContextCompaction(ContextCompactionItem),
@@ -107,6 +114,41 @@ pub struct ReasoningItem {
pub raw_content: Vec<String>,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FileChangeStatus {
InProgress,
Completed,
Failed,
Declined,
}
impl From<PatchApplyStatus> for FileChangeStatus {
fn from(value: PatchApplyStatus) -> Self {
match value {
PatchApplyStatus::Completed => FileChangeStatus::Completed,
PatchApplyStatus::Failed => FileChangeStatus::Failed,
PatchApplyStatus::Declined => FileChangeStatus::Declined,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]
pub struct FileChangeItem {
pub id: String,
pub changes: HashMap<PathBuf, FileChange>,
pub status: FileChangeStatus,
#[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>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub success: Option<bool>,
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]
pub struct WebSearchItem {
pub id: String,
@@ -389,6 +431,7 @@ impl TurnItem {
TurnItem::AgentMessage(item) => item.id.clone(),
TurnItem::Plan(item) => item.id.clone(),
TurnItem::Reasoning(item) => item.id.clone(),
TurnItem::FileChange(item) => item.id.clone(),
TurnItem::WebSearch(item) => item.id.clone(),
TurnItem::ImageGeneration(item) => item.id.clone(),
TurnItem::ContextCompaction(item) => item.id.clone(),
@@ -401,6 +444,7 @@ impl TurnItem {
TurnItem::HookPrompt(_) => Vec::new(),
TurnItem::AgentMessage(item) => item.as_legacy_events(),
TurnItem::Plan(_) => Vec::new(),
TurnItem::FileChange(_) => Vec::new(),
TurnItem::WebSearch(item) => vec![item.as_legacy_event()],
TurnItem::ImageGeneration(item) => vec![item.as_legacy_event()],
TurnItem::Reasoning(item) => item.as_legacy_events(show_raw_agent_reasoning),
@@ -409,6 +453,41 @@ impl TurnItem {
}
}
impl FileChangeItem {
pub fn as_legacy_begin_event(&self, turn_id: String) -> Option<EventMsg> {
if self.status != FileChangeStatus::InProgress {
return None;
}
Some(EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
call_id: self.id.clone(),
turn_id,
auto_approved: false,
changes: self.changes.clone(),
}))
}
pub fn as_legacy_end_event(&self, turn_id: String) -> Option<EventMsg> {
let status = match self.status {
FileChangeStatus::InProgress => return None,
FileChangeStatus::Completed => PatchApplyStatus::Completed,
FileChangeStatus::Failed => PatchApplyStatus::Failed,
FileChangeStatus::Declined => PatchApplyStatus::Declined,
};
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: self
.success
.unwrap_or(matches!(self.status, FileChangeStatus::Completed)),
changes: self.changes.clone(),
status,
}))
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -1862,6 +1862,10 @@ impl HasLegacyEvent for ItemStartedEvent {
call_id: item.id.clone(),
})]
}
TurnItem::FileChange(item) => item
.as_legacy_begin_event(self.turn_id.clone())
.into_iter()
.collect(),
_ => Vec::new(),
}
}
@@ -1880,7 +1884,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),
}
}
}

View File

@@ -18,7 +18,6 @@ use codex_protocol::protocol::ExecCommandSource;
use codex_protocol::protocol::ExecCommandStatus;
use codex_protocol::protocol::McpToolCallBeginEvent;
use codex_protocol::protocol::McpToolCallEndEvent;
use codex_protocol::protocol::PatchApplyBeginEvent;
use codex_protocol::protocol::PatchApplyEndEvent;
use codex_protocol::protocol::PatchApplyStatus;
use codex_protocol::protocol::TurnAbortReason;
@@ -98,7 +97,6 @@ pub(crate) enum ToolRuntimeTraceEvent<'a> {
pub(crate) enum ToolRuntimePayload<'a> {
ExecCommandBegin(&'a ExecCommandBeginEvent),
ExecCommandEnd(&'a ExecCommandEndEvent),
PatchApplyBegin(&'a PatchApplyBeginEvent),
PatchApplyEnd(&'a PatchApplyEndEvent),
McpToolCallBegin(&'a McpToolCallBeginEvent),
McpToolCallEnd(&'a McpToolCallEndEvent),
@@ -120,7 +118,6 @@ impl Serialize for ToolRuntimePayload<'_> {
match self {
ToolRuntimePayload::ExecCommandBegin(event) => event.serialize(serializer),
ToolRuntimePayload::ExecCommandEnd(event) => event.serialize(serializer),
ToolRuntimePayload::PatchApplyBegin(event) => event.serialize(serializer),
ToolRuntimePayload::PatchApplyEnd(event) => event.serialize(serializer),
ToolRuntimePayload::McpToolCallBegin(event) => event.serialize(serializer),
ToolRuntimePayload::McpToolCallEnd(event) => event.serialize(serializer),
@@ -151,10 +148,6 @@ pub(crate) fn tool_runtime_trace_event(event: &EventMsg) -> Option<ToolRuntimeTr
payload: ToolRuntimePayload::ExecCommandEnd(event),
})
}
EventMsg::PatchApplyBegin(event) => Some(ToolRuntimeTraceEvent::Started {
tool_call_id: &event.call_id,
payload: ToolRuntimePayload::PatchApplyBegin(event),
}),
EventMsg::PatchApplyEnd(event) => Some(ToolRuntimeTraceEvent::Ended {
tool_call_id: &event.call_id,
status: event.status.trace_execution_status(),
@@ -264,6 +257,7 @@ pub(crate) fn tool_runtime_trace_event(event: &EventMsg) -> Option<ToolRuntimeTr
| EventMsg::UndoStarted(_)
| EventMsg::UndoCompleted(_)
| EventMsg::StreamError(_)
| EventMsg::PatchApplyBegin(_)
| EventMsg::PatchApplyUpdated(_)
| EventMsg::TurnDiff(_)
| EventMsg::GetHistoryEntryResponse(_)

View File

@@ -94,6 +94,7 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option<EventPersistenceMode> {
| EventMsg::AgentMessage(_)
| EventMsg::AgentReasoning(_)
| EventMsg::AgentReasoningRawContent(_)
| EventMsg::PatchApplyBegin(_)
| EventMsg::PatchApplyEnd(_)
| EventMsg::TokenCount(_)
| EventMsg::ThreadNameUpdated(_)
@@ -156,7 +157,6 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option<EventPersistenceMode> {
| EventMsg::ApplyPatchApprovalRequest(_)
| EventMsg::BackgroundEvent(_)
| EventMsg::StreamError(_)
| EventMsg::PatchApplyBegin(_)
| EventMsg::PatchApplyUpdated(_)
| EventMsg::TurnDiff(_)
| EventMsg::GetHistoryEntryResponse(_)

View File

@@ -309,7 +309,6 @@ async fn run_turn(thread: &CodexThread, thread_id: &str, prompt: String) -> anyh
| EventMsg::AgentReasoningSectionBreak(_)
| EventMsg::ItemStarted(_)
| EventMsg::ItemCompleted(_)
| EventMsg::PatchApplyBegin(_)
| EventMsg::PatchApplyUpdated(_)
| EventMsg::TerminalInteraction(_)
| EventMsg::ExecCommandBegin(_)