agcodex_protocol/
protocol.rs

1//! Defines the protocol for a Codex session between a client and an agent.
2//!
3//! Uses a SQ (Submission Queue) / EQ (Event Queue) pattern to asynchronously communicate
4//! between user and agent.
5
6use std::collections::HashMap;
7use std::fmt;
8use std::path::Path;
9use std::path::PathBuf;
10use std::str::FromStr;
11use std::time::Duration;
12
13use agcodex_mcp_types::CallToolResult;
14use agcodex_mcp_types::Tool as McpTool;
15use serde::Deserialize;
16use serde::Serialize;
17use serde_bytes::ByteBuf;
18use strum_macros::Display;
19use ts_rs::TS;
20use uuid::Uuid;
21
22use crate::config_types::ReasoningEffort as ReasoningEffortConfig;
23use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;
24use crate::message_history::HistoryEntry;
25use crate::parse_command::ParsedCommand;
26use crate::plan_tool::UpdatePlanArgs;
27
28/// Submission Queue Entry - requests from user
29#[derive(Debug, Clone, Deserialize, Serialize)]
30pub struct Submission {
31    /// Unique id for this Submission to correlate with Events
32    pub id: String,
33    /// Payload
34    pub op: Op,
35}
36
37/// Submission operation
38#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
39#[serde(tag = "type", rename_all = "snake_case")]
40#[allow(clippy::large_enum_variant)]
41#[non_exhaustive]
42pub enum Op {
43    /// Abort current task.
44    /// This server sends [`EventMsg::TurnAborted`] in response.
45    Interrupt,
46
47    /// Input from the user
48    UserInput {
49        /// User input items, see `InputItem`
50        items: Vec<InputItem>,
51    },
52
53    /// Similar to [`Op::UserInput`], but contains additional context required
54    /// for a turn of a [`crate::codex_conversation::CodexConversation`].
55    UserTurn {
56        /// User input items, see `InputItem`
57        items: Vec<InputItem>,
58
59        /// `cwd` to use with the [`SandboxPolicy`] and potentially tool calls
60        /// such as `local_shell`.
61        cwd: PathBuf,
62
63        /// Policy to use for command approval.
64        approval_policy: AskForApproval,
65
66        /// Policy to use for tool calls such as `local_shell`.
67        sandbox_policy: SandboxPolicy,
68
69        /// Must be a valid model slug for the [`crate::client::ModelClient`]
70        /// associated with this conversation.
71        model: String,
72
73        /// Will only be honored if the model is configured to use reasoning.
74        effort: ReasoningEffortConfig,
75
76        /// Will only be honored if the model is configured to use reasoning.
77        summary: ReasoningSummaryConfig,
78    },
79
80    /// Override parts of the persistent turn context for subsequent turns.
81    ///
82    /// All fields are optional; when omitted, the existing value is preserved.
83    /// This does not enqueue any input – it only updates defaults used for
84    /// future `UserInput` turns.
85    OverrideTurnContext {
86        /// Updated `cwd` for sandbox/tool calls.
87        #[serde(skip_serializing_if = "Option::is_none")]
88        cwd: Option<PathBuf>,
89
90        /// Updated command approval policy.
91        #[serde(skip_serializing_if = "Option::is_none")]
92        approval_policy: Option<AskForApproval>,
93
94        /// Updated sandbox policy for tool calls.
95        #[serde(skip_serializing_if = "Option::is_none")]
96        sandbox_policy: Option<SandboxPolicy>,
97
98        /// Updated model slug. When set, the model family is derived
99        /// automatically.
100        #[serde(skip_serializing_if = "Option::is_none")]
101        model: Option<String>,
102
103        /// Updated reasoning effort (honored only for reasoning-capable models).
104        #[serde(skip_serializing_if = "Option::is_none")]
105        effort: Option<ReasoningEffortConfig>,
106
107        /// Updated reasoning summary preference (honored only for reasoning-capable models).
108        #[serde(skip_serializing_if = "Option::is_none")]
109        summary: Option<ReasoningSummaryConfig>,
110    },
111
112    /// Approve a command execution
113    ExecApproval {
114        /// The id of the submission we are approving
115        id: String,
116        /// The user's decision in response to the request.
117        decision: ReviewDecision,
118    },
119
120    /// Approve a code patch
121    PatchApproval {
122        /// The id of the submission we are approving
123        id: String,
124        /// The user's decision in response to the request.
125        decision: ReviewDecision,
126    },
127
128    /// Append an entry to the persistent cross-session message history.
129    ///
130    /// Note the entry is not guaranteed to be logged if the user has
131    /// history disabled, it matches the list of "sensitive" patterns, etc.
132    AddToHistory {
133        /// The message text to be stored.
134        text: String,
135    },
136
137    /// Request a single history entry identified by `log_id` + `offset`.
138    GetHistoryEntryRequest { offset: usize, log_id: u64 },
139
140    /// Request the list of MCP tools available across all configured servers.
141    /// Reply is delivered via `EventMsg::McpListToolsResponse`.
142    ListMcpTools,
143
144    /// Request the agent to summarize the current conversation context.
145    /// The agent will use its existing context (either conversation history or previous response id)
146    /// to generate a summary which will be returned as an AgentMessage event.
147    Compact,
148    /// Request to shut down codex instance.
149    Shutdown,
150}
151
152/// Determines the conditions under which the user is consulted to approve
153/// running the command proposed by Codex.
154#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Display, TS)]
155#[serde(rename_all = "kebab-case")]
156#[strum(serialize_all = "kebab-case")]
157pub enum AskForApproval {
158    /// Under this policy, only "known safe" commands—as determined by
159    /// `is_safe_command()`—that **only read files** are auto‑approved.
160    /// Everything else will ask the user to approve.
161    #[serde(rename = "untrusted")]
162    #[strum(serialize = "untrusted")]
163    UnlessTrusted,
164
165    /// *All* commands are auto‑approved, but they are expected to run inside a
166    /// sandbox where network access is disabled and writes are confined to a
167    /// specific set of paths. If the command fails, it will be escalated to
168    /// the user to approve execution without a sandbox.
169    OnFailure,
170
171    /// The model decides when to ask the user for approval.
172    #[default]
173    OnRequest,
174
175    /// Never ask the user to approve commands. Failures are immediately returned
176    /// to the model, and never escalated to the user for approval.
177    Never,
178}
179
180/// Determines execution restrictions for model shell commands.
181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, TS)]
182#[strum(serialize_all = "kebab-case")]
183#[serde(tag = "mode", rename_all = "kebab-case")]
184pub enum SandboxPolicy {
185    /// No restrictions whatsoever. Use with caution.
186    #[serde(rename = "danger-full-access")]
187    DangerFullAccess,
188
189    /// Read-only access to the entire file-system.
190    #[serde(rename = "read-only")]
191    ReadOnly,
192
193    /// Same as `ReadOnly` but additionally grants write access to the current
194    /// working directory ("workspace").
195    #[serde(rename = "workspace-write")]
196    WorkspaceWrite {
197        /// Additional folders (beyond cwd and possibly TMPDIR) that should be
198        /// writable from within the sandbox.
199        #[serde(default, skip_serializing_if = "Vec::is_empty")]
200        writable_roots: Vec<PathBuf>,
201
202        /// When set to `true`, outbound network access is allowed. `false` by
203        /// default.
204        #[serde(default)]
205        network_access: bool,
206
207        /// When set to `true`, will NOT include the per-user `TMPDIR`
208        /// environment variable among the default writable roots. Defaults to
209        /// `false`.
210        #[serde(default)]
211        exclude_tmpdir_env_var: bool,
212
213        /// When set to `true`, will NOT include the `/tmp` among the default
214        /// writable roots on UNIX. Defaults to `false`.
215        #[serde(default)]
216        exclude_slash_tmp: bool,
217    },
218}
219
220/// A writable root path accompanied by a list of subpaths that should remain
221/// read‑only even when the root is writable. This is primarily used to ensure
222/// top‑level VCS metadata directories (e.g. `.git`) under a writable root are
223/// not modified by the agent.
224#[derive(Debug, Clone, PartialEq, Eq)]
225pub struct WritableRoot {
226    /// Absolute path, by construction.
227    pub root: PathBuf,
228
229    /// Also absolute paths, by construction.
230    pub read_only_subpaths: Vec<PathBuf>,
231}
232
233impl WritableRoot {
234    pub fn is_path_writable(&self, path: &Path) -> bool {
235        // Check if the path is under the root.
236        if !path.starts_with(&self.root) {
237            return false;
238        }
239
240        // Check if the path is under any of the read-only subpaths.
241        for subpath in &self.read_only_subpaths {
242            if path.starts_with(subpath) {
243                return false;
244            }
245        }
246
247        true
248    }
249}
250
251impl FromStr for SandboxPolicy {
252    type Err = serde_json::Error;
253
254    fn from_str(s: &str) -> Result<Self, Self::Err> {
255        serde_json::from_str(s)
256    }
257}
258
259impl SandboxPolicy {
260    /// Returns a policy with read-only disk access and no network.
261    pub const fn new_read_only_policy() -> Self {
262        SandboxPolicy::ReadOnly
263    }
264
265    /// Returns a policy that can read the entire disk, but can only write to
266    /// the current working directory and the per-user tmp dir on macOS. It does
267    /// not allow network access.
268    pub const fn new_workspace_write_policy() -> Self {
269        SandboxPolicy::WorkspaceWrite {
270            writable_roots: vec![],
271            network_access: false,
272            exclude_tmpdir_env_var: false,
273            exclude_slash_tmp: false,
274        }
275    }
276
277    /// Always returns `true`; restricting read access is not supported.
278    pub const fn has_full_disk_read_access(&self) -> bool {
279        true
280    }
281
282    pub const fn has_full_disk_write_access(&self) -> bool {
283        match self {
284            SandboxPolicy::DangerFullAccess => true,
285            SandboxPolicy::ReadOnly => false,
286            SandboxPolicy::WorkspaceWrite { .. } => false,
287        }
288    }
289
290    pub const fn has_full_network_access(&self) -> bool {
291        match self {
292            SandboxPolicy::DangerFullAccess => true,
293            SandboxPolicy::ReadOnly => false,
294            SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access,
295        }
296    }
297
298    /// Returns the list of writable roots (tailored to the current working
299    /// directory) together with subpaths that should remain read‑only under
300    /// each writable root.
301    pub fn get_writable_roots_with_cwd(&self, cwd: &Path) -> Vec<WritableRoot> {
302        match self {
303            SandboxPolicy::DangerFullAccess => Vec::new(),
304            SandboxPolicy::ReadOnly => Vec::new(),
305            SandboxPolicy::WorkspaceWrite {
306                writable_roots,
307                exclude_tmpdir_env_var,
308                exclude_slash_tmp,
309                network_access: _,
310            } => {
311                // Start from explicitly configured writable roots.
312                let mut roots: Vec<PathBuf> = writable_roots.clone();
313
314                // Always include defaults: cwd, /tmp (if present on Unix), and
315                // on macOS, the per-user TMPDIR unless explicitly excluded.
316                roots.push(cwd.to_path_buf());
317
318                // Include /tmp on Unix unless explicitly excluded.
319                if cfg!(unix) && !exclude_slash_tmp {
320                    let slash_tmp = PathBuf::from("/tmp");
321                    if slash_tmp.is_dir() {
322                        roots.push(slash_tmp);
323                    }
324                }
325
326                // Include $TMPDIR unless explicitly excluded. On macOS, TMPDIR
327                // is per-user, so writes to TMPDIR should not be readable by
328                // other users on the system.
329                //
330                // By comparison, TMPDIR is not guaranteed to be defined on
331                // Linux or Windows, but supporting it here gives users a way to
332                // provide the model with their own temporary directory without
333                // having to hardcode it in the config.
334                if !exclude_tmpdir_env_var
335                    && let Some(tmpdir) = std::env::var_os("TMPDIR")
336                    && !tmpdir.is_empty()
337                {
338                    roots.push(PathBuf::from(tmpdir));
339                }
340
341                // For each root, compute subpaths that should remain read-only.
342                roots
343                    .into_iter()
344                    .map(|writable_root| {
345                        let mut subpaths = Vec::new();
346                        let top_level_git = writable_root.join(".git");
347                        if top_level_git.is_dir() {
348                            subpaths.push(top_level_git);
349                        }
350                        WritableRoot {
351                            root: writable_root,
352                            read_only_subpaths: subpaths,
353                        }
354                    })
355                    .collect()
356            }
357        }
358    }
359}
360
361/// User input
362#[non_exhaustive]
363#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
364#[serde(tag = "type", rename_all = "snake_case")]
365pub enum InputItem {
366    Text {
367        text: String,
368    },
369    /// Pre‑encoded data: URI image.
370    Image {
371        image_url: String,
372    },
373
374    /// Local image path provided by the user.  This will be converted to an
375    /// `Image` variant (base64 data URL) during request serialization.
376    LocalImage {
377        path: std::path::PathBuf,
378    },
379}
380
381/// Event Queue Entry - events from agent
382#[derive(Debug, Clone, Deserialize, Serialize)]
383pub struct Event {
384    /// Submission `id` that this event is correlated with.
385    pub id: String,
386    /// Payload
387    pub msg: EventMsg,
388}
389
390/// Response event from the agent
391#[derive(Debug, Clone, Deserialize, Serialize, Display)]
392#[serde(tag = "type", rename_all = "snake_case")]
393#[strum(serialize_all = "snake_case")]
394pub enum EventMsg {
395    /// Error while executing a submission
396    Error(ErrorEvent),
397
398    /// Agent has started a task
399    TaskStarted,
400
401    /// Agent has completed all actions
402    TaskComplete(TaskCompleteEvent),
403
404    /// Token count event, sent periodically to report the number of tokens
405    /// used in the current session.
406    TokenCount(TokenUsage),
407
408    /// Agent text output message
409    AgentMessage(AgentMessageEvent),
410
411    /// Agent text output delta message
412    AgentMessageDelta(AgentMessageDeltaEvent),
413
414    /// Reasoning event from agent.
415    AgentReasoning(AgentReasoningEvent),
416
417    /// Agent reasoning delta event from agent.
418    AgentReasoningDelta(AgentReasoningDeltaEvent),
419
420    /// Raw chain-of-thought from agent.
421    AgentReasoningRawContent(AgentReasoningRawContentEvent),
422
423    /// Agent reasoning content delta event from agent.
424    AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent),
425    /// Signaled when the model begins a new reasoning summary section (e.g., a new titled block).
426    AgentReasoningSectionBreak(AgentReasoningSectionBreakEvent),
427
428    /// Ack the client's configure message.
429    SessionConfigured(SessionConfiguredEvent),
430
431    McpToolCallBegin(McpToolCallBeginEvent),
432
433    McpToolCallEnd(McpToolCallEndEvent),
434
435    /// Notification that the server is about to execute a command.
436    ExecCommandBegin(ExecCommandBeginEvent),
437
438    /// Incremental chunk of output from a running command.
439    ExecCommandOutputDelta(ExecCommandOutputDeltaEvent),
440
441    ExecCommandEnd(ExecCommandEndEvent),
442
443    ExecApprovalRequest(ExecApprovalRequestEvent),
444
445    ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent),
446
447    BackgroundEvent(BackgroundEventEvent),
448
449    /// Notification that the agent is about to apply a code patch. Mirrors
450    /// `ExecCommandBegin` so front‑ends can show progress indicators.
451    PatchApplyBegin(PatchApplyBeginEvent),
452
453    /// Notification that a patch application has finished.
454    PatchApplyEnd(PatchApplyEndEvent),
455
456    TurnDiff(TurnDiffEvent),
457
458    /// Response to GetHistoryEntryRequest.
459    GetHistoryEntryResponse(GetHistoryEntryResponseEvent),
460
461    /// List of MCP tools available to the agent.
462    McpListToolsResponse(McpListToolsResponseEvent),
463
464    PlanUpdate(UpdatePlanArgs),
465
466    TurnAborted(TurnAbortedEvent),
467
468    /// Notification that the agent is shutting down.
469    ShutdownComplete,
470}
471
472// Individual event payload types matching each `EventMsg` variant.
473
474#[derive(Debug, Clone, Deserialize, Serialize)]
475pub struct ErrorEvent {
476    pub message: String,
477}
478
479#[derive(Debug, Clone, Deserialize, Serialize)]
480pub struct TaskCompleteEvent {
481    pub last_agent_message: Option<String>,
482}
483
484#[derive(Debug, Clone, Deserialize, Serialize, Default)]
485pub struct TokenUsage {
486    pub input_tokens: u64,
487    pub cached_input_tokens: Option<u64>,
488    pub output_tokens: u64,
489    pub reasoning_output_tokens: Option<u64>,
490    pub total_tokens: u64,
491}
492
493impl TokenUsage {
494    pub const fn is_zero(&self) -> bool {
495        self.total_tokens == 0
496    }
497
498    pub fn cached_input(&self) -> u64 {
499        self.cached_input_tokens.unwrap_or(0)
500    }
501
502    pub fn non_cached_input(&self) -> u64 {
503        self.input_tokens.saturating_sub(self.cached_input())
504    }
505
506    /// Primary count for display as a single absolute value: non-cached input + output.
507    pub fn blended_total(&self) -> u64 {
508        self.non_cached_input() + self.output_tokens
509    }
510
511    /// For estimating what % of the model's context window is used, we need to account
512    /// for reasoning output tokens from prior turns being dropped from the context window.
513    /// We approximate this here by subtracting reasoning output tokens from the total.
514    /// This will be off for the current turn and pending function calls.
515    pub fn tokens_in_context_window(&self) -> u64 {
516        self.total_tokens
517            .saturating_sub(self.reasoning_output_tokens.unwrap_or(0))
518    }
519
520    /// Estimate the remaining user-controllable percentage of the model's context window.
521    ///
522    /// `context_window` is the total size of the model's context window.
523    /// `baseline_used_tokens` should capture tokens that are always present in
524    /// the context (e.g., system prompt and fixed tool instructions) so that
525    /// the percentage reflects the portion the user can influence.
526    ///
527    /// This normalizes both the numerator and denominator by subtracting the
528    /// baseline, so immediately after the first prompt the UI shows 100% left
529    /// and trends toward 0% as the user fills the effective window.
530    pub fn percent_of_context_window_remaining(
531        &self,
532        context_window: u64,
533        baseline_used_tokens: u64,
534    ) -> u8 {
535        if context_window <= baseline_used_tokens {
536            return 0;
537        }
538
539        let effective_window = context_window - baseline_used_tokens;
540        let used = self
541            .tokens_in_context_window()
542            .saturating_sub(baseline_used_tokens);
543        let remaining = effective_window.saturating_sub(used);
544        ((remaining as f32 / effective_window as f32) * 100.0).clamp(0.0, 100.0) as u8
545    }
546}
547
548#[derive(Debug, Clone, Deserialize, Serialize)]
549pub struct FinalOutput {
550    pub token_usage: TokenUsage,
551}
552
553impl From<TokenUsage> for FinalOutput {
554    fn from(token_usage: TokenUsage) -> Self {
555        Self { token_usage }
556    }
557}
558
559impl fmt::Display for FinalOutput {
560    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
561        let token_usage = &self.token_usage;
562        write!(
563            f,
564            "Token usage: total={} input={}{} output={}{}",
565            token_usage.blended_total(),
566            token_usage.non_cached_input(),
567            if token_usage.cached_input() > 0 {
568                format!(" (+ {} cached)", token_usage.cached_input())
569            } else {
570                String::new()
571            },
572            token_usage.output_tokens,
573            token_usage
574                .reasoning_output_tokens
575                .map(|r| format!(" (reasoning {r})"))
576                .unwrap_or_default()
577        )
578    }
579}
580
581#[derive(Debug, Clone, Deserialize, Serialize)]
582pub struct AgentMessageEvent {
583    pub message: String,
584}
585
586#[derive(Debug, Clone, Deserialize, Serialize)]
587pub struct AgentMessageDeltaEvent {
588    pub delta: String,
589}
590
591#[derive(Debug, Clone, Deserialize, Serialize)]
592pub struct AgentReasoningEvent {
593    pub text: String,
594}
595
596#[derive(Debug, Clone, Deserialize, Serialize)]
597pub struct AgentReasoningRawContentEvent {
598    pub text: String,
599}
600
601#[derive(Debug, Clone, Deserialize, Serialize)]
602pub struct AgentReasoningRawContentDeltaEvent {
603    pub delta: String,
604}
605
606#[derive(Debug, Clone, Deserialize, Serialize)]
607pub struct AgentReasoningSectionBreakEvent {}
608
609#[derive(Debug, Clone, Deserialize, Serialize)]
610pub struct AgentReasoningDeltaEvent {
611    pub delta: String,
612}
613
614#[derive(Debug, Clone, Deserialize, Serialize)]
615pub struct McpInvocation {
616    /// Name of the MCP server as defined in the config.
617    pub server: String,
618    /// Name of the tool as given by the MCP server.
619    pub tool: String,
620    /// Arguments to the tool call.
621    pub arguments: Option<serde_json::Value>,
622}
623
624#[derive(Debug, Clone, Deserialize, Serialize)]
625pub struct McpToolCallBeginEvent {
626    /// Identifier so this can be paired with the McpToolCallEnd event.
627    pub call_id: String,
628    pub invocation: McpInvocation,
629}
630
631#[derive(Debug, Clone, Deserialize, Serialize)]
632pub struct McpToolCallEndEvent {
633    /// Identifier for the corresponding McpToolCallBegin that finished.
634    pub call_id: String,
635    pub invocation: McpInvocation,
636    pub duration: Duration,
637    /// Result of the tool call. Note this could be an error.
638    pub result: Result<CallToolResult, String>,
639}
640
641impl McpToolCallEndEvent {
642    pub fn is_success(&self) -> bool {
643        match &self.result {
644            Ok(result) => !result.is_error.unwrap_or(false),
645            Err(_) => false,
646        }
647    }
648}
649
650#[derive(Debug, Clone, Deserialize, Serialize)]
651pub struct ExecCommandBeginEvent {
652    /// Identifier so this can be paired with the ExecCommandEnd event.
653    pub call_id: String,
654    /// The command to be executed.
655    pub command: Vec<String>,
656    /// The command's working directory if not the default cwd for the agent.
657    pub cwd: PathBuf,
658    pub parsed_cmd: Vec<ParsedCommand>,
659}
660
661#[derive(Debug, Clone, Deserialize, Serialize)]
662pub struct ExecCommandEndEvent {
663    /// Identifier for the ExecCommandBegin that finished.
664    pub call_id: String,
665    /// Captured stdout
666    pub stdout: String,
667    /// Captured stderr
668    pub stderr: String,
669    /// The command's exit code.
670    pub exit_code: i32,
671    /// The duration of the command execution.
672    pub duration: Duration,
673}
674
675#[derive(Debug, Clone, Deserialize, Serialize)]
676#[serde(rename_all = "snake_case")]
677pub enum ExecOutputStream {
678    Stdout,
679    Stderr,
680}
681
682#[derive(Debug, Clone, Deserialize, Serialize)]
683pub struct ExecCommandOutputDeltaEvent {
684    /// Identifier for the ExecCommandBegin that produced this chunk.
685    pub call_id: String,
686    /// Which stream produced this chunk.
687    pub stream: ExecOutputStream,
688    /// Raw bytes from the stream (may not be valid UTF-8).
689    #[serde(with = "serde_bytes")]
690    pub chunk: ByteBuf,
691}
692
693#[derive(Debug, Clone, Deserialize, Serialize)]
694pub struct ExecApprovalRequestEvent {
695    /// Identifier for the associated exec call, if available.
696    pub call_id: String,
697    /// The command to be executed.
698    pub command: Vec<String>,
699    /// The command's working directory.
700    pub cwd: PathBuf,
701    /// Optional human-readable reason for the approval (e.g. retry without sandbox).
702    #[serde(skip_serializing_if = "Option::is_none")]
703    pub reason: Option<String>,
704}
705
706#[derive(Debug, Clone, Deserialize, Serialize)]
707pub struct ApplyPatchApprovalRequestEvent {
708    /// Responses API call id for the associated patch apply call, if available.
709    pub call_id: String,
710    pub changes: HashMap<PathBuf, FileChange>,
711    /// Optional explanatory reason (e.g. request for extra write access).
712    #[serde(skip_serializing_if = "Option::is_none")]
713    pub reason: Option<String>,
714    /// When set, the agent is asking the user to allow writes under this root for the remainder of the session.
715    #[serde(skip_serializing_if = "Option::is_none")]
716    pub grant_root: Option<PathBuf>,
717}
718
719#[derive(Debug, Clone, Deserialize, Serialize)]
720pub struct BackgroundEventEvent {
721    pub message: String,
722}
723
724#[derive(Debug, Clone, Deserialize, Serialize)]
725pub struct PatchApplyBeginEvent {
726    /// Identifier so this can be paired with the PatchApplyEnd event.
727    pub call_id: String,
728    /// If true, there was no ApplyPatchApprovalRequest for this patch.
729    pub auto_approved: bool,
730    /// The changes to be applied.
731    pub changes: HashMap<PathBuf, FileChange>,
732}
733
734#[derive(Debug, Clone, Deserialize, Serialize)]
735pub struct PatchApplyEndEvent {
736    /// Identifier for the PatchApplyBegin that finished.
737    pub call_id: String,
738    /// Captured stdout (summary printed by apply_patch).
739    pub stdout: String,
740    /// Captured stderr (parser errors, IO failures, etc.).
741    pub stderr: String,
742    /// Whether the patch was applied successfully.
743    pub success: bool,
744}
745
746#[derive(Debug, Clone, Deserialize, Serialize)]
747pub struct TurnDiffEvent {
748    pub unified_diff: String,
749}
750
751#[derive(Debug, Clone, Deserialize, Serialize)]
752pub struct GetHistoryEntryResponseEvent {
753    pub offset: usize,
754    pub log_id: u64,
755    /// The entry at the requested offset, if available and parseable.
756    #[serde(skip_serializing_if = "Option::is_none")]
757    pub entry: Option<HistoryEntry>,
758}
759
760/// Response payload for `Op::ListMcpTools`.
761#[derive(Debug, Clone, Deserialize, Serialize)]
762pub struct McpListToolsResponseEvent {
763    /// Fully qualified tool name -> tool definition.
764    pub tools: std::collections::HashMap<String, McpTool>,
765}
766
767#[derive(Debug, Default, Clone, Deserialize, Serialize)]
768pub struct SessionConfiguredEvent {
769    /// Unique id for this session.
770    pub session_id: Uuid,
771
772    /// Tell the client what model is being queried.
773    pub model: String,
774
775    /// Identifier of the history log file (inode on Unix, 0 otherwise).
776    pub history_log_id: u64,
777
778    /// Current number of entries in the history log.
779    pub history_entry_count: usize,
780}
781
782/// User's decision in response to an ExecApprovalRequest.
783#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, TS)]
784#[serde(rename_all = "snake_case")]
785pub enum ReviewDecision {
786    /// User has approved this command and the agent should execute it.
787    Approved,
788
789    /// User has approved this command and wants to automatically approve any
790    /// future identical instances (`command` and `cwd` match exactly) for the
791    /// remainder of the session.
792    ApprovedForSession,
793
794    /// User has denied this command and the agent should not execute it, but
795    /// it should continue the session and try something else.
796    #[default]
797    Denied,
798
799    /// User has denied this command and the agent should not do anything until
800    /// the user's next command.
801    Abort,
802}
803
804#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)]
805#[serde(rename_all = "snake_case")]
806pub enum FileChange {
807    Add {
808        content: String,
809    },
810    Delete,
811    Update {
812        unified_diff: String,
813        move_path: Option<PathBuf>,
814    },
815}
816
817#[derive(Debug, Clone, Deserialize, Serialize)]
818pub struct Chunk {
819    /// 1-based line index of the first line in the original file
820    pub orig_index: u32,
821    pub deleted_lines: Vec<String>,
822    pub inserted_lines: Vec<String>,
823}
824
825#[derive(Debug, Clone, Deserialize, Serialize)]
826pub struct TurnAbortedEvent {
827    pub reason: TurnAbortReason,
828}
829
830#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)]
831#[serde(rename_all = "snake_case")]
832pub enum TurnAbortReason {
833    Interrupted,
834    Replaced,
835}
836
837#[cfg(test)]
838mod tests {
839    use super::*;
840
841    /// Serialize Event to verify that its JSON representation has the expected
842    /// amount of nesting.
843    #[test]
844    fn serialize_event() {
845        let session_id: Uuid = uuid::uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8");
846        let event = Event {
847            id: "1234".to_string(),
848            msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
849                session_id,
850                model: "agcodex-mini-latest".to_string(),
851                history_log_id: 0,
852                history_entry_count: 0,
853            }),
854        };
855        let serialized = serde_json::to_string(&event).unwrap();
856        assert_eq!(
857            serialized,
858            r#"{"id":"1234","msg":{"type":"session_configured","session_id":"67e55044-10b1-426f-9247-bb680e5fe0c8","model":"agcodex-mini-latest","history_log_id":0,"history_entry_count":0}}"#
859        );
860    }
861}