diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index f1d8392135..e35a54ed3e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1126,6 +1126,10 @@ pub enum ThreadItem { id: String, changes: Vec, status: PatchApplyStatus, + /// Captured stdout (summary printed by the underlying `apply_patch` tool call). + stdout: Option, + /// Captured stderr (parser errors, IO failures, etc.). + stderr: Option, }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index df4cdb8980..64062d30ab 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -136,6 +136,8 @@ pub(crate) async fn apply_bespoke_event_handling( id: item_id.clone(), changes: patch_changes.clone(), status: PatchApplyStatus::InProgress, + stdout: None, + stderr: None, }; let notification = ItemStartedNotification { thread_id: conversation_id.to_string(), @@ -462,6 +464,8 @@ pub(crate) async fn apply_bespoke_event_handling( id: item_id.clone(), changes: convert_patch_changes(&patch_begin_event.changes), status: PatchApplyStatus::InProgress, + stdout: None, + stderr: None, }; let notification = ItemStartedNotification { thread_id: conversation_id.to_string(), @@ -489,6 +493,8 @@ pub(crate) async fn apply_bespoke_event_handling( item_id, changes, status, + Some(patch_end_event.stdout), + Some(patch_end_event.stderr), event_turn_id.clone(), outgoing.as_ref(), &turn_summary_store, @@ -738,11 +744,14 @@ async fn emit_turn_completed_with_status( .await; } +#[allow(clippy::too_many_arguments)] async fn complete_file_change_item( conversation_id: ConversationId, item_id: String, changes: Vec, status: PatchApplyStatus, + stdout: Option, + stderr: Option, turn_id: String, outgoing: &OutgoingMessageSender, turn_summary_store: &TurnSummaryStore, @@ -758,6 +767,8 @@ async fn complete_file_change_item( id: item_id, changes, status, + stdout, + stderr, }; let notification = ItemCompletedNotification { thread_id: conversation_id.to_string(), @@ -1068,6 +1079,8 @@ async fn on_file_change_request_approval_response( item_id, changes, status, + None, + None, event_turn_id.clone(), outgoing.as_ref(), &turn_summary_store, diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index e4cd722947..f91751b774 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -688,12 +688,16 @@ async fn turn_start_file_change_approval_v2() -> Result<()> { ref id, status, ref changes, + stdout, + stderr, } = started_file_change else { unreachable!("loop ensures we break on file change items"); }; assert_eq!(id, "patch-call"); assert_eq!(status, PatchApplyStatus::InProgress); + assert!(stdout.is_none()); + assert!(stderr.is_none()); let started_changes = changes.clone(); let server_req = timeout( @@ -763,11 +767,20 @@ async fn turn_start_file_change_approval_v2() -> Result<()> { } }) .await??; - let ThreadItem::FileChange { ref id, status, .. } = completed_file_change else { + let ThreadItem::FileChange { + ref id, + status, + stdout, + stderr, + .. + } = completed_file_change + else { unreachable!("loop ensures we break on file change items"); }; assert_eq!(id, "patch-call"); assert_eq!(status, PatchApplyStatus::Completed); + assert!(stdout.is_some()); + assert!(stderr.is_some()); timeout( DEFAULT_READ_TIMEOUT, @@ -854,12 +867,16 @@ async fn turn_start_file_change_approval_decline_v2() -> Result<()> { ref id, status, ref changes, + stdout, + stderr, } = started_file_change else { unreachable!("loop ensures we break on file change items"); }; assert_eq!(id, "patch-call"); assert_eq!(status, PatchApplyStatus::InProgress); + assert!(stdout.is_none()); + assert!(stderr.is_none()); let started_changes = changes.clone(); let server_req = timeout( @@ -909,11 +926,20 @@ async fn turn_start_file_change_approval_decline_v2() -> Result<()> { } }) .await??; - let ThreadItem::FileChange { ref id, status, .. } = completed_file_change else { + let ThreadItem::FileChange { + ref id, + status, + stdout, + stderr, + .. + } = completed_file_change + else { unreachable!("loop ensures we break on file change items"); }; assert_eq!(id, "patch-call"); assert_eq!(status, PatchApplyStatus::Declined); + assert!(stdout.is_none()); + assert!(stderr.is_none()); timeout( DEFAULT_READ_TIMEOUT,