mirror of
https://github.com/openai/codex.git
synced 2026-06-02 19:31:59 +00:00
Compare commits
1 Commits
fcoury/vim
...
pakrym/mig
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6d25e9ee5 |
@@ -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,
|
||||
|
||||
@@ -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(_) => {}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(_)
|
||||
|
||||
@@ -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(_)
|
||||
|
||||
@@ -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(_)
|
||||
|
||||
Reference in New Issue
Block a user