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::ffi::OsStr;
8use std::fmt;
9use std::path::Path;
10use std::path::PathBuf;
11use std::str::FromStr;
12use std::time::Duration;
13
14use crate::ThreadId;
15use crate::approvals::ElicitationRequestEvent;
16use crate::config_types::CollaborationMode;
17use crate::config_types::ModeKind;
18use crate::config_types::Personality;
19use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;
20use crate::config_types::WindowsSandboxLevel;
21use crate::custom_prompts::CustomPrompt;
22use crate::dynamic_tools::DynamicToolCallRequest;
23use crate::dynamic_tools::DynamicToolResponse;
24use crate::dynamic_tools::DynamicToolSpec;
25use crate::items::TurnItem;
26use crate::mcp::CallToolResult;
27use crate::mcp::RequestId;
28use crate::mcp::Resource as McpResource;
29use crate::mcp::ResourceTemplate as McpResourceTemplate;
30use crate::mcp::Tool as McpTool;
31use crate::message_history::HistoryEntry;
32use crate::models::BaseInstructions;
33use crate::models::ContentItem;
34use crate::models::ResponseItem;
35use crate::models::WebSearchAction;
36use crate::num_format::format_with_separators;
37use crate::openai_models::ReasoningEffort as ReasoningEffortConfig;
38use crate::parse_command::ParsedCommand;
39use crate::plan_tool::UpdatePlanArgs;
40use crate::request_user_input::RequestUserInputResponse;
41use crate::user_input::UserInput;
42use hanzo_utils_absolute_path::AbsolutePathBuf;
43use schemars::JsonSchema;
44use serde::Deserialize;
45use serde::Serialize;
46use serde_json::Value;
47use serde_with::serde_as;
48use strum_macros::Display;
49use tracing::error;
50use ts_rs::TS;
51
52pub use crate::approvals::ApplyPatchApprovalRequestEvent;
53pub use crate::approvals::ElicitationAction;
54pub use crate::approvals::ExecApprovalRequestEvent;
55pub use crate::approvals::ExecPolicyAmendment;
56pub use crate::approvals::NetworkApprovalContext;
57pub use crate::approvals::NetworkApprovalProtocol;
58pub use crate::mcp_protocol::InputItem;
59pub use crate::request_user_input::RequestUserInputEvent;
60
61/// Open/close tags for special user-input blocks. Used across crates to avoid
62/// duplicated hardcoded strings.
63pub const USER_INSTRUCTIONS_OPEN_TAG: &str = "<user_instructions>";
64pub const USER_INSTRUCTIONS_CLOSE_TAG: &str = "</user_instructions>";
65pub const ENVIRONMENT_CONTEXT_OPEN_TAG: &str = "<environment_context>";
66pub const ENVIRONMENT_CONTEXT_CLOSE_TAG: &str = "</environment_context>";
67pub const ENVIRONMENT_CONTEXT_DELTA_OPEN_TAG: &str = "<environment_context_delta>";
68pub const ENVIRONMENT_CONTEXT_DELTA_CLOSE_TAG: &str = "</environment_context_delta>";
69pub const BROWSER_SNAPSHOT_OPEN_TAG: &str = "<browser_snapshot>";
70pub const BROWSER_SNAPSHOT_CLOSE_TAG: &str = "</browser_snapshot>";
71pub const COLLABORATION_MODE_OPEN_TAG: &str = "<collaboration_mode>";
72pub const COLLABORATION_MODE_CLOSE_TAG: &str = "</collaboration_mode>";
73pub const USER_MESSAGE_BEGIN: &str = "## My request for Codex:";
74
75/// Submission Queue Entry - requests from user
76#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
77pub struct Submission {
78    /// Unique id for this Submission to correlate with Events
79    pub id: String,
80    /// Payload
81    pub op: Op,
82}
83
84/// Config payload for refreshing MCP servers.
85#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema)]
86pub struct McpServerRefreshConfig {
87    pub mcp_servers: Value,
88    pub mcp_oauth_credentials_store_mode: Value,
89}
90
91/// Submission operation
92#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema)]
93#[serde(tag = "type", rename_all = "snake_case")]
94#[allow(clippy::large_enum_variant)]
95#[non_exhaustive]
96pub enum Op {
97    /// Abort current task.
98    /// This server sends [`EventMsg::TurnAborted`] in response.
99    Interrupt,
100
101    /// Terminate all running background terminal processes for this thread.
102    CleanBackgroundTerminals,
103
104    /// Legacy user input.
105    ///
106    /// Prefer [`Op::UserTurn`] so the caller provides full turn context
107    /// (cwd/approval/sandbox/model/etc.) for each turn.
108    UserInput {
109        /// User input items, see `InputItem`
110        items: Vec<UserInput>,
111        /// Optional JSON Schema used to constrain the final assistant message for this turn.
112        #[serde(skip_serializing_if = "Option::is_none")]
113        final_output_json_schema: Option<Value>,
114    },
115
116    /// Similar to [`Op::UserInput`], but contains additional context required
117    /// for a turn of a [`crate::codex_thread::CodexThread`].
118    UserTurn {
119        /// User input items, see `InputItem`
120        items: Vec<UserInput>,
121
122        /// `cwd` to use with the [`SandboxPolicy`] and potentially tool calls
123        /// such as `local_shell`.
124        cwd: PathBuf,
125
126        /// Policy to use for command approval.
127        approval_policy: AskForApproval,
128
129        /// Policy to use for tool calls such as `local_shell`.
130        sandbox_policy: SandboxPolicy,
131
132        /// Must be a valid model slug for the configured client session
133        /// associated with this conversation.
134        model: String,
135
136        /// Will only be honored if the model is configured to use reasoning.
137        #[serde(skip_serializing_if = "Option::is_none")]
138        effort: Option<ReasoningEffortConfig>,
139
140        /// Will only be honored if the model is configured to use reasoning.
141        summary: ReasoningSummaryConfig,
142        // The JSON schema to use for the final assistant message
143        final_output_json_schema: Option<Value>,
144
145        /// EXPERIMENTAL - set a pre-set collaboration mode.
146        /// Takes precedence over model, effort, and developer instructions if set.
147        #[serde(skip_serializing_if = "Option::is_none")]
148        collaboration_mode: Option<CollaborationMode>,
149
150        /// Optional personality override for this turn.
151        #[serde(skip_serializing_if = "Option::is_none")]
152        personality: Option<Personality>,
153    },
154
155    /// Override parts of the persistent turn context for subsequent turns.
156    ///
157    /// All fields are optional; when omitted, the existing value is preserved.
158    /// This does not enqueue any input – it only updates defaults used for
159    /// turns that rely on persistent session-level context (for example,
160    /// [`Op::UserInput`]).
161    OverrideTurnContext {
162        /// Updated `cwd` for sandbox/tool calls.
163        #[serde(skip_serializing_if = "Option::is_none")]
164        cwd: Option<PathBuf>,
165
166        /// Updated command approval policy.
167        #[serde(skip_serializing_if = "Option::is_none")]
168        approval_policy: Option<AskForApproval>,
169
170        /// Updated sandbox policy for tool calls.
171        #[serde(skip_serializing_if = "Option::is_none")]
172        sandbox_policy: Option<SandboxPolicy>,
173
174        /// Updated Windows sandbox mode for tool execution.
175        #[serde(skip_serializing_if = "Option::is_none")]
176        windows_sandbox_level: Option<WindowsSandboxLevel>,
177
178        /// Updated model slug. When set, the model info is derived
179        /// automatically.
180        #[serde(skip_serializing_if = "Option::is_none")]
181        model: Option<String>,
182
183        /// Updated reasoning effort (honored only for reasoning-capable models).
184        ///
185        /// Use `Some(Some(_))` to set a specific effort, `Some(None)` to clear
186        /// the effort, or `None` to leave the existing value unchanged.
187        #[serde(skip_serializing_if = "Option::is_none")]
188        effort: Option<Option<ReasoningEffortConfig>>,
189
190        /// Updated reasoning summary preference (honored only for reasoning-capable models).
191        #[serde(skip_serializing_if = "Option::is_none")]
192        summary: Option<ReasoningSummaryConfig>,
193
194        /// EXPERIMENTAL - set a pre-set collaboration mode.
195        /// Takes precedence over model, effort, and developer instructions if set.
196        #[serde(skip_serializing_if = "Option::is_none")]
197        collaboration_mode: Option<CollaborationMode>,
198
199        /// Updated personality preference.
200        #[serde(skip_serializing_if = "Option::is_none")]
201        personality: Option<Personality>,
202    },
203
204    /// Approve a command execution
205    ExecApproval {
206        /// The id of the submission we are approving
207        id: String,
208        /// Turn id associated with the approval event, when available.
209        #[serde(default, skip_serializing_if = "Option::is_none")]
210        turn_id: Option<String>,
211        /// The user's decision in response to the request.
212        decision: ReviewDecision,
213    },
214
215    /// Approve a code patch
216    PatchApproval {
217        /// The id of the submission we are approving
218        id: String,
219        /// The user's decision in response to the request.
220        decision: ReviewDecision,
221    },
222
223    /// Resolve an MCP elicitation request.
224    ResolveElicitation {
225        /// Name of the MCP server that issued the request.
226        server_name: String,
227        /// Request identifier from the MCP server.
228        request_id: RequestId,
229        /// User's decision for the request.
230        decision: ElicitationAction,
231    },
232
233    /// Resolve a request_user_input tool call.
234    #[serde(rename = "user_input_answer", alias = "request_user_input_response")]
235    UserInputAnswer {
236        /// Turn id for the in-flight request.
237        id: String,
238        /// User-provided answers.
239        response: RequestUserInputResponse,
240    },
241
242    /// Resolve a dynamic tool call request.
243    DynamicToolResponse {
244        /// Call id for the in-flight request.
245        id: String,
246        /// Tool output payload.
247        response: DynamicToolResponse,
248    },
249
250    /// Append an entry to the persistent cross-session message history.
251    ///
252    /// Note the entry is not guaranteed to be logged if the user has
253    /// history disabled, it matches the list of "sensitive" patterns, etc.
254    AddToHistory {
255        /// The message text to be stored.
256        text: String,
257    },
258
259    /// Request a single history entry identified by `log_id` + `offset`.
260    GetHistoryEntryRequest { offset: usize, log_id: u64 },
261
262    /// Request the list of MCP tools available across all configured servers.
263    /// Reply is delivered via `EventMsg::McpListToolsResponse`.
264    ListMcpTools,
265
266    /// Request MCP servers to reinitialize and refresh cached tool lists.
267    RefreshMcpServers { config: McpServerRefreshConfig },
268
269    /// Request the list of available custom prompts.
270    ListCustomPrompts,
271
272    /// Request the list of skills for the provided `cwd` values or the session default.
273    ListSkills {
274        /// Working directories to scope repo skills discovery.
275        ///
276        /// When empty, the session default working directory is used.
277        #[serde(default, skip_serializing_if = "Vec::is_empty")]
278        cwds: Vec<PathBuf>,
279
280        /// When true, recompute skills even if a cached result exists.
281        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
282        force_reload: bool,
283    },
284
285    /// Request the list of remote skills available via ChatGPT sharing.
286    ListRemoteSkills,
287
288    /// Download a remote skill by id into the local skills cache.
289    DownloadRemoteSkill {
290        hazelnut_id: String,
291        is_preload: bool,
292    },
293
294    /// Request the agent to summarize the current conversation context.
295    /// The agent will use its existing context (either conversation history or previous response id)
296    /// to generate a summary which will be returned as an AgentMessage event.
297    Compact,
298
299    /// Set a user-facing thread name in the persisted rollout metadata.
300    /// This is a local-only operation handled by codex-core; it does not
301    /// involve the model.
302    SetThreadName { name: String },
303
304    /// Request Codex to undo a turn (turn are stacked so it is the same effect as CMD + Z).
305    Undo,
306
307    /// Request Codex to drop the last N user turns from in-memory context.
308    ///
309    /// This does not attempt to revert local filesystem changes. Clients are
310    /// responsible for undoing any edits on disk.
311    ThreadRollback { num_turns: u32 },
312
313    /// Request a code review from the agent.
314    Review { review_request: ReviewRequest },
315
316    /// Request to shut down codex instance.
317    Shutdown,
318
319    /// Execute a user-initiated one-off shell command (triggered by "!cmd").
320    ///
321    /// The command string is executed using the user's default shell and may
322    /// include shell syntax (pipes, redirects, etc.). Output is streamed via
323    /// `ExecCommand*` events and the UI regains control upon `TurnComplete`.
324    RunUserShellCommand {
325        /// The raw command string after '!'
326        command: String,
327    },
328
329    /// Request the list of available models.
330    ListModels,
331}
332
333/// Determines the conditions under which the user is consulted to approve
334/// running the command proposed by Codex.
335#[derive(
336    Debug,
337    Clone,
338    Copy,
339    Default,
340    PartialEq,
341    Eq,
342    Hash,
343    Serialize,
344    Deserialize,
345    Display,
346    JsonSchema,
347    TS,
348)]
349#[serde(rename_all = "kebab-case")]
350#[strum(serialize_all = "kebab-case")]
351pub enum AskForApproval {
352    /// Under this policy, only "known safe" commands—as determined by
353    /// `is_safe_command()`—that **only read files** are auto‑approved.
354    /// Everything else will ask the user to approve.
355    #[serde(rename = "untrusted")]
356    #[strum(serialize = "untrusted")]
357    UnlessTrusted,
358
359    /// DEPRECATED: *All* commands are auto‑approved, but they are expected to
360    /// run inside a sandbox where network access is disabled and writes are
361    /// confined to a specific set of paths. If the command fails, it will be
362    /// escalated to the user to approve execution without a sandbox.
363    /// Prefer `OnRequest` for interactive runs or `Never` for non-interactive
364    /// runs.
365    OnFailure,
366
367    /// The model decides when to ask the user for approval.
368    #[default]
369    OnRequest,
370
371    /// Fine-grained rejection controls for approval prompts.
372    ///
373    /// When a field is `true`, prompts of that category are automatically
374    /// rejected instead of shown to the user.
375    Reject(RejectConfig),
376
377    /// Never ask the user to approve commands. Failures are immediately returned
378    /// to the model, and never escalated to the user for approval.
379    Never,
380}
381
382#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)]
383pub struct RejectConfig {
384    /// Reject approval prompts related to sandbox escalation.
385    pub sandbox_approval: bool,
386    /// Reject prompts triggered by execpolicy `prompt` rules.
387    pub rules: bool,
388    /// Reject MCP elicitation prompts.
389    pub mcp_elicitations: bool,
390}
391
392impl RejectConfig {
393    pub const fn rejects_sandbox_approval(self) -> bool {
394        self.sandbox_approval
395    }
396
397    pub const fn rejects_rules_approval(self) -> bool {
398        self.rules
399    }
400
401    pub const fn rejects_mcp_elicitations(self) -> bool {
402        self.mcp_elicitations
403    }
404}
405
406/// Represents whether outbound network access is available to the agent.
407#[derive(
408    Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, Default, JsonSchema, TS,
409)]
410#[serde(rename_all = "kebab-case")]
411#[strum(serialize_all = "kebab-case")]
412pub enum NetworkAccess {
413    #[default]
414    Restricted,
415    Enabled,
416}
417
418impl NetworkAccess {
419    pub fn is_enabled(self) -> bool {
420        matches!(self, NetworkAccess::Enabled)
421    }
422}
423
424/// Determines execution restrictions for model shell commands.
425#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, JsonSchema, TS)]
426#[strum(serialize_all = "kebab-case")]
427#[serde(tag = "type", rename_all = "kebab-case")]
428pub enum SandboxPolicy {
429    /// No restrictions whatsoever. Use with caution.
430    #[serde(rename = "danger-full-access")]
431    DangerFullAccess,
432
433    /// Read-only access to the entire file-system.
434    #[serde(rename = "read-only")]
435    ReadOnly,
436
437    /// Indicates the process is already in an external sandbox. Allows full
438    /// disk access while honoring the provided network setting.
439    #[serde(rename = "external-sandbox")]
440    ExternalSandbox {
441        /// Whether the external sandbox permits outbound network traffic.
442        #[serde(default)]
443        network_access: NetworkAccess,
444    },
445
446    /// Same as `ReadOnly` but additionally grants write access to the current
447    /// working directory ("workspace").
448    #[serde(rename = "workspace-write")]
449    WorkspaceWrite {
450        /// Additional folders (beyond cwd and possibly TMPDIR) that should be
451        /// writable from within the sandbox.
452        #[serde(default, skip_serializing_if = "Vec::is_empty")]
453        writable_roots: Vec<AbsolutePathBuf>,
454
455        /// When set to `true`, outbound network access is allowed. `false` by
456        /// default.
457        #[serde(default)]
458        network_access: bool,
459
460        /// When set to `true`, will NOT include the per-user `TMPDIR`
461        /// environment variable among the default writable roots. Defaults to
462        /// `false`.
463        #[serde(default)]
464        exclude_tmpdir_env_var: bool,
465
466        /// When set to `true`, will NOT include the `/tmp` among the default
467        /// writable roots on UNIX. Defaults to `false`.
468        #[serde(default)]
469        exclude_slash_tmp: bool,
470
471        /// Whether sandboxed commands may perform write operations via Git.
472        #[serde(default)]
473        allow_git_writes: bool,
474    },
475}
476
477/// A writable root path accompanied by a list of subpaths that should remain
478/// read‑only even when the root is writable. This is primarily used to ensure
479/// that folders containing files that could be modified to escalate the
480/// privileges of the agent (e.g. `.codex`, `.git`, notably `.git/hooks`) under
481/// a writable root are not modified by the agent.
482#[derive(Debug, Clone, PartialEq, Eq, JsonSchema)]
483pub struct WritableRoot {
484    pub root: AbsolutePathBuf,
485
486    /// By construction, these subpaths are all under `root`.
487    pub read_only_subpaths: Vec<AbsolutePathBuf>,
488}
489
490impl WritableRoot {
491    pub fn is_path_writable(&self, path: &Path) -> bool {
492        // Check if the path is under the root.
493        if !path.starts_with(&self.root) {
494            return false;
495        }
496
497        // Check if the path is under any of the read-only subpaths.
498        for subpath in &self.read_only_subpaths {
499            if path.starts_with(subpath) {
500                return false;
501            }
502        }
503
504        true
505    }
506}
507
508impl FromStr for SandboxPolicy {
509    type Err = serde_json::Error;
510
511    fn from_str(s: &str) -> Result<Self, Self::Err> {
512        serde_json::from_str(s)
513    }
514}
515
516impl SandboxPolicy {
517    /// Returns a policy with read-only disk access and no network.
518    pub fn new_read_only_policy() -> Self {
519        SandboxPolicy::ReadOnly
520    }
521
522    /// Returns a policy that can read the entire disk, but can only write to
523    /// the current working directory and the per-user tmp dir on macOS. It does
524    /// not allow network access.
525    pub fn new_workspace_write_policy() -> Self {
526        SandboxPolicy::WorkspaceWrite {
527            writable_roots: vec![],
528            network_access: false,
529            exclude_tmpdir_env_var: false,
530            exclude_slash_tmp: false,
531            allow_git_writes: false,
532        }
533    }
534
535    /// Always returns `true`; restricting read access is not supported.
536    pub fn has_full_disk_read_access(&self) -> bool {
537        true
538    }
539
540    pub fn has_full_disk_write_access(&self) -> bool {
541        match self {
542            SandboxPolicy::DangerFullAccess => true,
543            SandboxPolicy::ExternalSandbox { .. } => true,
544            SandboxPolicy::ReadOnly => false,
545            SandboxPolicy::WorkspaceWrite { .. } => false,
546        }
547    }
548
549    pub fn has_full_network_access(&self) -> bool {
550        match self {
551            SandboxPolicy::DangerFullAccess => true,
552            SandboxPolicy::ExternalSandbox { network_access } => network_access.is_enabled(),
553            SandboxPolicy::ReadOnly => false,
554            SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access,
555        }
556    }
557
558    /// Returns the list of writable roots (tailored to the current working
559    /// directory) together with subpaths that should remain read‑only under
560    /// each writable root.
561    pub fn get_writable_roots_with_cwd(&self, cwd: &Path) -> Vec<WritableRoot> {
562        match self {
563            SandboxPolicy::DangerFullAccess => Vec::new(),
564            SandboxPolicy::ExternalSandbox { .. } => Vec::new(),
565            SandboxPolicy::ReadOnly => Vec::new(),
566            SandboxPolicy::WorkspaceWrite {
567                writable_roots,
568                exclude_tmpdir_env_var,
569                exclude_slash_tmp,
570                network_access: _,
571                allow_git_writes: _,
572            } => {
573                // Start from explicitly configured writable roots.
574                let mut roots: Vec<AbsolutePathBuf> = writable_roots.clone();
575
576                // Always include defaults: cwd, /tmp (if present on Unix), and
577                // on macOS, the per-user TMPDIR unless explicitly excluded.
578                // TODO(mbolin): cwd param should be AbsolutePathBuf.
579                let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd);
580                match cwd_absolute {
581                    Ok(cwd) => {
582                        roots.push(cwd);
583                    }
584                    Err(e) => {
585                        error!(
586                            "Ignoring invalid cwd {:?} for sandbox writable root: {}",
587                            cwd, e
588                        );
589                    }
590                }
591
592                // Include /tmp on Unix unless explicitly excluded.
593                if cfg!(unix) && !exclude_slash_tmp {
594                    #[allow(clippy::expect_used)]
595                    let slash_tmp =
596                        AbsolutePathBuf::from_absolute_path("/tmp").expect("/tmp is absolute");
597                    if slash_tmp.as_path().is_dir() {
598                        roots.push(slash_tmp);
599                    }
600                }
601
602                // Include $TMPDIR unless explicitly excluded. On macOS, TMPDIR
603                // is per-user, so writes to TMPDIR should not be readable by
604                // other users on the system.
605                //
606                // By comparison, TMPDIR is not guaranteed to be defined on
607                // Linux or Windows, but supporting it here gives users a way to
608                // provide the model with their own temporary directory without
609                // having to hardcode it in the config.
610                if !exclude_tmpdir_env_var
611                    && let Some(tmpdir) = std::env::var_os("TMPDIR")
612                    && !tmpdir.is_empty()
613                {
614                    match AbsolutePathBuf::from_absolute_path(PathBuf::from(&tmpdir)) {
615                        Ok(tmpdir_path) => {
616                            roots.push(tmpdir_path);
617                        }
618                        Err(e) => {
619                            error!(
620                                "Ignoring invalid TMPDIR value {tmpdir:?} for sandbox writable root: {e}",
621                            );
622                        }
623                    }
624                }
625
626                // For each root, compute subpaths that should remain read-only.
627                roots
628                    .into_iter()
629                    .map(|writable_root| {
630                        let mut subpaths: Vec<AbsolutePathBuf> = Vec::new();
631                        #[allow(clippy::expect_used)]
632                        let top_level_git = writable_root
633                            .join(".git")
634                            .expect(".git is a valid relative path");
635                        // This applies to typical repos (directory .git), worktrees/submodules
636                        // (file .git with gitdir pointer), and bare repos when the gitdir is the
637                        // writable root itself.
638                        let top_level_git_is_file = top_level_git.as_path().is_file();
639                        let top_level_git_is_dir = top_level_git.as_path().is_dir();
640                        if top_level_git_is_dir || top_level_git_is_file {
641                            if top_level_git_is_file
642                                && is_git_pointer_file(&top_level_git)
643                                && let Some(gitdir) = resolve_gitdir_from_file(&top_level_git)
644                                && !subpaths
645                                    .iter()
646                                    .any(|subpath| subpath.as_path() == gitdir.as_path())
647                            {
648                                subpaths.push(gitdir);
649                            }
650                            subpaths.push(top_level_git);
651                        }
652
653                        // Make .agents/skills and .codex/config.toml and
654                        // related files read-only to the agent, by default.
655                        for subdir in &[".agents", ".codex"] {
656                            #[allow(clippy::expect_used)]
657                            let top_level_codex =
658                                writable_root.join(subdir).expect("valid relative path");
659                            if top_level_codex.as_path().is_dir() {
660                                subpaths.push(top_level_codex);
661                            }
662                        }
663
664                        WritableRoot {
665                            root: writable_root,
666                            read_only_subpaths: subpaths,
667                        }
668                    })
669                    .collect()
670            }
671        }
672    }
673}
674
675fn is_git_pointer_file(path: &AbsolutePathBuf) -> bool {
676    path.as_path().is_file() && path.as_path().file_name() == Some(OsStr::new(".git"))
677}
678
679fn resolve_gitdir_from_file(dot_git: &AbsolutePathBuf) -> Option<AbsolutePathBuf> {
680    let contents = match std::fs::read_to_string(dot_git.as_path()) {
681        Ok(contents) => contents,
682        Err(err) => {
683            error!(
684                "Failed to read {path} for gitdir pointer: {err}",
685                path = dot_git.as_path().display()
686            );
687            return None;
688        }
689    };
690
691    let trimmed = contents.trim();
692    let (_, gitdir_raw) = match trimmed.split_once(':') {
693        Some(parts) => parts,
694        None => {
695            error!(
696                "Expected {path} to contain a gitdir pointer, but it did not match `gitdir: <path>`.",
697                path = dot_git.as_path().display()
698            );
699            return None;
700        }
701    };
702    let gitdir_raw = gitdir_raw.trim();
703    if gitdir_raw.is_empty() {
704        error!(
705            "Expected {path} to contain a gitdir pointer, but it was empty.",
706            path = dot_git.as_path().display()
707        );
708        return None;
709    }
710    let base = match dot_git.as_path().parent() {
711        Some(base) => base,
712        None => {
713            error!(
714                "Unable to resolve parent directory for {path}.",
715                path = dot_git.as_path().display()
716            );
717            return None;
718        }
719    };
720    let gitdir_path = match AbsolutePathBuf::resolve_path_against_base(gitdir_raw, base) {
721        Ok(path) => path,
722        Err(err) => {
723            error!(
724                "Failed to resolve gitdir path {gitdir_raw} from {path}: {err}",
725                path = dot_git.as_path().display()
726            );
727            return None;
728        }
729    };
730    if !gitdir_path.as_path().exists() {
731        error!(
732            "Resolved gitdir path {path} does not exist.",
733            path = gitdir_path.as_path().display()
734        );
735        return None;
736    }
737    Some(gitdir_path)
738}
739
740/// Event Queue Entry - events from agent
741#[derive(Debug, Clone, Deserialize, Serialize)]
742pub struct Event {
743    /// Submission `id` that this event is correlated with.
744    pub id: String,
745    /// Payload
746    pub msg: EventMsg,
747}
748
749/// Response event from the agent
750/// NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.
751#[derive(Debug, Clone, Deserialize, Serialize, Display, JsonSchema, TS)]
752#[serde(tag = "type", rename_all = "snake_case")]
753#[ts(tag = "type")]
754#[strum(serialize_all = "snake_case")]
755pub enum EventMsg {
756    /// Error while executing a submission
757    Error(ErrorEvent),
758
759    /// Warning issued while processing a submission. Unlike `Error`, this
760    /// indicates the turn continued but the user should still be notified.
761    Warning(WarningEvent),
762
763    /// Conversation history was compacted (either automatically or manually).
764    ContextCompacted(ContextCompactedEvent),
765
766    /// Conversation history was rolled back by dropping the last N user turns.
767    ThreadRolledBack(ThreadRolledBackEvent),
768
769    /// Agent has started a turn.
770    /// v1 wire format uses `task_started`; accept `turn_started` for v2 interop.
771    #[serde(rename = "task_started", alias = "turn_started")]
772    TurnStarted(TurnStartedEvent),
773
774    /// Agent has completed all actions.
775    /// v1 wire format uses `task_complete`; accept `turn_complete` for v2 interop.
776    #[serde(rename = "task_complete", alias = "turn_complete")]
777    TurnComplete(TurnCompleteEvent),
778
779    /// Usage update for the current session, including totals and last turn.
780    /// Optional means unknown — UIs should not display when `None`.
781    TokenCount(TokenCountEvent),
782
783    /// Auto Context is evaluating whether to compact before the next turn.
784    AutoContextCheck(AutoContextCheckEvent),
785
786    /// Agent text output message
787    AgentMessage(AgentMessageEvent),
788
789    /// User/system input message (what was sent to the model)
790    UserMessage(UserMessageEvent),
791
792    /// Agent text output delta message
793    AgentMessageDelta(AgentMessageDeltaEvent),
794
795    /// Reasoning event from agent.
796    AgentReasoning(AgentReasoningEvent),
797
798    /// Agent reasoning delta event from agent.
799    AgentReasoningDelta(AgentReasoningDeltaEvent),
800
801    /// Raw chain-of-thought from agent.
802    AgentReasoningRawContent(AgentReasoningRawContentEvent),
803
804    /// Agent reasoning content delta event from agent.
805    AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent),
806    /// Signaled when the model begins a new reasoning summary section (e.g., a new titled block).
807    AgentReasoningSectionBreak(AgentReasoningSectionBreakEvent),
808
809    /// Ack the client's configure message.
810    SessionConfigured(SessionConfiguredEvent),
811
812    /// Updated session metadata (e.g., thread name changes).
813    ThreadNameUpdated(ThreadNameUpdatedEvent),
814
815    /// Incremental MCP startup progress updates.
816    McpStartupUpdate(McpStartupUpdateEvent),
817
818    /// Aggregate MCP startup completion summary.
819    McpStartupComplete(McpStartupCompleteEvent),
820
821    McpToolCallBegin(McpToolCallBeginEvent),
822
823    McpToolCallEnd(McpToolCallEndEvent),
824
825    WebSearchBegin(WebSearchBeginEvent),
826
827    WebSearchEnd(WebSearchEndEvent),
828
829    /// Notification that the server is about to execute a command.
830    ExecCommandBegin(ExecCommandBeginEvent),
831
832    /// Incremental chunk of output from a running command.
833    ExecCommandOutputDelta(ExecCommandOutputDeltaEvent),
834
835    /// Terminal interaction for an in-progress command (stdin sent and stdout observed).
836    TerminalInteraction(TerminalInteractionEvent),
837
838    ExecCommandEnd(ExecCommandEndEvent),
839
840    /// Notification that the agent attached a local image via the view_image tool.
841    ViewImageToolCall(ViewImageToolCallEvent),
842
843    ExecApprovalRequest(ExecApprovalRequestEvent),
844
845    RequestUserInput(RequestUserInputEvent),
846
847    DynamicToolCallRequest(DynamicToolCallRequest),
848
849    ElicitationRequest(ElicitationRequestEvent),
850
851    ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent),
852
853    /// Notification advising the user that something they are using has been
854    /// deprecated and should be phased out.
855    DeprecationNotice(DeprecationNoticeEvent),
856
857    BackgroundEvent(BackgroundEventEvent),
858
859    UndoStarted(UndoStartedEvent),
860
861    UndoCompleted(UndoCompletedEvent),
862
863    /// Notification that a model stream experienced an error or disconnect
864    /// and the system is handling it (e.g., retrying with backoff).
865    StreamError(StreamErrorEvent),
866
867    /// Notification that the agent is about to apply a code patch. Mirrors
868    /// `ExecCommandBegin` so front‑ends can show progress indicators.
869    PatchApplyBegin(PatchApplyBeginEvent),
870
871    /// Notification that a patch application has finished.
872    PatchApplyEnd(PatchApplyEndEvent),
873
874    TurnDiff(TurnDiffEvent),
875
876    /// Response to GetHistoryEntryRequest.
877    GetHistoryEntryResponse(GetHistoryEntryResponseEvent),
878
879    /// List of MCP tools available to the agent.
880    McpListToolsResponse(McpListToolsResponseEvent),
881
882    /// List of custom prompts available to the agent.
883    ListCustomPromptsResponse(ListCustomPromptsResponseEvent),
884
885    /// List of skills available to the agent.
886    ListSkillsResponse(ListSkillsResponseEvent),
887
888    /// List of remote skills available to the agent.
889    ListRemoteSkillsResponse(ListRemoteSkillsResponseEvent),
890
891    /// Remote skill downloaded to local cache.
892    RemoteSkillDownloaded(RemoteSkillDownloadedEvent),
893
894    /// Notification that skill data may have been updated and clients may want to reload.
895    SkillsUpdateAvailable,
896
897    PlanUpdate(UpdatePlanArgs),
898
899    TurnAborted(TurnAbortedEvent),
900
901    /// Notification that the agent is shutting down.
902    ShutdownComplete,
903
904    /// Entered review mode.
905    EnteredReviewMode(ReviewRequest),
906
907    /// Exited review mode with an optional final result to apply.
908    ExitedReviewMode(ExitedReviewModeEvent),
909
910    RawResponseItem(RawResponseItemEvent),
911
912    ItemStarted(ItemStartedEvent),
913    ItemCompleted(ItemCompletedEvent),
914
915    AgentMessageContentDelta(AgentMessageContentDeltaEvent),
916    PlanDelta(PlanDeltaEvent),
917    ReasoningContentDelta(ReasoningContentDeltaEvent),
918    ReasoningRawContentDelta(ReasoningRawContentDeltaEvent),
919
920    /// Collab interaction: agent spawn begin.
921    CollabAgentSpawnBegin(CollabAgentSpawnBeginEvent),
922    /// Collab interaction: agent spawn end.
923    CollabAgentSpawnEnd(CollabAgentSpawnEndEvent),
924    /// Collab interaction: agent interaction begin.
925    CollabAgentInteractionBegin(CollabAgentInteractionBeginEvent),
926    /// Collab interaction: agent interaction end.
927    CollabAgentInteractionEnd(CollabAgentInteractionEndEvent),
928    /// Collab interaction: waiting begin.
929    CollabWaitingBegin(CollabWaitingBeginEvent),
930    /// Collab interaction: waiting end.
931    CollabWaitingEnd(CollabWaitingEndEvent),
932    /// Collab interaction: close begin.
933    CollabCloseBegin(CollabCloseBeginEvent),
934    /// Collab interaction: close end.
935    CollabCloseEnd(CollabCloseEndEvent),
936    /// Collab interaction: resume begin.
937    CollabResumeBegin(CollabResumeBeginEvent),
938    /// Collab interaction: resume end.
939    CollabResumeEnd(CollabResumeEndEvent),
940}
941
942impl From<CollabAgentSpawnBeginEvent> for EventMsg {
943    fn from(event: CollabAgentSpawnBeginEvent) -> Self {
944        EventMsg::CollabAgentSpawnBegin(event)
945    }
946}
947
948impl From<CollabAgentSpawnEndEvent> for EventMsg {
949    fn from(event: CollabAgentSpawnEndEvent) -> Self {
950        EventMsg::CollabAgentSpawnEnd(event)
951    }
952}
953
954impl From<CollabAgentInteractionBeginEvent> for EventMsg {
955    fn from(event: CollabAgentInteractionBeginEvent) -> Self {
956        EventMsg::CollabAgentInteractionBegin(event)
957    }
958}
959
960impl From<CollabAgentInteractionEndEvent> for EventMsg {
961    fn from(event: CollabAgentInteractionEndEvent) -> Self {
962        EventMsg::CollabAgentInteractionEnd(event)
963    }
964}
965
966impl From<CollabWaitingBeginEvent> for EventMsg {
967    fn from(event: CollabWaitingBeginEvent) -> Self {
968        EventMsg::CollabWaitingBegin(event)
969    }
970}
971
972impl From<CollabWaitingEndEvent> for EventMsg {
973    fn from(event: CollabWaitingEndEvent) -> Self {
974        EventMsg::CollabWaitingEnd(event)
975    }
976}
977
978impl From<CollabCloseBeginEvent> for EventMsg {
979    fn from(event: CollabCloseBeginEvent) -> Self {
980        EventMsg::CollabCloseBegin(event)
981    }
982}
983
984impl From<CollabCloseEndEvent> for EventMsg {
985    fn from(event: CollabCloseEndEvent) -> Self {
986        EventMsg::CollabCloseEnd(event)
987    }
988}
989
990impl From<CollabResumeBeginEvent> for EventMsg {
991    fn from(event: CollabResumeBeginEvent) -> Self {
992        EventMsg::CollabResumeBegin(event)
993    }
994}
995
996impl From<CollabResumeEndEvent> for EventMsg {
997    fn from(event: CollabResumeEndEvent) -> Self {
998        EventMsg::CollabResumeEnd(event)
999    }
1000}
1001
1002/// Agent lifecycle status, derived from emitted events.
1003#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS, Default)]
1004#[serde(rename_all = "snake_case")]
1005#[ts(rename_all = "snake_case")]
1006pub enum AgentStatus {
1007    /// Agent is waiting for initialization.
1008    #[default]
1009    PendingInit,
1010    /// Agent is currently running.
1011    Running,
1012    /// Agent is done. Contains the final assistant message.
1013    Completed(Option<String>),
1014    /// Agent encountered an error.
1015    Errored(String),
1016    /// Agent has been shutdown.
1017    Shutdown,
1018    /// Agent is not found.
1019    NotFound,
1020}
1021
1022/// Codex errors that we expose to clients.
1023#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
1024#[serde(rename_all = "snake_case")]
1025#[ts(rename_all = "snake_case")]
1026pub enum CodexErrorInfo {
1027    ContextWindowExceeded,
1028    UsageLimitExceeded,
1029    ModelCap {
1030        model: String,
1031        reset_after_seconds: Option<u64>,
1032    },
1033    HttpConnectionFailed {
1034        http_status_code: Option<u16>,
1035    },
1036    /// Failed to connect to the response SSE stream.
1037    ResponseStreamConnectionFailed {
1038        http_status_code: Option<u16>,
1039    },
1040    InternalServerError,
1041    Unauthorized,
1042    BadRequest,
1043    SandboxError,
1044    /// The response SSE stream disconnected in the middle of a turnbefore completion.
1045    ResponseStreamDisconnected {
1046        http_status_code: Option<u16>,
1047    },
1048    /// Reached the retry limit for responses.
1049    ResponseTooManyFailedAttempts {
1050        http_status_code: Option<u16>,
1051    },
1052    ThreadRollbackFailed,
1053    Other,
1054}
1055
1056#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1057pub struct RawResponseItemEvent {
1058    pub item: ResponseItem,
1059}
1060
1061#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1062pub struct ItemStartedEvent {
1063    pub thread_id: ThreadId,
1064    pub turn_id: String,
1065    pub item: TurnItem,
1066}
1067
1068impl HasLegacyEvent for ItemStartedEvent {
1069    fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
1070        match &self.item {
1071            TurnItem::WebSearch(item) => vec![EventMsg::WebSearchBegin(WebSearchBeginEvent {
1072                call_id: item.id.clone(),
1073            })],
1074            _ => Vec::new(),
1075        }
1076    }
1077}
1078
1079#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1080pub struct ItemCompletedEvent {
1081    pub thread_id: ThreadId,
1082    pub turn_id: String,
1083    pub item: TurnItem,
1084}
1085
1086pub trait HasLegacyEvent {
1087    fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg>;
1088}
1089
1090impl HasLegacyEvent for ItemCompletedEvent {
1091    fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg> {
1092        self.item.as_legacy_events(show_raw_agent_reasoning)
1093    }
1094}
1095
1096#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1097pub struct AgentMessageContentDeltaEvent {
1098    pub thread_id: String,
1099    pub turn_id: String,
1100    pub item_id: String,
1101    pub delta: String,
1102}
1103
1104impl HasLegacyEvent for AgentMessageContentDeltaEvent {
1105    fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
1106        vec![EventMsg::AgentMessageDelta(AgentMessageDeltaEvent {
1107            delta: self.delta.clone(),
1108        })]
1109    }
1110}
1111
1112#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1113pub struct PlanDeltaEvent {
1114    pub thread_id: String,
1115    pub turn_id: String,
1116    pub item_id: String,
1117    pub delta: String,
1118}
1119
1120#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1121pub struct ReasoningContentDeltaEvent {
1122    pub thread_id: String,
1123    pub turn_id: String,
1124    pub item_id: String,
1125    pub delta: String,
1126    // load with default value so it's backward compatible with the old format.
1127    #[serde(default)]
1128    pub summary_index: i64,
1129}
1130
1131impl HasLegacyEvent for ReasoningContentDeltaEvent {
1132    fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
1133        vec![EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent {
1134            delta: self.delta.clone(),
1135        })]
1136    }
1137}
1138
1139#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1140pub struct ReasoningRawContentDeltaEvent {
1141    pub thread_id: String,
1142    pub turn_id: String,
1143    pub item_id: String,
1144    pub delta: String,
1145    // load with default value so it's backward compatible with the old format.
1146    #[serde(default)]
1147    pub content_index: i64,
1148}
1149
1150impl HasLegacyEvent for ReasoningRawContentDeltaEvent {
1151    fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
1152        vec![EventMsg::AgentReasoningRawContentDelta(
1153            AgentReasoningRawContentDeltaEvent {
1154                delta: self.delta.clone(),
1155            },
1156        )]
1157    }
1158}
1159
1160impl HasLegacyEvent for EventMsg {
1161    fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg> {
1162        match self {
1163            EventMsg::ItemStarted(event) => event.as_legacy_events(show_raw_agent_reasoning),
1164            EventMsg::ItemCompleted(event) => event.as_legacy_events(show_raw_agent_reasoning),
1165            EventMsg::AgentMessageContentDelta(event) => {
1166                event.as_legacy_events(show_raw_agent_reasoning)
1167            }
1168            EventMsg::ReasoningContentDelta(event) => {
1169                event.as_legacy_events(show_raw_agent_reasoning)
1170            }
1171            EventMsg::ReasoningRawContentDelta(event) => {
1172                event.as_legacy_events(show_raw_agent_reasoning)
1173            }
1174            _ => Vec::new(),
1175        }
1176    }
1177}
1178
1179#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1180pub struct ExitedReviewModeEvent {
1181    pub review_output: Option<ReviewOutputEvent>,
1182    #[ts(optional)]
1183    #[serde(default, skip_serializing_if = "Option::is_none")]
1184    pub snapshot: Option<ReviewSnapshotInfo>,
1185}
1186
1187// Individual event payload types matching each `EventMsg` variant.
1188
1189#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1190pub struct ErrorEvent {
1191    pub message: String,
1192    #[serde(default)]
1193    pub codex_error_info: Option<CodexErrorInfo>,
1194}
1195
1196#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1197pub struct WarningEvent {
1198    pub message: String,
1199}
1200
1201#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1202pub struct ContextCompactedEvent;
1203
1204#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1205pub struct TurnCompleteEvent {
1206    pub last_agent_message: Option<String>,
1207}
1208
1209#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1210pub struct TurnStartedEvent {
1211    // TODO(aibrahim): make this not optional
1212    pub model_context_window: Option<i64>,
1213    #[serde(default)]
1214    pub collaboration_mode_kind: ModeKind,
1215}
1216
1217#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1218#[serde(rename_all = "snake_case")]
1219pub enum AutoContextPhase {
1220    Checking,
1221    Compacting,
1222}
1223
1224#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1225pub struct AutoContextCheckEvent {
1226    pub phase: Option<AutoContextPhase>,
1227}
1228
1229#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, TS)]
1230pub struct TokenUsage {
1231    #[ts(type = "number")]
1232    pub input_tokens: i64,
1233    #[ts(type = "number")]
1234    pub cached_input_tokens: i64,
1235    #[ts(type = "number")]
1236    pub output_tokens: i64,
1237    #[ts(type = "number")]
1238    pub reasoning_output_tokens: i64,
1239    #[ts(type = "number")]
1240    pub total_tokens: i64,
1241}
1242
1243#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1244pub struct TokenUsageInfo {
1245    pub total_token_usage: TokenUsage,
1246    pub last_token_usage: TokenUsage,
1247    #[ts(optional)]
1248    #[serde(default, skip_serializing_if = "Option::is_none")]
1249    pub requested_model: Option<String>,
1250    #[ts(optional)]
1251    #[serde(default, skip_serializing_if = "Option::is_none")]
1252    pub latest_response_model: Option<String>,
1253    // TODO(aibrahim): make this not optional
1254    #[ts(type = "number | null")]
1255    pub model_context_window: Option<i64>,
1256}
1257
1258impl TokenUsageInfo {
1259    pub fn new_or_append(
1260        info: &Option<TokenUsageInfo>,
1261        last: &Option<TokenUsage>,
1262        model_context_window: Option<i64>,
1263    ) -> Option<Self> {
1264        if info.is_none() && last.is_none() {
1265            return None;
1266        }
1267
1268        let mut info = match info {
1269            Some(info) => info.clone(),
1270            None => Self {
1271                total_token_usage: TokenUsage::default(),
1272                last_token_usage: TokenUsage::default(),
1273                requested_model: None,
1274                latest_response_model: None,
1275                model_context_window,
1276            },
1277        };
1278        if let Some(last) = last {
1279            info.append_last_usage(last);
1280        }
1281        Some(info)
1282    }
1283
1284    pub fn append_last_usage(&mut self, last: &TokenUsage) {
1285        self.total_token_usage.add_assign(last);
1286        self.last_token_usage = last.clone();
1287    }
1288
1289    pub fn fill_to_context_window(&mut self, context_window: i64) {
1290        let previous_total = self.total_token_usage.total_tokens;
1291        let delta = (context_window - previous_total).max(0);
1292
1293        self.model_context_window = Some(context_window);
1294        self.total_token_usage = TokenUsage {
1295            total_tokens: context_window,
1296            ..TokenUsage::default()
1297        };
1298        self.last_token_usage = TokenUsage {
1299            total_tokens: delta,
1300            ..TokenUsage::default()
1301        };
1302    }
1303
1304    pub fn full_context_window(context_window: i64) -> Self {
1305        let mut info = Self {
1306            total_token_usage: TokenUsage::default(),
1307            last_token_usage: TokenUsage::default(),
1308            requested_model: None,
1309            latest_response_model: None,
1310            model_context_window: Some(context_window),
1311        };
1312        info.fill_to_context_window(context_window);
1313        info
1314    }
1315}
1316
1317#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1318pub struct TokenCountEvent {
1319    pub info: Option<TokenUsageInfo>,
1320    pub rate_limits: Option<RateLimitSnapshot>,
1321}
1322
1323#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
1324pub struct RateLimitSnapshot {
1325    #[ts(type = "string | null")]
1326    #[serde(default)]
1327    pub limit_id: Option<String>,
1328    #[ts(type = "string | null")]
1329    #[serde(default)]
1330    pub limit_name: Option<String>,
1331    pub primary: Option<RateLimitWindow>,
1332    pub secondary: Option<RateLimitWindow>,
1333    pub credits: Option<CreditsSnapshot>,
1334    pub plan_type: Option<crate::account::PlanType>,
1335}
1336
1337#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
1338pub struct RateLimitWindow {
1339    /// Percentage (0-100) of the window that has been consumed.
1340    pub used_percent: f64,
1341    /// Rolling window duration, in minutes.
1342    #[ts(type = "number | null")]
1343    pub window_minutes: Option<u64>,
1344    /// Legacy relative reset in seconds.
1345    #[ts(optional)]
1346    #[ts(type = "number")]
1347    #[serde(default, skip_serializing_if = "Option::is_none")]
1348    pub resets_in_seconds: Option<u64>,
1349    /// Unix timestamp (seconds since epoch) when the window resets.
1350    #[ts(optional)]
1351    #[ts(type = "number")]
1352    #[serde(default, skip_serializing_if = "Option::is_none")]
1353    pub resets_at: Option<i64>,
1354}
1355
1356#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
1357pub struct CreditsSnapshot {
1358    pub has_credits: bool,
1359    pub unlimited: bool,
1360    pub balance: Option<String>,
1361}
1362
1363// Includes prompts, tools and space to call compact.
1364const BASELINE_TOKENS: i64 = 12000;
1365
1366impl TokenUsage {
1367    pub fn is_zero(&self) -> bool {
1368        self.total_tokens == 0
1369    }
1370
1371    pub fn cached_input(&self) -> i64 {
1372        self.cached_input_tokens.max(0)
1373    }
1374
1375    pub fn non_cached_input(&self) -> i64 {
1376        (self.input_tokens - self.cached_input()).max(0)
1377    }
1378
1379    /// Primary count for display as a single absolute value: non-cached input + output.
1380    pub fn blended_total(&self) -> i64 {
1381        (self.non_cached_input() + self.output_tokens.max(0)).max(0)
1382    }
1383
1384    pub fn tokens_in_context_window(&self) -> i64 {
1385        self.total_tokens
1386    }
1387
1388    /// Estimate the remaining user-controllable percentage of the model's context window.
1389    ///
1390    /// `context_window` is the total size of the model's context window.
1391    /// `BASELINE_TOKENS` should capture tokens that are always present in
1392    /// the context (e.g., system prompt and fixed tool instructions) so that
1393    /// the percentage reflects the portion the user can influence.
1394    ///
1395    /// This normalizes both the numerator and denominator by subtracting the
1396    /// baseline, so immediately after the first prompt the UI shows 100% left
1397    /// and trends toward 0% as the user fills the effective window.
1398    pub fn percent_of_context_window_remaining(&self, context_window: i64) -> i64 {
1399        if context_window <= BASELINE_TOKENS {
1400            return 0;
1401        }
1402
1403        let effective_window = context_window - BASELINE_TOKENS;
1404        let used = (self.tokens_in_context_window() - BASELINE_TOKENS).max(0);
1405        let remaining = (effective_window - used).max(0);
1406        ((remaining as f64 / effective_window as f64) * 100.0)
1407            .clamp(0.0, 100.0)
1408            .round() as i64
1409    }
1410
1411    /// In-place element-wise sum of token counts.
1412    pub fn add_assign(&mut self, other: &TokenUsage) {
1413        self.input_tokens += other.input_tokens;
1414        self.cached_input_tokens += other.cached_input_tokens;
1415        self.output_tokens += other.output_tokens;
1416        self.reasoning_output_tokens += other.reasoning_output_tokens;
1417        self.total_tokens += other.total_tokens;
1418    }
1419}
1420
1421#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1422pub struct FinalOutput {
1423    pub token_usage: TokenUsage,
1424}
1425
1426impl From<TokenUsage> for FinalOutput {
1427    fn from(token_usage: TokenUsage) -> Self {
1428        Self { token_usage }
1429    }
1430}
1431
1432impl fmt::Display for FinalOutput {
1433    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1434        let token_usage = &self.token_usage;
1435
1436        write!(
1437            f,
1438            "Token usage: total={} input={}{} output={}{}",
1439            format_with_separators(token_usage.blended_total()),
1440            format_with_separators(token_usage.non_cached_input()),
1441            if token_usage.cached_input() > 0 {
1442                format!(
1443                    " (+ {} cached)",
1444                    format_with_separators(token_usage.cached_input())
1445                )
1446            } else {
1447                String::new()
1448            },
1449            format_with_separators(token_usage.output_tokens),
1450            if token_usage.reasoning_output_tokens > 0 {
1451                format!(
1452                    " (reasoning {})",
1453                    format_with_separators(token_usage.reasoning_output_tokens)
1454                )
1455            } else {
1456                String::new()
1457            }
1458        )
1459    }
1460}
1461
1462#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1463pub struct AgentMessageEvent {
1464    pub message: String,
1465}
1466
1467#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1468pub struct UserMessageEvent {
1469    pub message: String,
1470    /// Image URLs sourced from `UserInput::Image`. These are safe
1471    /// to replay in legacy UI history events and correspond to images sent to
1472    /// the model.
1473    #[serde(skip_serializing_if = "Option::is_none")]
1474    pub images: Option<Vec<String>>,
1475    /// Local file paths sourced from `UserInput::LocalImage`. These are kept so
1476    /// the UI can reattach images when editing history, and should not be sent
1477    /// to the model or treated as API-ready URLs.
1478    #[serde(default)]
1479    pub local_images: Vec<std::path::PathBuf>,
1480    /// UI-defined spans within `message` used to render or persist special elements.
1481    #[serde(default)]
1482    pub text_elements: Vec<crate::user_input::TextElement>,
1483}
1484
1485#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema, TS)]
1486#[serde(rename_all = "snake_case")]
1487pub enum InputMessageKind {
1488    User,
1489    Assistant,
1490    UserInstructions,
1491    EnvironmentContext,
1492    BrowserSnapshot,
1493    Other,
1494}
1495
1496impl From<(&str, &str)> for InputMessageKind {
1497    fn from((role, text): (&str, &str)) -> Self {
1498        if role == "user" {
1499            if text.contains(USER_INSTRUCTIONS_OPEN_TAG) {
1500                return Self::UserInstructions;
1501            }
1502            if text.contains(ENVIRONMENT_CONTEXT_OPEN_TAG)
1503                || text.contains(ENVIRONMENT_CONTEXT_DELTA_OPEN_TAG)
1504            {
1505                return Self::EnvironmentContext;
1506            }
1507            if text.contains(BROWSER_SNAPSHOT_OPEN_TAG) {
1508                return Self::BrowserSnapshot;
1509            }
1510            return Self::User;
1511        }
1512        if role == "assistant" {
1513            return Self::Assistant;
1514        }
1515        Self::Other
1516    }
1517}
1518
1519#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1520pub struct AgentMessageDeltaEvent {
1521    pub delta: String,
1522}
1523
1524#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1525pub struct AgentReasoningEvent {
1526    pub text: String,
1527}
1528
1529#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1530pub struct AgentReasoningRawContentEvent {
1531    pub text: String,
1532}
1533
1534#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1535pub struct AgentReasoningRawContentDeltaEvent {
1536    pub delta: String,
1537}
1538
1539#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1540pub struct AgentReasoningSectionBreakEvent {
1541    // load with default value so it's backward compatible with the old format.
1542    #[serde(default)]
1543    pub item_id: String,
1544    #[serde(default)]
1545    pub summary_index: i64,
1546}
1547
1548#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1549pub struct AgentReasoningDeltaEvent {
1550    pub delta: String,
1551}
1552
1553#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1554pub struct CompactionCheckpointWarningEvent {
1555    pub message: String,
1556}
1557
1558#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
1559pub struct McpInvocation {
1560    /// Name of the MCP server as defined in the config.
1561    pub server: String,
1562    /// Name of the tool as given by the MCP server.
1563    pub tool: String,
1564    /// Arguments to the tool call.
1565    pub arguments: Option<serde_json::Value>,
1566}
1567
1568#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
1569pub struct McpToolCallBeginEvent {
1570    /// Identifier so this can be paired with the McpToolCallEnd event.
1571    pub call_id: String,
1572    pub invocation: McpInvocation,
1573}
1574
1575#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
1576pub struct McpToolCallEndEvent {
1577    /// Identifier for the corresponding McpToolCallBegin that finished.
1578    pub call_id: String,
1579    pub invocation: McpInvocation,
1580    #[ts(type = "string")]
1581    pub duration: Duration,
1582    /// Result of the tool call. Note this could be an error.
1583    pub result: Result<CallToolResult, String>,
1584}
1585
1586impl McpToolCallEndEvent {
1587    pub fn is_success(&self) -> bool {
1588        match &self.result {
1589            Ok(result) => !result.is_error.unwrap_or(false),
1590            Err(_) => false,
1591        }
1592    }
1593}
1594
1595#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1596pub struct WebSearchBeginEvent {
1597    pub call_id: String,
1598}
1599
1600#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1601pub struct WebSearchEndEvent {
1602    pub call_id: String,
1603    pub query: String,
1604    pub action: WebSearchAction,
1605}
1606
1607// Conversation kept for backward compatibility.
1608/// Response payload for `Op::GetHistory` containing the current session's
1609/// in-memory transcript.
1610#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1611pub struct ConversationPathResponseEvent {
1612    pub conversation_id: ThreadId,
1613    pub path: PathBuf,
1614}
1615
1616#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1617pub struct ResumedHistory {
1618    pub conversation_id: ThreadId,
1619    pub history: Vec<RolloutItem>,
1620    pub rollout_path: PathBuf,
1621}
1622
1623#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1624pub enum InitialHistory {
1625    New,
1626    Resumed(ResumedHistory),
1627    Forked(Vec<RolloutItem>),
1628}
1629
1630impl InitialHistory {
1631    pub fn forked_from_id(&self) -> Option<ThreadId> {
1632        match self {
1633            InitialHistory::New => None,
1634            InitialHistory::Resumed(resumed) => {
1635                resumed.history.iter().find_map(|item| match item {
1636                    RolloutItem::SessionMeta(meta_line) => meta_line.meta.forked_from_id,
1637                    _ => None,
1638                })
1639            }
1640            InitialHistory::Forked(items) => items.iter().find_map(|item| match item {
1641                RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.id),
1642                _ => None,
1643            }),
1644        }
1645    }
1646
1647    pub fn session_cwd(&self) -> Option<PathBuf> {
1648        match self {
1649            InitialHistory::New => None,
1650            InitialHistory::Resumed(resumed) => session_cwd_from_items(&resumed.history),
1651            InitialHistory::Forked(items) => session_cwd_from_items(items),
1652        }
1653    }
1654
1655    pub fn get_rollout_items(&self) -> Vec<RolloutItem> {
1656        match self {
1657            InitialHistory::New => Vec::new(),
1658            InitialHistory::Resumed(resumed) => resumed.history.clone(),
1659            InitialHistory::Forked(items) => items.clone(),
1660        }
1661    }
1662
1663    pub fn get_event_msgs(&self) -> Option<Vec<EventMsg>> {
1664        match self {
1665            InitialHistory::New => None,
1666            InitialHistory::Resumed(resumed) => Some(
1667                resumed
1668                    .history
1669                    .iter()
1670                    .filter_map(|ri| match ri {
1671                        RolloutItem::Event(ev) => Some(ev.msg.clone()),
1672                        RolloutItem::EventMsg(ev) => Some(ev.clone()),
1673                        _ => None,
1674                    })
1675                    .collect(),
1676            ),
1677            InitialHistory::Forked(items) => Some(
1678                items
1679                    .iter()
1680                    .filter_map(|ri| match ri {
1681                        RolloutItem::Event(ev) => Some(ev.msg.clone()),
1682                        RolloutItem::EventMsg(ev) => Some(ev.clone()),
1683                        _ => None,
1684                    })
1685                    .collect(),
1686            ),
1687        }
1688    }
1689
1690    pub fn get_base_instructions(&self) -> Option<BaseInstructions> {
1691        // TODO: SessionMeta should (in theory) always be first in the history, so we can probably only check the first item?
1692        match self {
1693            InitialHistory::New => None,
1694            InitialHistory::Resumed(resumed) => {
1695                resumed.history.iter().find_map(|item| match item {
1696                    RolloutItem::SessionMeta(meta_line) => meta_line.meta.base_instructions.clone(),
1697                    _ => None,
1698                })
1699            }
1700            InitialHistory::Forked(items) => items.iter().find_map(|item| match item {
1701                RolloutItem::SessionMeta(meta_line) => meta_line.meta.base_instructions.clone(),
1702                _ => None,
1703            }),
1704        }
1705    }
1706
1707    pub fn get_dynamic_tools(&self) -> Option<Vec<DynamicToolSpec>> {
1708        match self {
1709            InitialHistory::New => None,
1710            InitialHistory::Resumed(resumed) => {
1711                resumed.history.iter().find_map(|item| match item {
1712                    RolloutItem::SessionMeta(meta_line) => meta_line.meta.dynamic_tools.clone(),
1713                    _ => None,
1714                })
1715            }
1716            InitialHistory::Forked(items) => items.iter().find_map(|item| match item {
1717                RolloutItem::SessionMeta(meta_line) => meta_line.meta.dynamic_tools.clone(),
1718                _ => None,
1719            }),
1720        }
1721    }
1722}
1723
1724fn session_cwd_from_items(items: &[RolloutItem]) -> Option<PathBuf> {
1725    items.iter().find_map(|item| match item {
1726        RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.cwd.clone()),
1727        _ => None,
1728    })
1729}
1730
1731#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
1732#[serde(rename_all = "lowercase")]
1733#[ts(rename_all = "lowercase")]
1734pub enum SessionSource {
1735    Cli,
1736    #[default]
1737    VSCode,
1738    Exec,
1739    Mcp,
1740    SubAgent(SubAgentSource),
1741    #[serde(other)]
1742    Unknown,
1743}
1744
1745#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
1746#[serde(rename_all = "snake_case")]
1747#[ts(rename_all = "snake_case")]
1748pub enum SubAgentSource {
1749    Review,
1750    Compact,
1751    ThreadSpawn {
1752        parent_thread_id: ThreadId,
1753        depth: i32,
1754    },
1755    MemoryConsolidation,
1756    Other(String),
1757}
1758
1759impl fmt::Display for SessionSource {
1760    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1761        match self {
1762            SessionSource::Cli => f.write_str("cli"),
1763            SessionSource::VSCode => f.write_str("vscode"),
1764            SessionSource::Exec => f.write_str("exec"),
1765            SessionSource::Mcp => f.write_str("mcp"),
1766            SessionSource::SubAgent(sub_source) => write!(f, "subagent_{sub_source}"),
1767            SessionSource::Unknown => f.write_str("unknown"),
1768        }
1769    }
1770}
1771
1772impl fmt::Display for SubAgentSource {
1773    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1774        match self {
1775            SubAgentSource::Review => f.write_str("review"),
1776            SubAgentSource::Compact => f.write_str("compact"),
1777            SubAgentSource::MemoryConsolidation => f.write_str("memory_consolidation"),
1778            SubAgentSource::ThreadSpawn {
1779                parent_thread_id,
1780                depth,
1781            } => {
1782                write!(f, "thread_spawn_{parent_thread_id}_d{depth}")
1783            }
1784            SubAgentSource::Other(other) => f.write_str(other),
1785        }
1786    }
1787}
1788
1789/// SessionMeta contains session-level data that doesn't correspond to a specific turn.
1790///
1791/// NOTE: There used to be an `instructions` field here, which stored user_instructions, but we
1792/// now save that on TurnContext. base_instructions stores the base instructions for the session,
1793/// and should be used when there is no config override.
1794#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
1795pub struct SessionMeta {
1796    pub id: ThreadId,
1797    #[serde(skip_serializing_if = "Option::is_none")]
1798    pub forked_from_id: Option<ThreadId>,
1799    pub timestamp: String,
1800    pub cwd: PathBuf,
1801    pub originator: String,
1802    pub cli_version: String,
1803    #[serde(default)]
1804    pub source: SessionSource,
1805    pub model_provider: Option<String>,
1806    /// base_instructions for the session. This *should* always be present when creating a new session,
1807    /// but may be missing for older sessions. If not present, fall back to rendering the base_instructions
1808    /// from ModelsManager.
1809    pub base_instructions: Option<BaseInstructions>,
1810    #[serde(skip_serializing_if = "Option::is_none")]
1811    pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
1812}
1813
1814impl Default for SessionMeta {
1815    fn default() -> Self {
1816        SessionMeta {
1817            id: ThreadId::default(),
1818            forked_from_id: None,
1819            timestamp: String::new(),
1820            cwd: PathBuf::new(),
1821            originator: String::new(),
1822            cli_version: String::new(),
1823            source: SessionSource::default(),
1824            model_provider: None,
1825            base_instructions: None,
1826            dynamic_tools: None,
1827        }
1828    }
1829}
1830
1831#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
1832pub struct SessionMetaLine {
1833    #[serde(flatten)]
1834    pub meta: SessionMeta,
1835    #[serde(skip_serializing_if = "Option::is_none")]
1836    pub git: Option<GitInfo>,
1837}
1838
1839#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1840pub struct OrderMeta {
1841    /// 1-based ordinal of this request/turn in the session.
1842    pub request_ordinal: u64,
1843    /// Model-provided output_index for the top-level item.
1844    #[serde(skip_serializing_if = "Option::is_none")]
1845    pub output_index: Option<u32>,
1846    /// Model-provided sequence_number within the output_index stream.
1847    #[serde(skip_serializing_if = "Option::is_none")]
1848    pub sequence_number: Option<u64>,
1849}
1850
1851#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1852pub struct RecordedEvent {
1853    pub id: String,
1854    pub event_seq: u64,
1855    #[serde(skip_serializing_if = "Option::is_none")]
1856    pub order: Option<OrderMeta>,
1857    pub msg: EventMsg,
1858}
1859
1860#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
1861#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
1862pub enum RolloutItem {
1863    SessionMeta(SessionMetaLine),
1864    ResponseItem(ResponseItem),
1865    Compacted(CompactedItem),
1866    TurnContext(TurnContextItem),
1867    Event(RecordedEvent),
1868    EventMsg(EventMsg),
1869}
1870
1871#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
1872pub struct CompactedItem {
1873    pub message: String,
1874    #[serde(default, skip_serializing_if = "Option::is_none")]
1875    pub replacement_history: Option<Vec<ResponseItem>>,
1876}
1877
1878impl From<CompactedItem> for ResponseItem {
1879    fn from(value: CompactedItem) -> Self {
1880        ResponseItem::Message {
1881            id: None,
1882            role: "assistant".to_string(),
1883            content: vec![ContentItem::OutputText {
1884                text: value.message,
1885            }],
1886            end_turn: None,
1887            phase: None,
1888        }
1889    }
1890}
1891
1892#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
1893pub struct TurnContextItem {
1894    pub cwd: PathBuf,
1895    pub approval_policy: AskForApproval,
1896    pub sandbox_policy: SandboxPolicy,
1897    pub model: String,
1898    #[serde(skip_serializing_if = "Option::is_none")]
1899    pub personality: Option<Personality>,
1900    #[serde(default, skip_serializing_if = "Option::is_none")]
1901    pub collaboration_mode: Option<CollaborationMode>,
1902    #[serde(skip_serializing_if = "Option::is_none")]
1903    pub effort: Option<ReasoningEffortConfig>,
1904    pub summary: ReasoningSummaryConfig,
1905    #[serde(skip_serializing_if = "Option::is_none")]
1906    pub user_instructions: Option<String>,
1907    #[serde(skip_serializing_if = "Option::is_none")]
1908    pub developer_instructions: Option<String>,
1909    #[serde(skip_serializing_if = "Option::is_none")]
1910    pub final_output_json_schema: Option<Value>,
1911    #[serde(skip_serializing_if = "Option::is_none")]
1912    pub truncation_policy: Option<TruncationPolicy>,
1913}
1914
1915#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1916#[serde(tag = "mode", content = "limit", rename_all = "snake_case")]
1917pub enum TruncationPolicy {
1918    Bytes(usize),
1919    Tokens(usize),
1920}
1921
1922#[derive(Serialize, Deserialize, Clone, JsonSchema)]
1923pub struct RolloutLine {
1924    pub timestamp: String,
1925    #[serde(flatten)]
1926    pub item: RolloutItem,
1927}
1928
1929#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
1930pub struct GitInfo {
1931    /// Current commit hash (SHA)
1932    #[serde(skip_serializing_if = "Option::is_none")]
1933    pub commit_hash: Option<String>,
1934    /// Current branch name
1935    #[serde(skip_serializing_if = "Option::is_none")]
1936    pub branch: Option<String>,
1937    /// Repository URL (if available from remote)
1938    #[serde(skip_serializing_if = "Option::is_none")]
1939    pub repository_url: Option<String>,
1940}
1941
1942#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1943#[serde(rename_all = "snake_case")]
1944pub enum ReviewDelivery {
1945    Inline,
1946    Detached,
1947}
1948
1949#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)]
1950#[serde(tag = "type", rename_all = "camelCase")]
1951#[ts(tag = "type")]
1952pub enum ReviewTarget {
1953    /// Review the working tree: staged, unstaged, and untracked files.
1954    UncommittedChanges,
1955
1956    /// Review changes between the current branch and the given base branch.
1957    #[serde(rename_all = "camelCase")]
1958    #[ts(rename_all = "camelCase")]
1959    BaseBranch { branch: String },
1960
1961    /// Review the changes introduced by a specific commit.
1962    #[serde(rename_all = "camelCase")]
1963    #[ts(rename_all = "camelCase")]
1964    Commit {
1965        sha: String,
1966        /// Optional human-readable label (e.g., commit subject) for UIs.
1967        title: Option<String>,
1968    },
1969
1970    /// Arbitrary instructions provided by the user.
1971    #[serde(rename_all = "camelCase")]
1972    #[ts(rename_all = "camelCase")]
1973    Custom { instructions: String },
1974}
1975
1976#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1977/// Review request sent to the review session.
1978pub struct ReviewRequest {
1979    pub target: ReviewTarget,
1980    #[serde(skip_serializing_if = "Option::is_none")]
1981    #[ts(optional)]
1982    pub user_facing_hint: Option<String>,
1983    /// Legacy plain-text prompt retained for compatibility with older review
1984    /// flows.
1985    #[serde(default)]
1986    pub prompt: String,
1987}
1988
1989#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1990pub struct ReviewSnapshotInfo {
1991    #[serde(skip_serializing_if = "Option::is_none")]
1992    pub snapshot_commit: Option<String>,
1993    #[serde(skip_serializing_if = "Option::is_none")]
1994    pub branch: Option<String>,
1995    #[serde(skip_serializing_if = "Option::is_none")]
1996    pub worktree_path: Option<PathBuf>,
1997    #[serde(skip_serializing_if = "Option::is_none")]
1998    pub repo_root: Option<PathBuf>,
1999}
2000
2001#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2002pub struct ReviewContextMetadata {
2003    #[serde(skip_serializing_if = "Option::is_none")]
2004    pub snapshot: Option<ReviewSnapshotInfo>,
2005}
2006
2007/// Structured review result produced by a child review session.
2008#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2009pub struct ReviewOutputEvent {
2010    pub findings: Vec<ReviewFinding>,
2011    pub overall_correctness: String,
2012    pub overall_explanation: String,
2013    pub overall_confidence_score: f32,
2014}
2015
2016impl Default for ReviewOutputEvent {
2017    fn default() -> Self {
2018        Self {
2019            findings: Vec::new(),
2020            overall_correctness: String::default(),
2021            overall_explanation: String::default(),
2022            overall_confidence_score: 0.0,
2023        }
2024    }
2025}
2026
2027/// A single review finding describing an observed issue or recommendation.
2028#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2029pub struct ReviewFinding {
2030    pub title: String,
2031    pub body: String,
2032    pub confidence_score: f32,
2033    pub priority: i32,
2034    pub code_location: ReviewCodeLocation,
2035}
2036
2037/// Location of the code related to a review finding.
2038#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2039pub struct ReviewCodeLocation {
2040    pub absolute_file_path: PathBuf,
2041    pub line_range: ReviewLineRange,
2042}
2043
2044/// Inclusive line range in a file associated with the finding.
2045#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2046pub struct ReviewLineRange {
2047    pub start: u32,
2048    pub end: u32,
2049}
2050
2051#[derive(
2052    Debug, Clone, Copy, Display, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS, Default,
2053)]
2054#[serde(rename_all = "snake_case")]
2055pub enum ExecCommandSource {
2056    #[default]
2057    Agent,
2058    UserShell,
2059    UnifiedExecStartup,
2060    UnifiedExecInteraction,
2061}
2062
2063#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2064pub struct ExecCommandBeginEvent {
2065    /// Identifier so this can be paired with the ExecCommandEnd event.
2066    pub call_id: String,
2067    /// Identifier for the underlying PTY process (when available).
2068    #[serde(default, skip_serializing_if = "Option::is_none")]
2069    #[ts(optional)]
2070    pub process_id: Option<String>,
2071    /// Turn ID that this command belongs to.
2072    pub turn_id: String,
2073    /// The command to be executed.
2074    pub command: Vec<String>,
2075    /// The command's working directory if not the default cwd for the agent.
2076    pub cwd: PathBuf,
2077    pub parsed_cmd: Vec<ParsedCommand>,
2078    /// Where the command originated. Defaults to Agent for backward compatibility.
2079    #[serde(default)]
2080    pub source: ExecCommandSource,
2081    /// Raw input sent to a unified exec session (if this is an interaction event).
2082    #[serde(default, skip_serializing_if = "Option::is_none")]
2083    #[ts(optional)]
2084    pub interaction_input: Option<String>,
2085}
2086
2087#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2088pub struct ExecCommandEndEvent {
2089    /// Identifier for the ExecCommandBegin that finished.
2090    pub call_id: String,
2091    /// Identifier for the underlying PTY process (when available).
2092    #[serde(default, skip_serializing_if = "Option::is_none")]
2093    #[ts(optional)]
2094    pub process_id: Option<String>,
2095    /// Turn ID that this command belongs to.
2096    pub turn_id: String,
2097    /// The command that was executed.
2098    pub command: Vec<String>,
2099    /// The command's working directory if not the default cwd for the agent.
2100    pub cwd: PathBuf,
2101    pub parsed_cmd: Vec<ParsedCommand>,
2102    /// Where the command originated. Defaults to Agent for backward compatibility.
2103    #[serde(default)]
2104    pub source: ExecCommandSource,
2105    /// Raw input sent to a unified exec session (if this is an interaction event).
2106    #[serde(default, skip_serializing_if = "Option::is_none")]
2107    #[ts(optional)]
2108    pub interaction_input: Option<String>,
2109
2110    /// Captured stdout
2111    pub stdout: String,
2112    /// Captured stderr
2113    pub stderr: String,
2114    /// Captured aggregated output
2115    #[serde(default)]
2116    pub aggregated_output: String,
2117    /// The command's exit code.
2118    pub exit_code: i32,
2119    /// The duration of the command execution.
2120    #[ts(type = "string")]
2121    pub duration: Duration,
2122    /// Formatted output from the command, as seen by the model.
2123    pub formatted_output: String,
2124}
2125
2126#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2127pub struct ViewImageToolCallEvent {
2128    /// Identifier for the originating tool call.
2129    pub call_id: String,
2130    /// Local filesystem path provided to the tool.
2131    pub path: PathBuf,
2132}
2133
2134#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2135#[serde(rename_all = "snake_case")]
2136pub enum ExecOutputStream {
2137    Stdout,
2138    Stderr,
2139}
2140
2141#[serde_as]
2142#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2143pub struct ExecCommandOutputDeltaEvent {
2144    /// Identifier for the ExecCommandBegin that produced this chunk.
2145    pub call_id: String,
2146    /// Which stream produced this chunk.
2147    pub stream: ExecOutputStream,
2148    /// Raw bytes from the stream (may not be valid UTF-8).
2149    #[serde_as(as = "serde_with::base64::Base64")]
2150    #[schemars(with = "String")]
2151    #[ts(type = "string")]
2152    pub chunk: Vec<u8>,
2153}
2154
2155#[serde_as]
2156#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2157pub struct TerminalInteractionEvent {
2158    /// Identifier for the ExecCommandBegin that produced this chunk.
2159    pub call_id: String,
2160    /// Process id associated with the running command.
2161    pub process_id: String,
2162    /// Stdin sent to the running session.
2163    pub stdin: String,
2164}
2165
2166#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2167pub struct BackgroundEventEvent {
2168    pub message: String,
2169}
2170
2171#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2172pub struct DeprecationNoticeEvent {
2173    /// Concise summary of what is deprecated.
2174    pub summary: String,
2175    /// Optional extra guidance, such as migration steps or rationale.
2176    #[serde(skip_serializing_if = "Option::is_none")]
2177    pub details: Option<String>,
2178}
2179
2180#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2181pub struct UndoStartedEvent {
2182    #[serde(skip_serializing_if = "Option::is_none")]
2183    pub message: Option<String>,
2184}
2185
2186#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2187pub struct UndoCompletedEvent {
2188    pub success: bool,
2189    #[serde(skip_serializing_if = "Option::is_none")]
2190    pub message: Option<String>,
2191}
2192
2193#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2194pub struct ThreadRolledBackEvent {
2195    /// Number of user turns that were removed from context.
2196    pub num_turns: u32,
2197}
2198
2199#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2200pub struct StreamErrorEvent {
2201    pub message: String,
2202    #[serde(default)]
2203    pub codex_error_info: Option<CodexErrorInfo>,
2204    /// Optional details about the underlying stream failure (often the same
2205    /// human-readable message that is surfaced as the terminal error if retries
2206    /// are exhausted).
2207    #[serde(default)]
2208    pub additional_details: Option<String>,
2209}
2210
2211#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2212pub struct StreamInfoEvent {
2213    pub message: String,
2214}
2215
2216#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2217pub struct PatchApplyBeginEvent {
2218    /// Identifier so this can be paired with the PatchApplyEnd event.
2219    pub call_id: String,
2220    /// Turn ID that this patch belongs to.
2221    /// Uses `#[serde(default)]` for backwards compatibility.
2222    #[serde(default)]
2223    pub turn_id: String,
2224    /// If true, there was no ApplyPatchApprovalRequest for this patch.
2225    pub auto_approved: bool,
2226    /// The changes to be applied.
2227    pub changes: HashMap<PathBuf, FileChange>,
2228}
2229
2230#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2231pub struct PatchApplyEndEvent {
2232    /// Identifier for the PatchApplyBegin that finished.
2233    pub call_id: String,
2234    /// Turn ID that this patch belongs to.
2235    /// Uses `#[serde(default)]` for backwards compatibility.
2236    #[serde(default)]
2237    pub turn_id: String,
2238    /// Captured stdout (summary printed by apply_patch).
2239    pub stdout: String,
2240    /// Captured stderr (parser errors, IO failures, etc.).
2241    pub stderr: String,
2242    /// Whether the patch was applied successfully.
2243    pub success: bool,
2244    /// The changes that were applied (mirrors PatchApplyBeginEvent::changes).
2245    #[serde(default)]
2246    pub changes: HashMap<PathBuf, FileChange>,
2247}
2248
2249#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2250pub struct TurnDiffEvent {
2251    pub unified_diff: String,
2252}
2253
2254#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2255pub struct GetHistoryEntryResponseEvent {
2256    pub offset: usize,
2257    pub log_id: u64,
2258    /// The entry at the requested offset, if available and parseable.
2259    #[serde(skip_serializing_if = "Option::is_none")]
2260    pub entry: Option<HistoryEntry>,
2261}
2262
2263#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2264pub struct McpListToolsResponseEvent {
2265    /// Fully qualified tool name -> tool definition.
2266    pub tools: std::collections::HashMap<String, McpTool>,
2267    /// Legacy server -> tool names map used by existing UI surfaces.
2268    #[serde(default, skip_serializing_if = "Option::is_none")]
2269    pub server_tools: Option<std::collections::HashMap<String, Vec<String>>>,
2270    /// Legacy server failure map keyed by server name.
2271    #[serde(default, skip_serializing_if = "Option::is_none")]
2272    pub server_failures: Option<std::collections::HashMap<String, McpServerFailure>>,
2273    /// Known resources grouped by server name.
2274    #[serde(default)]
2275    pub resources: std::collections::HashMap<String, Vec<McpResource>>,
2276    /// Known resource templates grouped by server name.
2277    #[serde(default)]
2278    pub resource_templates: std::collections::HashMap<String, Vec<McpResourceTemplate>>,
2279    /// Authentication status for each configured MCP server.
2280    #[serde(default)]
2281    pub auth_statuses: std::collections::HashMap<String, McpAuthStatus>,
2282}
2283
2284#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
2285#[serde(rename_all = "snake_case")]
2286#[ts(rename_all = "snake_case")]
2287pub enum McpServerFailurePhase {
2288    Start,
2289    ListTools,
2290}
2291
2292#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)]
2293pub struct McpServerFailure {
2294    pub phase: McpServerFailurePhase,
2295    pub message: String,
2296}
2297
2298#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2299pub struct McpStartupUpdateEvent {
2300    /// Server name being started.
2301    pub server: String,
2302    /// Current startup status.
2303    pub status: McpStartupStatus,
2304}
2305
2306#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2307#[serde(rename_all = "snake_case", tag = "state")]
2308#[ts(rename_all = "snake_case", tag = "state")]
2309pub enum McpStartupStatus {
2310    Starting,
2311    Ready,
2312    Failed { error: String },
2313    Cancelled,
2314}
2315
2316#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, Default)]
2317pub struct McpStartupCompleteEvent {
2318    pub ready: Vec<String>,
2319    pub failed: Vec<McpStartupFailure>,
2320    pub cancelled: Vec<String>,
2321}
2322
2323#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2324pub struct McpStartupFailure {
2325    pub server: String,
2326    pub error: String,
2327}
2328
2329#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
2330#[serde(rename_all = "snake_case")]
2331#[ts(rename_all = "snake_case")]
2332pub enum McpAuthStatus {
2333    Unsupported,
2334    NotLoggedIn,
2335    BearerToken,
2336    OAuth,
2337}
2338
2339impl fmt::Display for McpAuthStatus {
2340    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2341        let text = match self {
2342            McpAuthStatus::Unsupported => "Unsupported",
2343            McpAuthStatus::NotLoggedIn => "Not logged in",
2344            McpAuthStatus::BearerToken => "Bearer token",
2345            McpAuthStatus::OAuth => "OAuth",
2346        };
2347        f.write_str(text)
2348    }
2349}
2350
2351/// Response payload for `Op::ListCustomPrompts`.
2352#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2353pub struct ListCustomPromptsResponseEvent {
2354    pub custom_prompts: Vec<CustomPrompt>,
2355}
2356
2357/// Response payload for `Op::ListSkills`.
2358#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2359pub struct ListSkillsResponseEvent {
2360    pub skills: Vec<SkillsListEntry>,
2361}
2362
2363#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2364pub struct RemoteSkillSummary {
2365    pub id: String,
2366    pub name: String,
2367    pub description: String,
2368}
2369
2370/// Response payload for `Op::ListRemoteSkills`.
2371#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2372pub struct ListRemoteSkillsResponseEvent {
2373    pub skills: Vec<RemoteSkillSummary>,
2374}
2375
2376/// Response payload for `Op::DownloadRemoteSkill`.
2377#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2378pub struct RemoteSkillDownloadedEvent {
2379    pub id: String,
2380    pub name: String,
2381    pub path: PathBuf,
2382}
2383
2384#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
2385#[serde(rename_all = "snake_case")]
2386#[ts(rename_all = "snake_case")]
2387pub enum SkillScope {
2388    User,
2389    Repo,
2390    System,
2391    Admin,
2392}
2393
2394#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2395pub struct SkillMetadata {
2396    pub name: String,
2397    pub description: String,
2398    #[serde(default, skip_serializing_if = "Option::is_none")]
2399    #[ts(optional)]
2400    /// Legacy short_description from SKILL.md. Prefer SKILL.json interface.short_description.
2401    pub short_description: Option<String>,
2402    #[serde(default, skip_serializing_if = "Option::is_none")]
2403    #[ts(optional)]
2404    pub interface: Option<SkillInterface>,
2405    #[serde(default, skip_serializing_if = "Option::is_none")]
2406    #[ts(optional)]
2407    pub dependencies: Option<SkillDependencies>,
2408    pub path: PathBuf,
2409    pub scope: SkillScope,
2410    pub enabled: bool,
2411}
2412
2413#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)]
2414pub struct SkillInterface {
2415    #[ts(optional)]
2416    pub display_name: Option<String>,
2417    #[ts(optional)]
2418    pub short_description: Option<String>,
2419    #[ts(optional)]
2420    pub icon_small: Option<PathBuf>,
2421    #[ts(optional)]
2422    pub icon_large: Option<PathBuf>,
2423    #[ts(optional)]
2424    pub brand_color: Option<String>,
2425    #[ts(optional)]
2426    pub default_prompt: Option<String>,
2427}
2428
2429#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)]
2430pub struct SkillDependencies {
2431    pub tools: Vec<SkillToolDependency>,
2432}
2433
2434#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)]
2435pub struct SkillToolDependency {
2436    #[serde(rename = "type")]
2437    #[ts(rename = "type")]
2438    pub r#type: String,
2439    pub value: String,
2440    #[serde(default, skip_serializing_if = "Option::is_none")]
2441    #[ts(optional)]
2442    pub description: Option<String>,
2443    #[serde(default, skip_serializing_if = "Option::is_none")]
2444    #[ts(optional)]
2445    pub transport: Option<String>,
2446    #[serde(default, skip_serializing_if = "Option::is_none")]
2447    #[ts(optional)]
2448    pub command: Option<String>,
2449    #[serde(default, skip_serializing_if = "Option::is_none")]
2450    #[ts(optional)]
2451    pub url: Option<String>,
2452}
2453
2454#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2455pub struct SkillErrorInfo {
2456    pub path: PathBuf,
2457    pub message: String,
2458}
2459
2460#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2461pub struct SkillsListEntry {
2462    pub cwd: PathBuf,
2463    pub skills: Vec<SkillMetadata>,
2464    pub errors: Vec<SkillErrorInfo>,
2465}
2466
2467#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2468pub struct SessionConfiguredEvent {
2469    pub session_id: ThreadId,
2470    #[serde(skip_serializing_if = "Option::is_none")]
2471    pub forked_from_id: Option<ThreadId>,
2472
2473    /// Optional user-facing thread name (may be unset).
2474    #[serde(default, skip_serializing_if = "Option::is_none")]
2475    #[ts(optional)]
2476    pub thread_name: Option<String>,
2477
2478    /// Tell the client what model is being queried.
2479    pub model: String,
2480
2481    pub model_provider_id: String,
2482
2483    /// When to escalate for approval for execution
2484    pub approval_policy: AskForApproval,
2485
2486    /// How to sandbox commands executed in the system
2487    pub sandbox_policy: SandboxPolicy,
2488
2489    /// Working directory that should be treated as the *root* of the
2490    /// session.
2491    pub cwd: PathBuf,
2492
2493    /// The effort the model is putting into reasoning about the user's request.
2494    #[serde(skip_serializing_if = "Option::is_none")]
2495    pub reasoning_effort: Option<ReasoningEffortConfig>,
2496
2497    /// Identifier of the history log file (inode on Unix, 0 otherwise).
2498    pub history_log_id: u64,
2499
2500    /// Current number of entries in the history log.
2501    pub history_entry_count: usize,
2502
2503    /// Optional initial messages (as events) for resumed sessions.
2504    /// When present, UIs can use these to seed the history.
2505    #[serde(skip_serializing_if = "Option::is_none")]
2506    pub initial_messages: Option<Vec<EventMsg>>,
2507
2508    /// Path in which the rollout is stored. Can be `None` for ephemeral threads
2509    #[serde(skip_serializing_if = "Option::is_none")]
2510    pub rollout_path: Option<PathBuf>,
2511}
2512
2513#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2514pub struct ThreadNameUpdatedEvent {
2515    pub thread_id: ThreadId,
2516    #[serde(default, skip_serializing_if = "Option::is_none")]
2517    #[ts(optional)]
2518    pub thread_name: Option<String>,
2519}
2520
2521/// User's decision in response to an ExecApprovalRequest.
2522#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, Display, JsonSchema, TS)]
2523#[serde(rename_all = "snake_case")]
2524pub enum ReviewDecision {
2525    /// User has approved this command and the agent should execute it.
2526    Approved,
2527
2528    /// User has approved this command and wants to apply the proposed execpolicy
2529    /// amendment so future matching commands are permitted.
2530    ApprovedExecpolicyAmendment {
2531        proposed_execpolicy_amendment: ExecPolicyAmendment,
2532    },
2533
2534    /// User has approved this command and wants to automatically approve any
2535    /// future identical instances (`command` and `cwd` match exactly) for the
2536    /// remainder of the session.
2537    ApprovedForSession,
2538
2539    /// User has denied this command and the agent should not execute it, but
2540    /// it should continue the session and try something else.
2541    #[default]
2542    Denied,
2543
2544    /// User has denied this command and the agent should not do anything until
2545    /// the user's next command.
2546    Abort,
2547}
2548
2549impl ReviewDecision {
2550    /// Returns an opaque version of the decision without PII. We can't use an ignored flag
2551    /// on `serde` because the serialization is required by some surfaces.
2552    pub fn to_opaque_string(&self) -> &'static str {
2553        match self {
2554            ReviewDecision::Approved => "approved",
2555            ReviewDecision::ApprovedExecpolicyAmendment { .. } => "approved_with_amendment",
2556            ReviewDecision::ApprovedForSession => "approved_for_session",
2557            ReviewDecision::Denied => "denied",
2558            ReviewDecision::Abort => "abort",
2559        }
2560    }
2561}
2562
2563#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2564#[serde(tag = "type", rename_all = "snake_case")]
2565#[ts(tag = "type")]
2566pub enum FileChange {
2567    Add {
2568        content: String,
2569    },
2570    Delete {
2571        content: String,
2572    },
2573    Update {
2574        unified_diff: String,
2575        move_path: Option<PathBuf>,
2576    },
2577}
2578
2579#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2580pub struct Chunk {
2581    /// 1-based line index of the first line in the original file
2582    pub orig_index: u32,
2583    pub deleted_lines: Vec<String>,
2584    pub inserted_lines: Vec<String>,
2585}
2586
2587#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2588pub struct TurnAbortedEvent {
2589    pub reason: TurnAbortReason,
2590}
2591
2592#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2593#[serde(rename_all = "snake_case")]
2594pub enum TurnAbortReason {
2595    Interrupted,
2596    Replaced,
2597    ReviewEnded,
2598}
2599
2600#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2601pub struct CollabAgentSpawnBeginEvent {
2602    /// Identifier for the collab tool call.
2603    pub call_id: String,
2604    /// Thread ID of the sender.
2605    pub sender_thread_id: ThreadId,
2606    /// Initial prompt sent to the agent. Can be empty to prevent CoT leaking at the
2607    /// beginning.
2608    pub prompt: String,
2609}
2610
2611#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2612pub struct CollabAgentSpawnEndEvent {
2613    /// Identifier for the collab tool call.
2614    pub call_id: String,
2615    /// Thread ID of the sender.
2616    pub sender_thread_id: ThreadId,
2617    /// Thread ID of the newly spawned agent, if it was created.
2618    pub new_thread_id: Option<ThreadId>,
2619    /// Initial prompt sent to the agent. Can be empty to prevent CoT leaking at the
2620    /// beginning.
2621    pub prompt: String,
2622    /// Last known status of the new agent reported to the sender agent.
2623    pub status: AgentStatus,
2624}
2625
2626#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2627pub struct CollabAgentInteractionBeginEvent {
2628    /// Identifier for the collab tool call.
2629    pub call_id: String,
2630    /// Thread ID of the sender.
2631    pub sender_thread_id: ThreadId,
2632    /// Thread ID of the receiver.
2633    pub receiver_thread_id: ThreadId,
2634    /// Prompt sent from the sender to the receiver. Can be empty to prevent CoT
2635    /// leaking at the beginning.
2636    pub prompt: String,
2637}
2638
2639#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2640pub struct CollabAgentInteractionEndEvent {
2641    /// Identifier for the collab tool call.
2642    pub call_id: String,
2643    /// Thread ID of the sender.
2644    pub sender_thread_id: ThreadId,
2645    /// Thread ID of the receiver.
2646    pub receiver_thread_id: ThreadId,
2647    /// Prompt sent from the sender to the receiver. Can be empty to prevent CoT
2648    /// leaking at the beginning.
2649    pub prompt: String,
2650    /// Last known status of the receiver agent reported to the sender agent.
2651    pub status: AgentStatus,
2652}
2653
2654#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2655pub struct CollabWaitingBeginEvent {
2656    /// Thread ID of the sender.
2657    pub sender_thread_id: ThreadId,
2658    /// Thread ID of the receivers.
2659    pub receiver_thread_ids: Vec<ThreadId>,
2660    /// ID of the waiting call.
2661    pub call_id: String,
2662}
2663
2664#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2665pub struct CollabWaitingEndEvent {
2666    /// Thread ID of the sender.
2667    pub sender_thread_id: ThreadId,
2668    /// ID of the waiting call.
2669    pub call_id: String,
2670    /// Last known status of the receiver agents reported to the sender agent.
2671    pub statuses: HashMap<ThreadId, AgentStatus>,
2672}
2673
2674#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2675pub struct CollabCloseBeginEvent {
2676    /// Identifier for the collab tool call.
2677    pub call_id: String,
2678    /// Thread ID of the sender.
2679    pub sender_thread_id: ThreadId,
2680    /// Thread ID of the receiver.
2681    pub receiver_thread_id: ThreadId,
2682}
2683
2684#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2685pub struct CollabCloseEndEvent {
2686    /// Identifier for the collab tool call.
2687    pub call_id: String,
2688    /// Thread ID of the sender.
2689    pub sender_thread_id: ThreadId,
2690    /// Thread ID of the receiver.
2691    pub receiver_thread_id: ThreadId,
2692    /// Last known status of the receiver agent reported to the sender agent before
2693    /// the close.
2694    pub status: AgentStatus,
2695}
2696
2697#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2698pub struct CollabResumeBeginEvent {
2699    /// Identifier for the collab tool call.
2700    pub call_id: String,
2701    /// Thread ID of the sender.
2702    pub sender_thread_id: ThreadId,
2703    /// Thread ID of the receiver.
2704    pub receiver_thread_id: ThreadId,
2705}
2706
2707#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2708pub struct CollabResumeEndEvent {
2709    /// Identifier for the collab tool call.
2710    pub call_id: String,
2711    /// Thread ID of the sender.
2712    pub sender_thread_id: ThreadId,
2713    /// Thread ID of the receiver.
2714    pub receiver_thread_id: ThreadId,
2715    /// Last known status of the receiver agent reported to the sender agent after
2716    /// resume.
2717    pub status: AgentStatus,
2718}
2719
2720#[cfg(test)]
2721mod tests {
2722    use super::*;
2723    use crate::items::UserMessageItem;
2724    use crate::items::WebSearchItem;
2725    use anyhow::Result;
2726    use pretty_assertions::assert_eq;
2727    use serde_json::json;
2728    use tempfile::NamedTempFile;
2729
2730    #[test]
2731    fn external_sandbox_reports_full_access_flags() {
2732        let restricted = SandboxPolicy::ExternalSandbox {
2733            network_access: NetworkAccess::Restricted,
2734        };
2735        assert!(restricted.has_full_disk_write_access());
2736        assert!(!restricted.has_full_network_access());
2737
2738        let enabled = SandboxPolicy::ExternalSandbox {
2739            network_access: NetworkAccess::Enabled,
2740        };
2741        assert!(enabled.has_full_disk_write_access());
2742        assert!(enabled.has_full_network_access());
2743    }
2744
2745    #[test]
2746    fn item_started_event_from_web_search_emits_begin_event() {
2747        let event = ItemStartedEvent {
2748            thread_id: ThreadId::new(),
2749            turn_id: "turn-1".into(),
2750            item: TurnItem::WebSearch(WebSearchItem {
2751                id: "search-1".into(),
2752                query: "find docs".into(),
2753                action: WebSearchAction::Search {
2754                    query: Some("find docs".into()),
2755                    queries: None,
2756                },
2757            }),
2758        };
2759
2760        let legacy_events = event.as_legacy_events(false);
2761        assert_eq!(legacy_events.len(), 1);
2762        match &legacy_events[0] {
2763            EventMsg::WebSearchBegin(event) => assert_eq!(event.call_id, "search-1"),
2764            _ => panic!("expected WebSearchBegin event"),
2765        }
2766    }
2767
2768    #[test]
2769    fn item_started_event_from_non_web_search_emits_no_legacy_events() {
2770        let event = ItemStartedEvent {
2771            thread_id: ThreadId::new(),
2772            turn_id: "turn-1".into(),
2773            item: TurnItem::UserMessage(UserMessageItem::new(&[])),
2774        };
2775
2776        assert!(event.as_legacy_events(false).is_empty());
2777    }
2778
2779    #[test]
2780    fn user_input_serialization_omits_final_output_json_schema_when_none() -> Result<()> {
2781        let op = Op::UserInput {
2782            items: Vec::new(),
2783            final_output_json_schema: None,
2784        };
2785
2786        let json_op = serde_json::to_value(op)?;
2787        assert_eq!(json_op, json!({ "type": "user_input", "items": [] }));
2788
2789        Ok(())
2790    }
2791
2792    #[test]
2793    fn user_input_deserializes_without_final_output_json_schema_field() -> Result<()> {
2794        let op: Op = serde_json::from_value(json!({ "type": "user_input", "items": [] }))?;
2795
2796        assert_eq!(
2797            op,
2798            Op::UserInput {
2799                items: Vec::new(),
2800                final_output_json_schema: None,
2801            }
2802        );
2803
2804        Ok(())
2805    }
2806
2807    #[test]
2808    fn user_input_serialization_includes_final_output_json_schema_when_some() -> Result<()> {
2809        let schema = json!({
2810            "type": "object",
2811            "properties": {
2812                "answer": { "type": "string" }
2813            },
2814            "required": ["answer"],
2815            "additionalProperties": false
2816        });
2817        let op = Op::UserInput {
2818            items: Vec::new(),
2819            final_output_json_schema: Some(schema.clone()),
2820        };
2821
2822        let json_op = serde_json::to_value(op)?;
2823        assert_eq!(
2824            json_op,
2825            json!({
2826                "type": "user_input",
2827                "items": [],
2828                "final_output_json_schema": schema,
2829            })
2830        );
2831
2832        Ok(())
2833    }
2834
2835    #[test]
2836    fn user_input_text_serializes_empty_text_elements() -> Result<()> {
2837        let input = UserInput::Text {
2838            text: "hello".to_string(),
2839            text_elements: Vec::new(),
2840        };
2841
2842        let json_input = serde_json::to_value(input)?;
2843        assert_eq!(
2844            json_input,
2845            json!({
2846                "type": "text",
2847                "text": "hello",
2848                "text_elements": [],
2849            })
2850        );
2851
2852        Ok(())
2853    }
2854
2855    #[test]
2856    fn user_message_event_serializes_empty_metadata_vectors() -> Result<()> {
2857        let event = UserMessageEvent {
2858            message: "hello".to_string(),
2859            images: None,
2860            local_images: Vec::new(),
2861            text_elements: Vec::new(),
2862        };
2863
2864        let json_event = serde_json::to_value(event)?;
2865        assert_eq!(
2866            json_event,
2867            json!({
2868                "message": "hello",
2869                "local_images": [],
2870                "text_elements": [],
2871            })
2872        );
2873
2874        Ok(())
2875    }
2876
2877    /// Serialize Event to verify that its JSON representation has the expected
2878    /// amount of nesting.
2879    #[test]
2880    fn serialize_event() -> Result<()> {
2881        let conversation_id = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;
2882        let rollout_file = NamedTempFile::new()?;
2883        let event = Event {
2884            id: "1234".to_string(),
2885            msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
2886                session_id: conversation_id,
2887                forked_from_id: None,
2888                thread_name: None,
2889                model: "codex-mini-latest".to_string(),
2890                model_provider_id: "openai".to_string(),
2891                approval_policy: AskForApproval::Never,
2892                sandbox_policy: SandboxPolicy::ReadOnly,
2893                cwd: PathBuf::from("/home/user/project"),
2894                reasoning_effort: Some(ReasoningEffortConfig::default()),
2895                history_log_id: 0,
2896                history_entry_count: 0,
2897                initial_messages: None,
2898                rollout_path: Some(rollout_file.path().to_path_buf()),
2899            }),
2900        };
2901
2902        let expected = json!({
2903            "id": "1234",
2904            "msg": {
2905                "type": "session_configured",
2906                "session_id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
2907                "model": "codex-mini-latest",
2908                "model_provider_id": "openai",
2909                "approval_policy": "never",
2910                "sandbox_policy": {
2911                    "type": "read-only"
2912                },
2913                "cwd": "/home/user/project",
2914                "reasoning_effort": "medium",
2915                "history_log_id": 0,
2916                "history_entry_count": 0,
2917                "rollout_path": format!("{}", rollout_file.path().display()),
2918            }
2919        });
2920        assert_eq!(expected, serde_json::to_value(&event)?);
2921        Ok(())
2922    }
2923
2924    #[test]
2925    fn vec_u8_as_base64_serialization_and_deserialization() -> Result<()> {
2926        let event = ExecCommandOutputDeltaEvent {
2927            call_id: "call21".to_string(),
2928            stream: ExecOutputStream::Stdout,
2929            chunk: vec![1, 2, 3, 4, 5],
2930        };
2931        let serialized = serde_json::to_string(&event)?;
2932        assert_eq!(
2933            r#"{"call_id":"call21","stream":"stdout","chunk":"AQIDBAU="}"#,
2934            serialized,
2935        );
2936
2937        let deserialized: ExecCommandOutputDeltaEvent = serde_json::from_str(&serialized)?;
2938        assert_eq!(deserialized, event);
2939        Ok(())
2940    }
2941
2942    #[test]
2943    fn serialize_mcp_startup_update_event() -> Result<()> {
2944        let event = Event {
2945            id: "init".to_string(),
2946            msg: EventMsg::McpStartupUpdate(McpStartupUpdateEvent {
2947                server: "srv".to_string(),
2948                status: McpStartupStatus::Failed {
2949                    error: "boom".to_string(),
2950                },
2951            }),
2952        };
2953
2954        let value = serde_json::to_value(&event)?;
2955        assert_eq!(value["msg"]["type"], "mcp_startup_update");
2956        assert_eq!(value["msg"]["server"], "srv");
2957        assert_eq!(value["msg"]["status"]["state"], "failed");
2958        assert_eq!(value["msg"]["status"]["error"], "boom");
2959        Ok(())
2960    }
2961
2962    #[test]
2963    fn serialize_mcp_startup_complete_event() -> Result<()> {
2964        let event = Event {
2965            id: "init".to_string(),
2966            msg: EventMsg::McpStartupComplete(McpStartupCompleteEvent {
2967                ready: vec!["a".to_string()],
2968                failed: vec![McpStartupFailure {
2969                    server: "b".to_string(),
2970                    error: "bad".to_string(),
2971                }],
2972                cancelled: vec!["c".to_string()],
2973            }),
2974        };
2975
2976        let value = serde_json::to_value(&event)?;
2977        assert_eq!(value["msg"]["type"], "mcp_startup_complete");
2978        assert_eq!(value["msg"]["ready"][0], "a");
2979        assert_eq!(value["msg"]["failed"][0]["server"], "b");
2980        assert_eq!(value["msg"]["failed"][0]["error"], "bad");
2981        assert_eq!(value["msg"]["cancelled"][0], "c");
2982        Ok(())
2983    }
2984}