codex_protocol/
protocol.rs

1//! Defines the protocol for a Codex session between a client and an agent.
2//!
3//! Uses a SQ (Submission Queue) / EQ (Event Queue) pattern to asynchronously communicate
4//! between user and agent.
5
6use std::collections::HashMap;
7use std::fmt;
8use std::path::Path;
9use std::path::PathBuf;
10use std::str::FromStr;
11use std::time::Duration;
12
13use crate::ConversationId;
14use crate::config_types::ReasoningEffort as ReasoningEffortConfig;
15use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;
16use crate::custom_prompts::CustomPrompt;
17use crate::items::TurnItem;
18use crate::message_history::HistoryEntry;
19use crate::models::ContentItem;
20use crate::models::ResponseItem;
21use crate::num_format::format_with_separators;
22use crate::parse_command::ParsedCommand;
23use crate::plan_tool::UpdatePlanArgs;
24use crate::user_input::UserInput;
25use mcp_types::CallToolResult;
26use mcp_types::Resource as McpResource;
27use mcp_types::ResourceTemplate as McpResourceTemplate;
28use mcp_types::Tool as McpTool;
29use schemars::JsonSchema;
30use serde::Deserialize;
31use serde::Serialize;
32use serde_json::Value;
33use serde_with::serde_as;
34use strum_macros::Display;
35use ts_rs::TS;
36
37pub use crate::approvals::ApplyPatchApprovalRequestEvent;
38pub use crate::approvals::ExecApprovalRequestEvent;
39pub use crate::approvals::SandboxCommandAssessment;
40pub use crate::approvals::SandboxRiskLevel;
41
42/// Open/close tags for special user-input blocks. Used across crates to avoid
43/// duplicated hardcoded strings.
44pub const USER_INSTRUCTIONS_OPEN_TAG: &str = "<user_instructions>";
45pub const USER_INSTRUCTIONS_CLOSE_TAG: &str = "</user_instructions>";
46pub const ENVIRONMENT_CONTEXT_OPEN_TAG: &str = "<environment_context>";
47pub const ENVIRONMENT_CONTEXT_CLOSE_TAG: &str = "</environment_context>";
48pub const USER_MESSAGE_BEGIN: &str = "## My request for Codex:";
49
50/// Submission Queue Entry - requests from user
51#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
52pub struct Submission {
53    /// Unique id for this Submission to correlate with Events
54    pub id: String,
55    /// Payload
56    pub op: Op,
57}
58
59/// Submission operation
60#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema)]
61#[serde(tag = "type", rename_all = "snake_case")]
62#[allow(clippy::large_enum_variant)]
63#[non_exhaustive]
64pub enum Op {
65    /// Abort current task.
66    /// This server sends [`EventMsg::TurnAborted`] in response.
67    Interrupt,
68
69    /// Input from the user
70    UserInput {
71        /// User input items, see `InputItem`
72        items: Vec<UserInput>,
73    },
74
75    /// Similar to [`Op::UserInput`], but contains additional context required
76    /// for a turn of a [`crate::codex_conversation::CodexConversation`].
77    UserTurn {
78        /// User input items, see `InputItem`
79        items: Vec<UserInput>,
80
81        /// `cwd` to use with the [`SandboxPolicy`] and potentially tool calls
82        /// such as `local_shell`.
83        cwd: PathBuf,
84
85        /// Policy to use for command approval.
86        approval_policy: AskForApproval,
87
88        /// Policy to use for tool calls such as `local_shell`.
89        sandbox_policy: SandboxPolicy,
90
91        /// Must be a valid model slug for the [`crate::client::ModelClient`]
92        /// associated with this conversation.
93        model: String,
94
95        /// Will only be honored if the model is configured to use reasoning.
96        #[serde(skip_serializing_if = "Option::is_none")]
97        effort: Option<ReasoningEffortConfig>,
98
99        /// Will only be honored if the model is configured to use reasoning.
100        summary: ReasoningSummaryConfig,
101        // The JSON schema to use for the final assistant message
102        final_output_json_schema: Option<Value>,
103    },
104
105    /// Override parts of the persistent turn context for subsequent turns.
106    ///
107    /// All fields are optional; when omitted, the existing value is preserved.
108    /// This does not enqueue any input – it only updates defaults used for
109    /// future `UserInput` turns.
110    OverrideTurnContext {
111        /// Updated `cwd` for sandbox/tool calls.
112        #[serde(skip_serializing_if = "Option::is_none")]
113        cwd: Option<PathBuf>,
114
115        /// Updated command approval policy.
116        #[serde(skip_serializing_if = "Option::is_none")]
117        approval_policy: Option<AskForApproval>,
118
119        /// Updated sandbox policy for tool calls.
120        #[serde(skip_serializing_if = "Option::is_none")]
121        sandbox_policy: Option<SandboxPolicy>,
122
123        /// Updated model slug. When set, the model family is derived
124        /// automatically.
125        #[serde(skip_serializing_if = "Option::is_none")]
126        model: Option<String>,
127
128        /// Updated reasoning effort (honored only for reasoning-capable models).
129        ///
130        /// Use `Some(Some(_))` to set a specific effort, `Some(None)` to clear
131        /// the effort, or `None` to leave the existing value unchanged.
132        #[serde(skip_serializing_if = "Option::is_none")]
133        effort: Option<Option<ReasoningEffortConfig>>,
134
135        /// Updated reasoning summary preference (honored only for reasoning-capable models).
136        #[serde(skip_serializing_if = "Option::is_none")]
137        summary: Option<ReasoningSummaryConfig>,
138    },
139
140    /// Approve a command execution
141    ExecApproval {
142        /// The id of the submission we are approving
143        id: String,
144        /// The user's decision in response to the request.
145        decision: ReviewDecision,
146    },
147
148    /// Approve a code patch
149    PatchApproval {
150        /// The id of the submission we are approving
151        id: String,
152        /// The user's decision in response to the request.
153        decision: ReviewDecision,
154    },
155
156    /// Append an entry to the persistent cross-session message history.
157    ///
158    /// Note the entry is not guaranteed to be logged if the user has
159    /// history disabled, it matches the list of "sensitive" patterns, etc.
160    AddToHistory {
161        /// The message text to be stored.
162        text: String,
163    },
164
165    /// Request a single history entry identified by `log_id` + `offset`.
166    GetHistoryEntryRequest { offset: usize, log_id: u64 },
167
168    /// Request the list of MCP tools available across all configured servers.
169    /// Reply is delivered via `EventMsg::McpListToolsResponse`.
170    ListMcpTools,
171
172    /// Request the list of available custom prompts.
173    ListCustomPrompts,
174
175    /// Request the agent to summarize the current conversation context.
176    /// The agent will use its existing context (either conversation history or previous response id)
177    /// to generate a summary which will be returned as an AgentMessage event.
178    Compact,
179
180    /// Request Codex to undo a turn (turn are stacked so it is the same effect as CMD + Z).
181    Undo,
182
183    /// Request a code review from the agent.
184    Review { review_request: ReviewRequest },
185
186    /// Request to shut down codex instance.
187    Shutdown,
188
189    /// Execute a user-initiated one-off shell command (triggered by "!cmd").
190    ///
191    /// The command string is executed using the user's default shell and may
192    /// include shell syntax (pipes, redirects, etc.). Output is streamed via
193    /// `ExecCommand*` events and the UI regains control upon `TaskComplete`.
194    RunUserShellCommand {
195        /// The raw command string after '!'
196        command: String,
197    },
198}
199
200/// Determines the conditions under which the user is consulted to approve
201/// running the command proposed by Codex.
202#[derive(
203    Debug,
204    Clone,
205    Copy,
206    Default,
207    PartialEq,
208    Eq,
209    Hash,
210    Serialize,
211    Deserialize,
212    Display,
213    JsonSchema,
214    TS,
215)]
216#[serde(rename_all = "kebab-case")]
217#[strum(serialize_all = "kebab-case")]
218pub enum AskForApproval {
219    /// Under this policy, only "known safe" commands—as determined by
220    /// `is_safe_command()`—that **only read files** are auto‑approved.
221    /// Everything else will ask the user to approve.
222    #[serde(rename = "untrusted")]
223    #[strum(serialize = "untrusted")]
224    UnlessTrusted,
225
226    /// *All* commands are auto‑approved, but they are expected to run inside a
227    /// sandbox where network access is disabled and writes are confined to a
228    /// specific set of paths. If the command fails, it will be escalated to
229    /// the user to approve execution without a sandbox.
230    OnFailure,
231
232    /// The model decides when to ask the user for approval.
233    #[default]
234    OnRequest,
235
236    /// Never ask the user to approve commands. Failures are immediately returned
237    /// to the model, and never escalated to the user for approval.
238    Never,
239}
240
241/// Determines execution restrictions for model shell commands.
242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, JsonSchema, TS)]
243#[strum(serialize_all = "kebab-case")]
244#[serde(tag = "type", rename_all = "kebab-case")]
245pub enum SandboxPolicy {
246    /// No restrictions whatsoever. Use with caution.
247    #[serde(rename = "danger-full-access")]
248    DangerFullAccess,
249
250    /// Read-only access to the entire file-system.
251    #[serde(rename = "read-only")]
252    ReadOnly,
253
254    /// Same as `ReadOnly` but additionally grants write access to the current
255    /// working directory ("workspace").
256    #[serde(rename = "workspace-write")]
257    WorkspaceWrite {
258        /// Additional folders (beyond cwd and possibly TMPDIR) that should be
259        /// writable from within the sandbox.
260        #[serde(default, skip_serializing_if = "Vec::is_empty")]
261        writable_roots: Vec<PathBuf>,
262
263        /// When set to `true`, outbound network access is allowed. `false` by
264        /// default.
265        #[serde(default)]
266        network_access: bool,
267
268        /// When set to `true`, will NOT include the per-user `TMPDIR`
269        /// environment variable among the default writable roots. Defaults to
270        /// `false`.
271        #[serde(default)]
272        exclude_tmpdir_env_var: bool,
273
274        /// When set to `true`, will NOT include the `/tmp` among the default
275        /// writable roots on UNIX. Defaults to `false`.
276        #[serde(default)]
277        exclude_slash_tmp: bool,
278    },
279}
280
281/// A writable root path accompanied by a list of subpaths that should remain
282/// read‑only even when the root is writable. This is primarily used to ensure
283/// top‑level VCS metadata directories (e.g. `.git`) under a writable root are
284/// not modified by the agent.
285#[derive(Debug, Clone, PartialEq, Eq, JsonSchema)]
286pub struct WritableRoot {
287    /// Absolute path, by construction.
288    pub root: PathBuf,
289
290    /// Also absolute paths, by construction.
291    pub read_only_subpaths: Vec<PathBuf>,
292}
293
294impl WritableRoot {
295    pub fn is_path_writable(&self, path: &Path) -> bool {
296        // Check if the path is under the root.
297        if !path.starts_with(&self.root) {
298            return false;
299        }
300
301        // Check if the path is under any of the read-only subpaths.
302        for subpath in &self.read_only_subpaths {
303            if path.starts_with(subpath) {
304                return false;
305            }
306        }
307
308        true
309    }
310}
311
312impl FromStr for SandboxPolicy {
313    type Err = serde_json::Error;
314
315    fn from_str(s: &str) -> Result<Self, Self::Err> {
316        serde_json::from_str(s)
317    }
318}
319
320impl SandboxPolicy {
321    /// Returns a policy with read-only disk access and no network.
322    pub fn new_read_only_policy() -> Self {
323        SandboxPolicy::ReadOnly
324    }
325
326    /// Returns a policy that can read the entire disk, but can only write to
327    /// the current working directory and the per-user tmp dir on macOS. It does
328    /// not allow network access.
329    pub fn new_workspace_write_policy() -> Self {
330        SandboxPolicy::WorkspaceWrite {
331            writable_roots: vec![],
332            network_access: false,
333            exclude_tmpdir_env_var: false,
334            exclude_slash_tmp: false,
335        }
336    }
337
338    /// Always returns `true`; restricting read access is not supported.
339    pub fn has_full_disk_read_access(&self) -> bool {
340        true
341    }
342
343    pub fn has_full_disk_write_access(&self) -> bool {
344        match self {
345            SandboxPolicy::DangerFullAccess => true,
346            SandboxPolicy::ReadOnly => false,
347            SandboxPolicy::WorkspaceWrite { .. } => false,
348        }
349    }
350
351    pub fn has_full_network_access(&self) -> bool {
352        match self {
353            SandboxPolicy::DangerFullAccess => true,
354            SandboxPolicy::ReadOnly => false,
355            SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access,
356        }
357    }
358
359    /// Returns the list of writable roots (tailored to the current working
360    /// directory) together with subpaths that should remain read‑only under
361    /// each writable root.
362    pub fn get_writable_roots_with_cwd(&self, cwd: &Path) -> Vec<WritableRoot> {
363        match self {
364            SandboxPolicy::DangerFullAccess => Vec::new(),
365            SandboxPolicy::ReadOnly => Vec::new(),
366            SandboxPolicy::WorkspaceWrite {
367                writable_roots,
368                exclude_tmpdir_env_var,
369                exclude_slash_tmp,
370                network_access: _,
371            } => {
372                // Start from explicitly configured writable roots.
373                let mut roots: Vec<PathBuf> = writable_roots.clone();
374
375                // Always include defaults: cwd, /tmp (if present on Unix), and
376                // on macOS, the per-user TMPDIR unless explicitly excluded.
377                roots.push(cwd.to_path_buf());
378
379                // Include /tmp on Unix unless explicitly excluded.
380                if cfg!(unix) && !exclude_slash_tmp {
381                    let slash_tmp = PathBuf::from("/tmp");
382                    if slash_tmp.is_dir() {
383                        roots.push(slash_tmp);
384                    }
385                }
386
387                // Include $TMPDIR unless explicitly excluded. On macOS, TMPDIR
388                // is per-user, so writes to TMPDIR should not be readable by
389                // other users on the system.
390                //
391                // By comparison, TMPDIR is not guaranteed to be defined on
392                // Linux or Windows, but supporting it here gives users a way to
393                // provide the model with their own temporary directory without
394                // having to hardcode it in the config.
395                if !exclude_tmpdir_env_var
396                    && let Some(tmpdir) = std::env::var_os("TMPDIR")
397                    && !tmpdir.is_empty()
398                {
399                    roots.push(PathBuf::from(tmpdir));
400                }
401
402                // For each root, compute subpaths that should remain read-only.
403                roots
404                    .into_iter()
405                    .map(|writable_root| {
406                        let mut subpaths = Vec::new();
407                        let top_level_git = writable_root.join(".git");
408                        if top_level_git.is_dir() {
409                            subpaths.push(top_level_git);
410                        }
411                        WritableRoot {
412                            root: writable_root,
413                            read_only_subpaths: subpaths,
414                        }
415                    })
416                    .collect()
417            }
418        }
419    }
420}
421
422/// Event Queue Entry - events from agent
423#[derive(Debug, Clone, Deserialize, Serialize)]
424pub struct Event {
425    /// Submission `id` that this event is correlated with.
426    pub id: String,
427    /// Payload
428    pub msg: EventMsg,
429}
430
431/// Response event from the agent
432/// NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.
433#[derive(Debug, Clone, Deserialize, Serialize, Display, JsonSchema, TS)]
434#[serde(tag = "type", rename_all = "snake_case")]
435#[ts(tag = "type")]
436#[strum(serialize_all = "snake_case")]
437pub enum EventMsg {
438    /// Error while executing a submission
439    Error(ErrorEvent),
440
441    /// Warning issued while processing a submission. Unlike `Error`, this
442    /// indicates the task continued but the user should still be notified.
443    Warning(WarningEvent),
444
445    /// Agent has started a task
446    TaskStarted(TaskStartedEvent),
447
448    /// Agent has completed all actions
449    TaskComplete(TaskCompleteEvent),
450
451    /// Usage update for the current session, including totals and last turn.
452    /// Optional means unknown — UIs should not display when `None`.
453    TokenCount(TokenCountEvent),
454
455    /// Agent text output message
456    AgentMessage(AgentMessageEvent),
457
458    /// User/system input message (what was sent to the model)
459    UserMessage(UserMessageEvent),
460
461    /// Agent text output delta message
462    AgentMessageDelta(AgentMessageDeltaEvent),
463
464    /// Reasoning event from agent.
465    AgentReasoning(AgentReasoningEvent),
466
467    /// Agent reasoning delta event from agent.
468    AgentReasoningDelta(AgentReasoningDeltaEvent),
469
470    /// Raw chain-of-thought from agent.
471    AgentReasoningRawContent(AgentReasoningRawContentEvent),
472
473    /// Agent reasoning content delta event from agent.
474    AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent),
475    /// Signaled when the model begins a new reasoning summary section (e.g., a new titled block).
476    AgentReasoningSectionBreak(AgentReasoningSectionBreakEvent),
477
478    /// Ack the client's configure message.
479    SessionConfigured(SessionConfiguredEvent),
480
481    /// Incremental MCP startup progress updates.
482    McpStartupUpdate(McpStartupUpdateEvent),
483
484    /// Aggregate MCP startup completion summary.
485    McpStartupComplete(McpStartupCompleteEvent),
486
487    McpToolCallBegin(McpToolCallBeginEvent),
488
489    McpToolCallEnd(McpToolCallEndEvent),
490
491    WebSearchBegin(WebSearchBeginEvent),
492
493    WebSearchEnd(WebSearchEndEvent),
494
495    /// Notification that the server is about to execute a command.
496    ExecCommandBegin(ExecCommandBeginEvent),
497
498    /// Incremental chunk of output from a running command.
499    ExecCommandOutputDelta(ExecCommandOutputDeltaEvent),
500
501    ExecCommandEnd(ExecCommandEndEvent),
502
503    /// Notification that the agent attached a local image via the view_image tool.
504    ViewImageToolCall(ViewImageToolCallEvent),
505
506    ExecApprovalRequest(ExecApprovalRequestEvent),
507
508    ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent),
509
510    /// Notification advising the user that something they are using has been
511    /// deprecated and should be phased out.
512    DeprecationNotice(DeprecationNoticeEvent),
513
514    BackgroundEvent(BackgroundEventEvent),
515
516    UndoStarted(UndoStartedEvent),
517
518    UndoCompleted(UndoCompletedEvent),
519
520    /// Notification that a model stream experienced an error or disconnect
521    /// and the system is handling it (e.g., retrying with backoff).
522    StreamError(StreamErrorEvent),
523
524    /// Notification that the agent is about to apply a code patch. Mirrors
525    /// `ExecCommandBegin` so front‑ends can show progress indicators.
526    PatchApplyBegin(PatchApplyBeginEvent),
527
528    /// Notification that a patch application has finished.
529    PatchApplyEnd(PatchApplyEndEvent),
530
531    TurnDiff(TurnDiffEvent),
532
533    /// Response to GetHistoryEntryRequest.
534    GetHistoryEntryResponse(GetHistoryEntryResponseEvent),
535
536    /// List of MCP tools available to the agent.
537    McpListToolsResponse(McpListToolsResponseEvent),
538
539    /// List of custom prompts available to the agent.
540    ListCustomPromptsResponse(ListCustomPromptsResponseEvent),
541
542    PlanUpdate(UpdatePlanArgs),
543
544    TurnAborted(TurnAbortedEvent),
545
546    /// Notification that the agent is shutting down.
547    ShutdownComplete,
548
549    /// Entered review mode.
550    EnteredReviewMode(ReviewRequest),
551
552    /// Exited review mode with an optional final result to apply.
553    ExitedReviewMode(ExitedReviewModeEvent),
554
555    RawResponseItem(RawResponseItemEvent),
556
557    ItemStarted(ItemStartedEvent),
558    ItemCompleted(ItemCompletedEvent),
559
560    AgentMessageContentDelta(AgentMessageContentDeltaEvent),
561    ReasoningContentDelta(ReasoningContentDeltaEvent),
562    ReasoningRawContentDelta(ReasoningRawContentDeltaEvent),
563}
564
565/// Codex errors that we expose to clients.
566#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
567#[serde(rename_all = "snake_case")]
568#[ts(rename_all = "snake_case")]
569pub enum CodexErrorInfo {
570    ContextWindowExceeded,
571    UsageLimitExceeded,
572    HttpConnectionFailed {
573        http_status_code: Option<u16>,
574    },
575    /// Failed to connect to the response SSE stream.
576    ResponseStreamConnectionFailed {
577        http_status_code: Option<u16>,
578    },
579    InternalServerError,
580    Unauthorized,
581    BadRequest,
582    SandboxError,
583    /// The response SSE stream disconnected in the middle of a turnbefore completion.
584    ResponseStreamDisconnected {
585        http_status_code: Option<u16>,
586    },
587    /// Reached the retry limit for responses.
588    ResponseTooManyFailedAttempts {
589        http_status_code: Option<u16>,
590    },
591    Other,
592}
593
594#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
595pub struct RawResponseItemEvent {
596    pub item: ResponseItem,
597}
598
599#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
600pub struct ItemStartedEvent {
601    pub thread_id: ConversationId,
602    pub turn_id: String,
603    pub item: TurnItem,
604}
605
606impl HasLegacyEvent for ItemStartedEvent {
607    fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
608        match &self.item {
609            TurnItem::WebSearch(item) => vec![EventMsg::WebSearchBegin(WebSearchBeginEvent {
610                call_id: item.id.clone(),
611            })],
612            _ => Vec::new(),
613        }
614    }
615}
616
617#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
618pub struct ItemCompletedEvent {
619    pub thread_id: ConversationId,
620    pub turn_id: String,
621    pub item: TurnItem,
622}
623
624pub trait HasLegacyEvent {
625    fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg>;
626}
627
628impl HasLegacyEvent for ItemCompletedEvent {
629    fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg> {
630        self.item.as_legacy_events(show_raw_agent_reasoning)
631    }
632}
633
634#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
635pub struct AgentMessageContentDeltaEvent {
636    pub thread_id: String,
637    pub turn_id: String,
638    pub item_id: String,
639    pub delta: String,
640}
641
642impl HasLegacyEvent for AgentMessageContentDeltaEvent {
643    fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
644        vec![EventMsg::AgentMessageDelta(AgentMessageDeltaEvent {
645            delta: self.delta.clone(),
646        })]
647    }
648}
649
650#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
651pub struct ReasoningContentDeltaEvent {
652    pub thread_id: String,
653    pub turn_id: String,
654    pub item_id: String,
655    pub delta: String,
656    // load with default value so it's backward compatible with the old format.
657    #[serde(default)]
658    pub summary_index: i64,
659}
660
661impl HasLegacyEvent for ReasoningContentDeltaEvent {
662    fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
663        vec![EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent {
664            delta: self.delta.clone(),
665        })]
666    }
667}
668
669#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
670pub struct ReasoningRawContentDeltaEvent {
671    pub thread_id: String,
672    pub turn_id: String,
673    pub item_id: String,
674    pub delta: String,
675    // load with default value so it's backward compatible with the old format.
676    #[serde(default)]
677    pub content_index: i64,
678}
679
680impl HasLegacyEvent for ReasoningRawContentDeltaEvent {
681    fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
682        vec![EventMsg::AgentReasoningRawContentDelta(
683            AgentReasoningRawContentDeltaEvent {
684                delta: self.delta.clone(),
685            },
686        )]
687    }
688}
689
690impl HasLegacyEvent for EventMsg {
691    fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg> {
692        match self {
693            EventMsg::ItemCompleted(event) => event.as_legacy_events(show_raw_agent_reasoning),
694            EventMsg::AgentMessageContentDelta(event) => {
695                event.as_legacy_events(show_raw_agent_reasoning)
696            }
697            EventMsg::ReasoningContentDelta(event) => {
698                event.as_legacy_events(show_raw_agent_reasoning)
699            }
700            EventMsg::ReasoningRawContentDelta(event) => {
701                event.as_legacy_events(show_raw_agent_reasoning)
702            }
703            _ => Vec::new(),
704        }
705    }
706}
707
708#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
709pub struct ExitedReviewModeEvent {
710    pub review_output: Option<ReviewOutputEvent>,
711}
712
713// Individual event payload types matching each `EventMsg` variant.
714
715#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
716pub struct ErrorEvent {
717    pub message: String,
718    #[serde(default)]
719    pub codex_error_info: Option<CodexErrorInfo>,
720}
721
722#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
723pub struct WarningEvent {
724    pub message: String,
725}
726
727#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
728pub struct TaskCompleteEvent {
729    pub last_agent_message: Option<String>,
730}
731
732#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
733pub struct TaskStartedEvent {
734    pub model_context_window: Option<i64>,
735}
736
737#[derive(Debug, Clone, Deserialize, Serialize, Default, JsonSchema, TS)]
738pub struct TokenUsage {
739    #[ts(type = "number")]
740    pub input_tokens: i64,
741    #[ts(type = "number")]
742    pub cached_input_tokens: i64,
743    #[ts(type = "number")]
744    pub output_tokens: i64,
745    #[ts(type = "number")]
746    pub reasoning_output_tokens: i64,
747    #[ts(type = "number")]
748    pub total_tokens: i64,
749}
750
751#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
752pub struct TokenUsageInfo {
753    pub total_token_usage: TokenUsage,
754    pub last_token_usage: TokenUsage,
755    #[ts(type = "number | null")]
756    pub model_context_window: Option<i64>,
757}
758
759impl TokenUsageInfo {
760    pub fn new_or_append(
761        info: &Option<TokenUsageInfo>,
762        last: &Option<TokenUsage>,
763        model_context_window: Option<i64>,
764    ) -> Option<Self> {
765        if info.is_none() && last.is_none() {
766            return None;
767        }
768
769        let mut info = match info {
770            Some(info) => info.clone(),
771            None => Self {
772                total_token_usage: TokenUsage::default(),
773                last_token_usage: TokenUsage::default(),
774                model_context_window,
775            },
776        };
777        if let Some(last) = last {
778            info.append_last_usage(last);
779        }
780        Some(info)
781    }
782
783    pub fn append_last_usage(&mut self, last: &TokenUsage) {
784        self.total_token_usage.add_assign(last);
785        self.last_token_usage = last.clone();
786    }
787
788    pub fn fill_to_context_window(&mut self, context_window: i64) {
789        let previous_total = self.total_token_usage.total_tokens;
790        let delta = (context_window - previous_total).max(0);
791
792        self.model_context_window = Some(context_window);
793        self.total_token_usage = TokenUsage {
794            total_tokens: context_window,
795            ..TokenUsage::default()
796        };
797        self.last_token_usage = TokenUsage {
798            total_tokens: delta,
799            ..TokenUsage::default()
800        };
801    }
802
803    pub fn full_context_window(context_window: i64) -> Self {
804        let mut info = Self {
805            total_token_usage: TokenUsage::default(),
806            last_token_usage: TokenUsage::default(),
807            model_context_window: Some(context_window),
808        };
809        info.fill_to_context_window(context_window);
810        info
811    }
812}
813
814#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
815pub struct TokenCountEvent {
816    pub info: Option<TokenUsageInfo>,
817    pub rate_limits: Option<RateLimitSnapshot>,
818}
819
820#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
821pub struct RateLimitSnapshot {
822    pub primary: Option<RateLimitWindow>,
823    pub secondary: Option<RateLimitWindow>,
824    pub credits: Option<CreditsSnapshot>,
825}
826
827#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
828pub struct RateLimitWindow {
829    /// Percentage (0-100) of the window that has been consumed.
830    pub used_percent: f64,
831    /// Rolling window duration, in minutes.
832    #[ts(type = "number | null")]
833    pub window_minutes: Option<i64>,
834    /// Unix timestamp (seconds since epoch) when the window resets.
835    #[ts(type = "number | null")]
836    pub resets_at: Option<i64>,
837}
838
839#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
840pub struct CreditsSnapshot {
841    pub has_credits: bool,
842    pub unlimited: bool,
843    pub balance: Option<String>,
844}
845
846// Includes prompts, tools and space to call compact.
847const BASELINE_TOKENS: i64 = 12000;
848
849impl TokenUsage {
850    pub fn is_zero(&self) -> bool {
851        self.total_tokens == 0
852    }
853
854    pub fn cached_input(&self) -> i64 {
855        self.cached_input_tokens.max(0)
856    }
857
858    pub fn non_cached_input(&self) -> i64 {
859        (self.input_tokens - self.cached_input()).max(0)
860    }
861
862    /// Primary count for display as a single absolute value: non-cached input + output.
863    pub fn blended_total(&self) -> i64 {
864        (self.non_cached_input() + self.output_tokens.max(0)).max(0)
865    }
866
867    pub fn tokens_in_context_window(&self) -> i64 {
868        self.total_tokens
869    }
870
871    /// Estimate the remaining user-controllable percentage of the model's context window.
872    ///
873    /// `context_window` is the total size of the model's context window.
874    /// `BASELINE_TOKENS` should capture tokens that are always present in
875    /// the context (e.g., system prompt and fixed tool instructions) so that
876    /// the percentage reflects the portion the user can influence.
877    ///
878    /// This normalizes both the numerator and denominator by subtracting the
879    /// baseline, so immediately after the first prompt the UI shows 100% left
880    /// and trends toward 0% as the user fills the effective window.
881    pub fn percent_of_context_window_remaining(&self, context_window: i64) -> i64 {
882        if context_window <= BASELINE_TOKENS {
883            return 0;
884        }
885
886        let effective_window = context_window - BASELINE_TOKENS;
887        let used = (self.tokens_in_context_window() - BASELINE_TOKENS).max(0);
888        let remaining = (effective_window - used).max(0);
889        ((remaining as f64 / effective_window as f64) * 100.0)
890            .clamp(0.0, 100.0)
891            .round() as i64
892    }
893
894    /// In-place element-wise sum of token counts.
895    pub fn add_assign(&mut self, other: &TokenUsage) {
896        self.input_tokens += other.input_tokens;
897        self.cached_input_tokens += other.cached_input_tokens;
898        self.output_tokens += other.output_tokens;
899        self.reasoning_output_tokens += other.reasoning_output_tokens;
900        self.total_tokens += other.total_tokens;
901    }
902}
903
904#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
905pub struct FinalOutput {
906    pub token_usage: TokenUsage,
907}
908
909impl From<TokenUsage> for FinalOutput {
910    fn from(token_usage: TokenUsage) -> Self {
911        Self { token_usage }
912    }
913}
914
915impl fmt::Display for FinalOutput {
916    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
917        let token_usage = &self.token_usage;
918
919        write!(
920            f,
921            "Token usage: total={} input={}{} output={}{}",
922            format_with_separators(token_usage.blended_total()),
923            format_with_separators(token_usage.non_cached_input()),
924            if token_usage.cached_input() > 0 {
925                format!(
926                    " (+ {} cached)",
927                    format_with_separators(token_usage.cached_input())
928                )
929            } else {
930                String::new()
931            },
932            format_with_separators(token_usage.output_tokens),
933            if token_usage.reasoning_output_tokens > 0 {
934                format!(
935                    " (reasoning {})",
936                    format_with_separators(token_usage.reasoning_output_tokens)
937                )
938            } else {
939                String::new()
940            }
941        )
942    }
943}
944
945#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
946pub struct AgentMessageEvent {
947    pub message: String,
948}
949
950#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
951pub struct UserMessageEvent {
952    pub message: String,
953    #[serde(skip_serializing_if = "Option::is_none")]
954    pub images: Option<Vec<String>>,
955}
956
957#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
958pub struct AgentMessageDeltaEvent {
959    pub delta: String,
960}
961
962#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
963pub struct AgentReasoningEvent {
964    pub text: String,
965}
966
967#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
968pub struct AgentReasoningRawContentEvent {
969    pub text: String,
970}
971
972#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
973pub struct AgentReasoningRawContentDeltaEvent {
974    pub delta: String,
975}
976
977#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
978pub struct AgentReasoningSectionBreakEvent {
979    // load with default value so it's backward compatible with the old format.
980    #[serde(default)]
981    pub item_id: String,
982    #[serde(default)]
983    pub summary_index: i64,
984}
985
986#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
987pub struct AgentReasoningDeltaEvent {
988    pub delta: String,
989}
990
991#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
992pub struct McpInvocation {
993    /// Name of the MCP server as defined in the config.
994    pub server: String,
995    /// Name of the tool as given by the MCP server.
996    pub tool: String,
997    /// Arguments to the tool call.
998    pub arguments: Option<serde_json::Value>,
999}
1000
1001#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
1002pub struct McpToolCallBeginEvent {
1003    /// Identifier so this can be paired with the McpToolCallEnd event.
1004    pub call_id: String,
1005    pub invocation: McpInvocation,
1006}
1007
1008#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
1009pub struct McpToolCallEndEvent {
1010    /// Identifier for the corresponding McpToolCallBegin that finished.
1011    pub call_id: String,
1012    pub invocation: McpInvocation,
1013    #[ts(type = "string")]
1014    pub duration: Duration,
1015    /// Result of the tool call. Note this could be an error.
1016    pub result: Result<CallToolResult, String>,
1017}
1018
1019impl McpToolCallEndEvent {
1020    pub fn is_success(&self) -> bool {
1021        match &self.result {
1022            Ok(result) => !result.is_error.unwrap_or(false),
1023            Err(_) => false,
1024        }
1025    }
1026}
1027
1028#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1029pub struct WebSearchBeginEvent {
1030    pub call_id: String,
1031}
1032
1033#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1034pub struct WebSearchEndEvent {
1035    pub call_id: String,
1036    pub query: String,
1037}
1038
1039/// Response payload for `Op::GetHistory` containing the current session's
1040/// in-memory transcript.
1041#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1042pub struct ConversationPathResponseEvent {
1043    pub conversation_id: ConversationId,
1044    pub path: PathBuf,
1045}
1046
1047#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1048pub struct ResumedHistory {
1049    pub conversation_id: ConversationId,
1050    pub history: Vec<RolloutItem>,
1051    pub rollout_path: PathBuf,
1052}
1053
1054#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1055pub enum InitialHistory {
1056    New,
1057    Resumed(ResumedHistory),
1058    Forked(Vec<RolloutItem>),
1059}
1060
1061impl InitialHistory {
1062    pub fn get_rollout_items(&self) -> Vec<RolloutItem> {
1063        match self {
1064            InitialHistory::New => Vec::new(),
1065            InitialHistory::Resumed(resumed) => resumed.history.clone(),
1066            InitialHistory::Forked(items) => items.clone(),
1067        }
1068    }
1069
1070    pub fn get_event_msgs(&self) -> Option<Vec<EventMsg>> {
1071        match self {
1072            InitialHistory::New => None,
1073            InitialHistory::Resumed(resumed) => Some(
1074                resumed
1075                    .history
1076                    .iter()
1077                    .filter_map(|ri| match ri {
1078                        RolloutItem::EventMsg(ev) => Some(ev.clone()),
1079                        _ => None,
1080                    })
1081                    .collect(),
1082            ),
1083            InitialHistory::Forked(items) => Some(
1084                items
1085                    .iter()
1086                    .filter_map(|ri| match ri {
1087                        RolloutItem::EventMsg(ev) => Some(ev.clone()),
1088                        _ => None,
1089                    })
1090                    .collect(),
1091            ),
1092        }
1093    }
1094}
1095
1096#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
1097#[serde(rename_all = "lowercase")]
1098#[ts(rename_all = "lowercase")]
1099pub enum SessionSource {
1100    Cli,
1101    #[default]
1102    VSCode,
1103    Exec,
1104    Mcp,
1105    SubAgent(SubAgentSource),
1106    #[serde(other)]
1107    Unknown,
1108}
1109
1110#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
1111#[serde(rename_all = "snake_case")]
1112#[ts(rename_all = "snake_case")]
1113pub enum SubAgentSource {
1114    Review,
1115    Compact,
1116    Other(String),
1117}
1118
1119#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
1120pub struct SessionMeta {
1121    pub id: ConversationId,
1122    pub timestamp: String,
1123    pub cwd: PathBuf,
1124    pub originator: String,
1125    pub cli_version: String,
1126    pub instructions: Option<String>,
1127    #[serde(default)]
1128    pub source: SessionSource,
1129    pub model_provider: Option<String>,
1130}
1131
1132impl Default for SessionMeta {
1133    fn default() -> Self {
1134        SessionMeta {
1135            id: ConversationId::default(),
1136            timestamp: String::new(),
1137            cwd: PathBuf::new(),
1138            originator: String::new(),
1139            cli_version: String::new(),
1140            instructions: None,
1141            source: SessionSource::default(),
1142            model_provider: None,
1143        }
1144    }
1145}
1146
1147#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
1148pub struct SessionMetaLine {
1149    #[serde(flatten)]
1150    pub meta: SessionMeta,
1151    #[serde(skip_serializing_if = "Option::is_none")]
1152    pub git: Option<GitInfo>,
1153}
1154
1155#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
1156#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
1157pub enum RolloutItem {
1158    SessionMeta(SessionMetaLine),
1159    ResponseItem(ResponseItem),
1160    Compacted(CompactedItem),
1161    TurnContext(TurnContextItem),
1162    EventMsg(EventMsg),
1163}
1164
1165#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
1166pub struct CompactedItem {
1167    pub message: String,
1168    #[serde(default, skip_serializing_if = "Option::is_none")]
1169    pub replacement_history: Option<Vec<ResponseItem>>,
1170}
1171
1172impl From<CompactedItem> for ResponseItem {
1173    fn from(value: CompactedItem) -> Self {
1174        ResponseItem::Message {
1175            id: None,
1176            role: "assistant".to_string(),
1177            content: vec![ContentItem::OutputText {
1178                text: value.message,
1179            }],
1180        }
1181    }
1182}
1183
1184#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
1185pub struct TurnContextItem {
1186    pub cwd: PathBuf,
1187    pub approval_policy: AskForApproval,
1188    pub sandbox_policy: SandboxPolicy,
1189    pub model: String,
1190    #[serde(skip_serializing_if = "Option::is_none")]
1191    pub effort: Option<ReasoningEffortConfig>,
1192    pub summary: ReasoningSummaryConfig,
1193}
1194
1195#[derive(Serialize, Deserialize, Clone, JsonSchema)]
1196pub struct RolloutLine {
1197    pub timestamp: String,
1198    #[serde(flatten)]
1199    pub item: RolloutItem,
1200}
1201
1202#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
1203pub struct GitInfo {
1204    /// Current commit hash (SHA)
1205    #[serde(skip_serializing_if = "Option::is_none")]
1206    pub commit_hash: Option<String>,
1207    /// Current branch name
1208    #[serde(skip_serializing_if = "Option::is_none")]
1209    pub branch: Option<String>,
1210    /// Repository URL (if available from remote)
1211    #[serde(skip_serializing_if = "Option::is_none")]
1212    pub repository_url: Option<String>,
1213}
1214
1215#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1216/// Review request sent to the review session.
1217pub struct ReviewRequest {
1218    pub prompt: String,
1219    pub user_facing_hint: String,
1220    #[serde(default)]
1221    pub append_to_original_thread: bool,
1222}
1223
1224/// Structured review result produced by a child review session.
1225#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1226pub struct ReviewOutputEvent {
1227    pub findings: Vec<ReviewFinding>,
1228    pub overall_correctness: String,
1229    pub overall_explanation: String,
1230    pub overall_confidence_score: f32,
1231}
1232
1233impl Default for ReviewOutputEvent {
1234    fn default() -> Self {
1235        Self {
1236            findings: Vec::new(),
1237            overall_correctness: String::default(),
1238            overall_explanation: String::default(),
1239            overall_confidence_score: 0.0,
1240        }
1241    }
1242}
1243
1244/// A single review finding describing an observed issue or recommendation.
1245#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1246pub struct ReviewFinding {
1247    pub title: String,
1248    pub body: String,
1249    pub confidence_score: f32,
1250    pub priority: i32,
1251    pub code_location: ReviewCodeLocation,
1252}
1253
1254/// Location of the code related to a review finding.
1255#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1256pub struct ReviewCodeLocation {
1257    pub absolute_file_path: PathBuf,
1258    pub line_range: ReviewLineRange,
1259}
1260
1261/// Inclusive line range in a file associated with the finding.
1262#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1263pub struct ReviewLineRange {
1264    pub start: u32,
1265    pub end: u32,
1266}
1267
1268#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1269#[serde(rename_all = "snake_case")]
1270pub enum ExecCommandSource {
1271    Agent,
1272    UserShell,
1273    UnifiedExecStartup,
1274    UnifiedExecInteraction,
1275}
1276
1277impl Default for ExecCommandSource {
1278    fn default() -> Self {
1279        Self::Agent
1280    }
1281}
1282
1283#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1284pub struct ExecCommandBeginEvent {
1285    /// Identifier so this can be paired with the ExecCommandEnd event.
1286    pub call_id: String,
1287    /// Turn ID that this command belongs to.
1288    pub turn_id: String,
1289    /// The command to be executed.
1290    pub command: Vec<String>,
1291    /// The command's working directory if not the default cwd for the agent.
1292    pub cwd: PathBuf,
1293    pub parsed_cmd: Vec<ParsedCommand>,
1294    /// Where the command originated. Defaults to Agent for backward compatibility.
1295    #[serde(default)]
1296    pub source: ExecCommandSource,
1297    /// Raw input sent to a unified exec session (if this is an interaction event).
1298    #[serde(default, skip_serializing_if = "Option::is_none")]
1299    #[ts(optional)]
1300    pub interaction_input: Option<String>,
1301}
1302
1303#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1304pub struct ExecCommandEndEvent {
1305    /// Identifier for the ExecCommandBegin that finished.
1306    pub call_id: String,
1307    /// Turn ID that this command belongs to.
1308    pub turn_id: String,
1309    /// The command that was executed.
1310    pub command: Vec<String>,
1311    /// The command's working directory if not the default cwd for the agent.
1312    pub cwd: PathBuf,
1313    pub parsed_cmd: Vec<ParsedCommand>,
1314    /// Where the command originated. Defaults to Agent for backward compatibility.
1315    #[serde(default)]
1316    pub source: ExecCommandSource,
1317    /// Raw input sent to a unified exec session (if this is an interaction event).
1318    #[serde(default, skip_serializing_if = "Option::is_none")]
1319    #[ts(optional)]
1320    pub interaction_input: Option<String>,
1321
1322    /// Captured stdout
1323    pub stdout: String,
1324    /// Captured stderr
1325    pub stderr: String,
1326    /// Captured aggregated output
1327    #[serde(default)]
1328    pub aggregated_output: String,
1329    /// The command's exit code.
1330    pub exit_code: i32,
1331    /// The duration of the command execution.
1332    #[ts(type = "string")]
1333    pub duration: Duration,
1334    /// Formatted output from the command, as seen by the model.
1335    pub formatted_output: String,
1336}
1337
1338#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1339pub struct ViewImageToolCallEvent {
1340    /// Identifier for the originating tool call.
1341    pub call_id: String,
1342    /// Local filesystem path provided to the tool.
1343    pub path: PathBuf,
1344}
1345
1346#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1347#[serde(rename_all = "snake_case")]
1348pub enum ExecOutputStream {
1349    Stdout,
1350    Stderr,
1351}
1352
1353#[serde_as]
1354#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1355pub struct ExecCommandOutputDeltaEvent {
1356    /// Identifier for the ExecCommandBegin that produced this chunk.
1357    pub call_id: String,
1358    /// Which stream produced this chunk.
1359    pub stream: ExecOutputStream,
1360    /// Raw bytes from the stream (may not be valid UTF-8).
1361    #[serde_as(as = "serde_with::base64::Base64")]
1362    #[schemars(with = "String")]
1363    #[ts(type = "string")]
1364    pub chunk: Vec<u8>,
1365}
1366
1367#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1368pub struct BackgroundEventEvent {
1369    pub message: String,
1370}
1371
1372#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1373pub struct DeprecationNoticeEvent {
1374    /// Concise summary of what is deprecated.
1375    pub summary: String,
1376    /// Optional extra guidance, such as migration steps or rationale.
1377    #[serde(skip_serializing_if = "Option::is_none")]
1378    pub details: Option<String>,
1379}
1380
1381#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1382pub struct UndoStartedEvent {
1383    #[serde(skip_serializing_if = "Option::is_none")]
1384    pub message: Option<String>,
1385}
1386
1387#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1388pub struct UndoCompletedEvent {
1389    pub success: bool,
1390    #[serde(skip_serializing_if = "Option::is_none")]
1391    pub message: Option<String>,
1392}
1393
1394#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1395pub struct StreamErrorEvent {
1396    pub message: String,
1397    #[serde(default)]
1398    pub codex_error_info: Option<CodexErrorInfo>,
1399}
1400
1401#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1402pub struct StreamInfoEvent {
1403    pub message: String,
1404}
1405
1406#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1407pub struct PatchApplyBeginEvent {
1408    /// Identifier so this can be paired with the PatchApplyEnd event.
1409    pub call_id: String,
1410    /// Turn ID that this patch belongs to.
1411    /// Uses `#[serde(default)]` for backwards compatibility.
1412    #[serde(default)]
1413    pub turn_id: String,
1414    /// If true, there was no ApplyPatchApprovalRequest for this patch.
1415    pub auto_approved: bool,
1416    /// The changes to be applied.
1417    pub changes: HashMap<PathBuf, FileChange>,
1418}
1419
1420#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1421pub struct PatchApplyEndEvent {
1422    /// Identifier for the PatchApplyBegin that finished.
1423    pub call_id: String,
1424    /// Turn ID that this patch belongs to.
1425    /// Uses `#[serde(default)]` for backwards compatibility.
1426    #[serde(default)]
1427    pub turn_id: String,
1428    /// Captured stdout (summary printed by apply_patch).
1429    pub stdout: String,
1430    /// Captured stderr (parser errors, IO failures, etc.).
1431    pub stderr: String,
1432    /// Whether the patch was applied successfully.
1433    pub success: bool,
1434    /// The changes that were applied (mirrors PatchApplyBeginEvent::changes).
1435    #[serde(default)]
1436    pub changes: HashMap<PathBuf, FileChange>,
1437}
1438
1439#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1440pub struct TurnDiffEvent {
1441    pub unified_diff: String,
1442}
1443
1444#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1445pub struct GetHistoryEntryResponseEvent {
1446    pub offset: usize,
1447    pub log_id: u64,
1448    /// The entry at the requested offset, if available and parseable.
1449    #[serde(skip_serializing_if = "Option::is_none")]
1450    pub entry: Option<HistoryEntry>,
1451}
1452
1453#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1454pub struct McpListToolsResponseEvent {
1455    /// Fully qualified tool name -> tool definition.
1456    pub tools: std::collections::HashMap<String, McpTool>,
1457    /// Known resources grouped by server name.
1458    pub resources: std::collections::HashMap<String, Vec<McpResource>>,
1459    /// Known resource templates grouped by server name.
1460    pub resource_templates: std::collections::HashMap<String, Vec<McpResourceTemplate>>,
1461    /// Authentication status for each configured MCP server.
1462    pub auth_statuses: std::collections::HashMap<String, McpAuthStatus>,
1463}
1464
1465#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1466pub struct McpStartupUpdateEvent {
1467    /// Server name being started.
1468    pub server: String,
1469    /// Current startup status.
1470    pub status: McpStartupStatus,
1471}
1472
1473#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1474#[serde(rename_all = "snake_case", tag = "state")]
1475#[ts(rename_all = "snake_case", tag = "state")]
1476pub enum McpStartupStatus {
1477    Starting,
1478    Ready,
1479    Failed { error: String },
1480    Cancelled,
1481}
1482
1483#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, Default)]
1484pub struct McpStartupCompleteEvent {
1485    pub ready: Vec<String>,
1486    pub failed: Vec<McpStartupFailure>,
1487    pub cancelled: Vec<String>,
1488}
1489
1490#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1491pub struct McpStartupFailure {
1492    pub server: String,
1493    pub error: String,
1494}
1495
1496#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
1497#[serde(rename_all = "snake_case")]
1498#[ts(rename_all = "snake_case")]
1499pub enum McpAuthStatus {
1500    Unsupported,
1501    NotLoggedIn,
1502    BearerToken,
1503    OAuth,
1504}
1505
1506impl fmt::Display for McpAuthStatus {
1507    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1508        let text = match self {
1509            McpAuthStatus::Unsupported => "Unsupported",
1510            McpAuthStatus::NotLoggedIn => "Not logged in",
1511            McpAuthStatus::BearerToken => "Bearer token",
1512            McpAuthStatus::OAuth => "OAuth",
1513        };
1514        f.write_str(text)
1515    }
1516}
1517
1518/// Response payload for `Op::ListCustomPrompts`.
1519#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1520pub struct ListCustomPromptsResponseEvent {
1521    pub custom_prompts: Vec<CustomPrompt>,
1522}
1523
1524#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1525pub struct SessionConfiguredEvent {
1526    /// Name left as session_id instead of conversation_id for backwards compatibility.
1527    pub session_id: ConversationId,
1528
1529    /// Tell the client what model is being queried.
1530    pub model: String,
1531
1532    pub model_provider_id: String,
1533
1534    /// When to escalate for approval for execution
1535    pub approval_policy: AskForApproval,
1536
1537    /// How to sandbox commands executed in the system
1538    pub sandbox_policy: SandboxPolicy,
1539
1540    /// Working directory that should be treated as the *root* of the
1541    /// session.
1542    pub cwd: PathBuf,
1543
1544    /// The effort the model is putting into reasoning about the user's request.
1545    #[serde(skip_serializing_if = "Option::is_none")]
1546    pub reasoning_effort: Option<ReasoningEffortConfig>,
1547
1548    /// Identifier of the history log file (inode on Unix, 0 otherwise).
1549    pub history_log_id: u64,
1550
1551    /// Current number of entries in the history log.
1552    pub history_entry_count: usize,
1553
1554    /// Optional initial messages (as events) for resumed sessions.
1555    /// When present, UIs can use these to seed the history.
1556    #[serde(skip_serializing_if = "Option::is_none")]
1557    pub initial_messages: Option<Vec<EventMsg>>,
1558
1559    pub rollout_path: PathBuf,
1560}
1561
1562/// User's decision in response to an ExecApprovalRequest.
1563#[derive(
1564    Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Display, JsonSchema, TS,
1565)]
1566#[serde(rename_all = "snake_case")]
1567pub enum ReviewDecision {
1568    /// User has approved this command and the agent should execute it.
1569    Approved,
1570
1571    /// User has approved this command and wants to automatically approve any
1572    /// future identical instances (`command` and `cwd` match exactly) for the
1573    /// remainder of the session.
1574    ApprovedForSession,
1575
1576    /// User has denied this command and the agent should not execute it, but
1577    /// it should continue the session and try something else.
1578    #[default]
1579    Denied,
1580
1581    /// User has denied this command and the agent should not do anything until
1582    /// the user's next command.
1583    Abort,
1584}
1585
1586#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1587#[serde(tag = "type", rename_all = "snake_case")]
1588#[ts(tag = "type")]
1589pub enum FileChange {
1590    Add {
1591        content: String,
1592    },
1593    Delete {
1594        content: String,
1595    },
1596    Update {
1597        unified_diff: String,
1598        move_path: Option<PathBuf>,
1599    },
1600}
1601
1602#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1603pub struct Chunk {
1604    /// 1-based line index of the first line in the original file
1605    pub orig_index: u32,
1606    pub deleted_lines: Vec<String>,
1607    pub inserted_lines: Vec<String>,
1608}
1609
1610#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1611pub struct TurnAbortedEvent {
1612    pub reason: TurnAbortReason,
1613}
1614
1615#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1616#[serde(rename_all = "snake_case")]
1617pub enum TurnAbortReason {
1618    Interrupted,
1619    Replaced,
1620    ReviewEnded,
1621}
1622
1623#[cfg(test)]
1624mod tests {
1625    use super::*;
1626    use crate::items::UserMessageItem;
1627    use crate::items::WebSearchItem;
1628    use anyhow::Result;
1629    use pretty_assertions::assert_eq;
1630    use serde_json::json;
1631    use tempfile::NamedTempFile;
1632
1633    #[test]
1634    fn item_started_event_from_web_search_emits_begin_event() {
1635        let event = ItemStartedEvent {
1636            thread_id: ConversationId::new(),
1637            turn_id: "turn-1".into(),
1638            item: TurnItem::WebSearch(WebSearchItem {
1639                id: "search-1".into(),
1640                query: "find docs".into(),
1641            }),
1642        };
1643
1644        let legacy_events = event.as_legacy_events(false);
1645        assert_eq!(legacy_events.len(), 1);
1646        match &legacy_events[0] {
1647            EventMsg::WebSearchBegin(event) => assert_eq!(event.call_id, "search-1"),
1648            _ => panic!("expected WebSearchBegin event"),
1649        }
1650    }
1651
1652    #[test]
1653    fn item_started_event_from_non_web_search_emits_no_legacy_events() {
1654        let event = ItemStartedEvent {
1655            thread_id: ConversationId::new(),
1656            turn_id: "turn-1".into(),
1657            item: TurnItem::UserMessage(UserMessageItem::new(&[])),
1658        };
1659
1660        assert!(event.as_legacy_events(false).is_empty());
1661    }
1662
1663    /// Serialize Event to verify that its JSON representation has the expected
1664    /// amount of nesting.
1665    #[test]
1666    fn serialize_event() -> Result<()> {
1667        let conversation_id = ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;
1668        let rollout_file = NamedTempFile::new()?;
1669        let event = Event {
1670            id: "1234".to_string(),
1671            msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
1672                session_id: conversation_id,
1673                model: "codex-mini-latest".to_string(),
1674                model_provider_id: "openai".to_string(),
1675                approval_policy: AskForApproval::Never,
1676                sandbox_policy: SandboxPolicy::ReadOnly,
1677                cwd: PathBuf::from("/home/user/project"),
1678                reasoning_effort: Some(ReasoningEffortConfig::default()),
1679                history_log_id: 0,
1680                history_entry_count: 0,
1681                initial_messages: None,
1682                rollout_path: rollout_file.path().to_path_buf(),
1683            }),
1684        };
1685
1686        let expected = json!({
1687            "id": "1234",
1688            "msg": {
1689                "type": "session_configured",
1690                "session_id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
1691                "model": "codex-mini-latest",
1692                "model_provider_id": "openai",
1693                "approval_policy": "never",
1694                "sandbox_policy": {
1695                    "type": "read-only"
1696                },
1697                "cwd": "/home/user/project",
1698                "reasoning_effort": "medium",
1699                "history_log_id": 0,
1700                "history_entry_count": 0,
1701                "rollout_path": format!("{}", rollout_file.path().display()),
1702            }
1703        });
1704        assert_eq!(expected, serde_json::to_value(&event)?);
1705        Ok(())
1706    }
1707
1708    #[test]
1709    fn vec_u8_as_base64_serialization_and_deserialization() -> Result<()> {
1710        let event = ExecCommandOutputDeltaEvent {
1711            call_id: "call21".to_string(),
1712            stream: ExecOutputStream::Stdout,
1713            chunk: vec![1, 2, 3, 4, 5],
1714        };
1715        let serialized = serde_json::to_string(&event)?;
1716        assert_eq!(
1717            r#"{"call_id":"call21","stream":"stdout","chunk":"AQIDBAU="}"#,
1718            serialized,
1719        );
1720
1721        let deserialized: ExecCommandOutputDeltaEvent = serde_json::from_str(&serialized)?;
1722        assert_eq!(deserialized, event);
1723        Ok(())
1724    }
1725
1726    #[test]
1727    fn serialize_mcp_startup_update_event() -> Result<()> {
1728        let event = Event {
1729            id: "init".to_string(),
1730            msg: EventMsg::McpStartupUpdate(McpStartupUpdateEvent {
1731                server: "srv".to_string(),
1732                status: McpStartupStatus::Failed {
1733                    error: "boom".to_string(),
1734                },
1735            }),
1736        };
1737
1738        let value = serde_json::to_value(&event)?;
1739        assert_eq!(value["msg"]["type"], "mcp_startup_update");
1740        assert_eq!(value["msg"]["server"], "srv");
1741        assert_eq!(value["msg"]["status"]["state"], "failed");
1742        assert_eq!(value["msg"]["status"]["error"], "boom");
1743        Ok(())
1744    }
1745
1746    #[test]
1747    fn serialize_mcp_startup_complete_event() -> Result<()> {
1748        let event = Event {
1749            id: "init".to_string(),
1750            msg: EventMsg::McpStartupComplete(McpStartupCompleteEvent {
1751                ready: vec!["a".to_string()],
1752                failed: vec![McpStartupFailure {
1753                    server: "b".to_string(),
1754                    error: "bad".to_string(),
1755                }],
1756                cancelled: vec!["c".to_string()],
1757            }),
1758        };
1759
1760        let value = serde_json::to_value(&event)?;
1761        assert_eq!(value["msg"]["type"], "mcp_startup_complete");
1762        assert_eq!(value["msg"]["ready"][0], "a");
1763        assert_eq!(value["msg"]["failed"][0]["server"], "b");
1764        assert_eq!(value["msg"]["failed"][0]["error"], "bad");
1765        assert_eq!(value["msg"]["cancelled"][0], "c");
1766        Ok(())
1767    }
1768}