Skip to main content

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