mirror of
https://github.com/openai/codex.git
synced 2026-11-02 19:43:24 +00:00
This PR adds the API V2 version of the apply_patch approval flow, which centers around `ThreadItem::FileChange`. This PR wires the new RPC (`item/fileChange/requestApproval`, V2 only) and related events (`item/started`, `item/completed` for `ThreadItem::FileChange`, which are emitted in both V1 and V2) through the app-server protocol. The new approval RPC is only sent when the user initiates a turn with the new `turn/start` API so we don't break backwards compatibility with VSCE. Similar to https://github.com/openai/codex/pull/6758, the approach I took was to make as few changes to the Codex core as possible, leveraging existing `EventMsg` core events, and translating those in app-server. I did have to add a few additional fields to `EventMsg::PatchApplyBegin` and `EventMsg::PatchApplyEnd`, but those were fairly lightweight. However, the `EventMsg`s emitted by core are the following: ``` 1) Auto-approved (no request for approval) - EventMsg::PatchApplyBegin - EventMsg::PatchApplyEnd 2) Approved by user - EventMsg::ApplyPatchApprovalRequest - EventMsg::PatchApplyBegin - EventMsg::PatchApplyEnd 3) Declined by user - EventMsg::ApplyPatchApprovalRequest - EventMsg::PatchApplyBegin - EventMsg::PatchApplyEnd ``` For a request triggering an approval, this would result in: ``` item/fileChange/requestApproval item/started item/completed ``` which is different from the `ThreadItem::CommandExecution` flow introduced in https://github.com/openai/codex/pull/6758, which does the below and is preferable: ``` item/started item/commandExecution/requestApproval item/completed ``` To fix this, we leverage `TurnSummaryStore` on codex_message_processor to store a little bit of state, allowing us to fire `item/started` and `item/fileChange/requestApproval` whenever we receive the underlying `EventMsg::ApplyPatchApprovalRequest`, and no-oping when we receive the `EventMsg::PatchApplyBegin` later. This is much less invasive than modifying the order of EventMsg within core (I tried). The resulting payloads: ``` { "method": "item/started", "params": { "item": { "changes": [ { "diff": "Hello from Codex!\n", "kind": "add", "path": "/Users/owen/repos/codex/codex-rs/APPROVAL_DEMO.txt" } ], "id": "call_Nxnwj7B3YXigfV6Mwh03d686", "status": "inProgress", "type": "fileChange" } } } ``` ``` { "id": 0, "method": "item/fileChange/requestApproval", "params": { "grantRoot": null, "itemId": "call_Nxnwj7B3YXigfV6Mwh03d686", "reason": null, "threadId": "019a9e11-8295-7883-a283-779e06502c6f", "turnId": "1" } } ``` ``` { "id": 0, "result": { "decision": "accept" } } ``` ``` { "method": "item/completed", "params": { "item": { "changes": [ { "diff": "Hello from Codex!\n", "kind": "add", "path": "/Users/owen/repos/codex/codex-rs/APPROVAL_DEMO.txt" } ], "id": "call_Nxnwj7B3YXigfV6Mwh03d686", "status": "completed", "type": "fileChange" } } } ```
248 lines
7.9 KiB
Rust
248 lines
7.9 KiB
Rust
use mcp_types::ContentBlock as McpContentBlock;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use serde_json::Value as JsonValue;
|
|
use ts_rs::TS;
|
|
|
|
/// Top-level JSONL events emitted by codex exec
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
#[serde(tag = "type")]
|
|
pub enum ThreadEvent {
|
|
/// Emitted when a new thread is started as the first event.
|
|
#[serde(rename = "thread.started")]
|
|
ThreadStarted(ThreadStartedEvent),
|
|
/// Emitted when a turn is started by sending a new prompt to the model.
|
|
/// A turn encompasses all events that happen while agent is processing the prompt.
|
|
#[serde(rename = "turn.started")]
|
|
TurnStarted(TurnStartedEvent),
|
|
/// Emitted when a turn is completed. Typically right after the assistant's response.
|
|
#[serde(rename = "turn.completed")]
|
|
TurnCompleted(TurnCompletedEvent),
|
|
/// Indicates that a turn failed with an error.
|
|
#[serde(rename = "turn.failed")]
|
|
TurnFailed(TurnFailedEvent),
|
|
/// Emitted when a new item is added to the thread. Typically the item will be in an "in progress" state.
|
|
#[serde(rename = "item.started")]
|
|
ItemStarted(ItemStartedEvent),
|
|
/// Emitted when an item is updated.
|
|
#[serde(rename = "item.updated")]
|
|
ItemUpdated(ItemUpdatedEvent),
|
|
/// Signals that an item has reached a terminal state—either success or failure.
|
|
#[serde(rename = "item.completed")]
|
|
ItemCompleted(ItemCompletedEvent),
|
|
/// Represents an unrecoverable error emitted directly by the event stream.
|
|
#[serde(rename = "error")]
|
|
Error(ThreadErrorEvent),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
pub struct ThreadStartedEvent {
|
|
/// The identified of the new thread. Can be used to resume the thread later.
|
|
pub thread_id: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, Default)]
|
|
|
|
pub struct TurnStartedEvent {}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
pub struct TurnCompletedEvent {
|
|
pub usage: Usage,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
pub struct TurnFailedEvent {
|
|
pub error: ThreadErrorEvent,
|
|
}
|
|
|
|
/// Describes the usage of tokens during a turn.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, Default)]
|
|
pub struct Usage {
|
|
/// The number of input tokens used during the turn.
|
|
pub input_tokens: i64,
|
|
/// The number of cached input tokens used during the turn.
|
|
pub cached_input_tokens: i64,
|
|
/// The number of output tokens used during the turn.
|
|
pub output_tokens: i64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
pub struct ItemStartedEvent {
|
|
pub item: ThreadItem,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
pub struct ItemCompletedEvent {
|
|
pub item: ThreadItem,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
pub struct ItemUpdatedEvent {
|
|
pub item: ThreadItem,
|
|
}
|
|
|
|
/// Fatal error emitted by the stream.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
pub struct ThreadErrorEvent {
|
|
pub message: String,
|
|
}
|
|
|
|
/// Canonical representation of a thread item and its domain-specific payload.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
pub struct ThreadItem {
|
|
pub id: String,
|
|
#[serde(flatten)]
|
|
pub details: ThreadItemDetails,
|
|
}
|
|
|
|
/// Typed payloads for each supported thread item type.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
pub enum ThreadItemDetails {
|
|
/// Response from the agent.
|
|
/// Either a natural-language response or a JSON string when structured output is requested.
|
|
AgentMessage(AgentMessageItem),
|
|
/// Agent's reasoning summary.
|
|
Reasoning(ReasoningItem),
|
|
/// Tracks a command executed by the agent. The item starts when the command is
|
|
/// spawned, and completes when the process exits with an exit code.
|
|
CommandExecution(CommandExecutionItem),
|
|
/// Represents a set of file changes by the agent. The item is emitted only as a
|
|
/// completed event once the patch succeeds or fails.
|
|
FileChange(FileChangeItem),
|
|
/// Represents a call to an MCP tool. The item starts when the invocation is
|
|
/// dispatched and completes when the MCP server reports success or failure.
|
|
McpToolCall(McpToolCallItem),
|
|
/// Captures a web search request. It starts when the search is kicked off
|
|
/// and completes when results are returned to the agent.
|
|
WebSearch(WebSearchItem),
|
|
/// Tracks the agent's running to-do list. It starts when the plan is first
|
|
/// issued, updates as steps change state, and completes when the turn ends.
|
|
TodoList(TodoListItem),
|
|
/// Describes a non-fatal error surfaced as an item.
|
|
Error(ErrorItem),
|
|
}
|
|
|
|
/// Response from the agent.
|
|
/// Either a natural-language response or a JSON string when structured output is requested.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
pub struct AgentMessageItem {
|
|
pub text: String,
|
|
}
|
|
|
|
/// Agent's reasoning summary.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
pub struct ReasoningItem {
|
|
pub text: String,
|
|
}
|
|
|
|
/// The status of a command execution.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum CommandExecutionStatus {
|
|
#[default]
|
|
InProgress,
|
|
Completed,
|
|
Failed,
|
|
}
|
|
|
|
/// A command executed by the agent.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
pub struct CommandExecutionItem {
|
|
pub command: String,
|
|
pub aggregated_output: String,
|
|
pub exit_code: Option<i32>,
|
|
pub status: CommandExecutionStatus,
|
|
}
|
|
|
|
/// A set of file changes by the agent.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
pub struct FileUpdateChange {
|
|
pub path: String,
|
|
pub kind: PatchChangeKind,
|
|
}
|
|
|
|
/// The status of a file change.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum PatchApplyStatus {
|
|
InProgress,
|
|
Completed,
|
|
Failed,
|
|
}
|
|
|
|
/// A set of file changes by the agent.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
pub struct FileChangeItem {
|
|
pub changes: Vec<FileUpdateChange>,
|
|
pub status: PatchApplyStatus,
|
|
}
|
|
|
|
/// Indicates the type of the file change.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum PatchChangeKind {
|
|
Add,
|
|
Delete,
|
|
Update,
|
|
}
|
|
|
|
/// The status of an MCP tool call.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum McpToolCallStatus {
|
|
#[default]
|
|
InProgress,
|
|
Completed,
|
|
Failed,
|
|
}
|
|
|
|
/// Result payload produced by an MCP tool invocation.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
pub struct McpToolCallItemResult {
|
|
pub content: Vec<McpContentBlock>,
|
|
pub structured_content: Option<JsonValue>,
|
|
}
|
|
|
|
/// Error details reported by a failed MCP tool invocation.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
pub struct McpToolCallItemError {
|
|
pub message: String,
|
|
}
|
|
|
|
/// A call to an MCP tool.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
pub struct McpToolCallItem {
|
|
pub server: String,
|
|
pub tool: String,
|
|
#[serde(default)]
|
|
pub arguments: JsonValue,
|
|
pub result: Option<McpToolCallItemResult>,
|
|
pub error: Option<McpToolCallItemError>,
|
|
pub status: McpToolCallStatus,
|
|
}
|
|
|
|
/// A web search request.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
pub struct WebSearchItem {
|
|
pub query: String,
|
|
}
|
|
|
|
/// An error notification.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
pub struct ErrorItem {
|
|
pub message: String,
|
|
}
|
|
|
|
/// An item in agent's to-do list.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
pub struct TodoItem {
|
|
pub text: String,
|
|
pub completed: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
|
pub struct TodoListItem {
|
|
pub items: Vec<TodoItem>,
|
|
}
|