Codex Handbook
protocol/src/protocol.rs 5585 lines
//! Defines the protocol for a Codex session between a client and an agent.//!//! Uses a SQ (Submission Queue) / EQ (Event Queue) pattern to asynchronously communicate//! between user and agent.use std::collections::BTreeMap;use std::collections::HashMap;use std::fmt;use std::ops::Mul;use std::path::Path;use std::path::PathBuf;use std::str::FromStr;use std::time::Duration;use strum_macros::EnumIter;use crate::AgentPath;use crate::SessionId;use crate::ThreadId;use crate::approvals::ElicitationRequestEvent;use crate::config_types::ApprovalsReviewer;use crate::config_types::CollaborationMode;use crate::config_types::ModeKind;use crate::config_types::Personality;use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;use crate::config_types::WindowsSandboxLevel;use crate::dynamic_tools::DynamicToolCallOutputContentItem;use crate::dynamic_tools::DynamicToolCallRequest;use crate::dynamic_tools::DynamicToolResponse;use crate::dynamic_tools::DynamicToolSpec;use crate::items::TurnItem;use crate::mcp::CallToolResult;use crate::mcp::RequestId;use crate::memory_citation::MemoryCitation;use crate::models::ActivePermissionProfile;use crate::models::AgentMessageInputContent;use crate::models::BaseInstructions;use crate::models::ContentItem;use crate::models::ImageDetail;use crate::models::MessagePhase;use crate::models::PermissionProfile;use crate::models::ResponseInputItem;use crate::models::ResponseItem;use crate::models::ResponseItemMetadata;use crate::models::SandboxEnforcement;use crate::models::WebSearchAction;use crate::num_format::format_with_separators;use crate::openai_models::ReasoningEffort as ReasoningEffortConfig;use crate::parse_command::ParsedCommand;use crate::plan_tool::UpdatePlanArgs;use crate::request_permissions::RequestPermissionsEvent;use crate::request_permissions::RequestPermissionsResponse;use crate::request_user_input::RequestUserInputResponse;use crate::user_input::UserInput;use codex_utils_absolute_path::AbsolutePathBuf;use codex_utils_path_uri::PathUri;use schemars::JsonSchema;use serde::Deserialize;use serde::Serialize;use serde_json::Value;use serde_with::serde_as;use strum_macros::Display;use tracing::error;use ts_rs::TS;pub use crate::approvals::ApplyPatchApprovalRequestEvent;pub use crate::approvals::ElicitationAction;pub use crate::approvals::ExecApprovalRequestEvent;pub use crate::approvals::ExecPolicyAmendment;pub use crate::approvals::GuardianAssessmentAction;pub use crate::approvals::GuardianAssessmentDecisionSource;pub use crate::approvals::GuardianAssessmentEvent;pub use crate::approvals::GuardianAssessmentOutcome;pub use crate::approvals::GuardianAssessmentStatus;pub use crate::approvals::GuardianCommandSource;pub use crate::approvals::GuardianRiskLevel;pub use crate::approvals::GuardianUserAuthorization;pub use crate::approvals::NetworkApprovalContext;pub use crate::approvals::NetworkApprovalProtocol;pub use crate::approvals::NetworkPolicyAmendment;pub use crate::approvals::NetworkPolicyRuleAction;pub use crate::permissions::FileSystemAccessMode;pub use crate::permissions::FileSystemPath;pub use crate::permissions::FileSystemSandboxEntry;pub use crate::permissions::FileSystemSandboxKind;pub use crate::permissions::FileSystemSandboxPolicy;pub use crate::permissions::FileSystemSpecialPath;pub use crate::permissions::NetworkSandboxPolicy;use crate::permissions::default_read_only_subpaths_for_writable_root;pub use crate::request_permissions::RequestPermissionsArgs;pub use crate::request_user_input::RequestUserInputEvent;/// Open/close tags for special user-input blocks. Used across crates to avoid/// duplicated hardcoded strings.pub const USER_INSTRUCTIONS_OPEN_TAG: &str = "<user_instructions>";pub const USER_INSTRUCTIONS_CLOSE_TAG: &str = "</user_instructions>";pub const ENVIRONMENT_CONTEXT_OPEN_TAG: &str = "<environment_context>";pub const ENVIRONMENT_CONTEXT_CLOSE_TAG: &str = "</environment_context>";pub const APPS_INSTRUCTIONS_OPEN_TAG: &str = "<apps_instructions>";pub const APPS_INSTRUCTIONS_CLOSE_TAG: &str = "</apps_instructions>";pub const SKILLS_INSTRUCTIONS_OPEN_TAG: &str = "<skills_instructions>";pub const SKILLS_INSTRUCTIONS_CLOSE_TAG: &str = "</skills_instructions>";pub const PLUGINS_INSTRUCTIONS_OPEN_TAG: &str = "<plugins_instructions>";pub const PLUGINS_INSTRUCTIONS_CLOSE_TAG: &str = "</plugins_instructions>";pub const COLLABORATION_MODE_OPEN_TAG: &str = "<collaboration_mode>";pub const COLLABORATION_MODE_CLOSE_TAG: &str = "</collaboration_mode>";pub const REALTIME_CONVERSATION_OPEN_TAG: &str = "<realtime_conversation>";pub const REALTIME_CONVERSATION_CLOSE_TAG: &str = "</realtime_conversation>";pub const USER_MESSAGE_BEGIN: &str = "## My request for Codex:";// TODO(anp): Replace `TurnEnvironmentSelection` with `PathUri` once path URIs carry environment// identifiers.#[derive(Debug, Clone, PartialEq)]pub struct TurnEnvironmentSelection {    pub environment_id: String,    pub cwd: PathUri,}#[derive(Debug, Clone, PartialEq)]pub struct TurnEnvironmentSelections {    pub legacy_fallback_cwd: AbsolutePathBuf,    pub environments: Vec<TurnEnvironmentSelection>,}impl TurnEnvironmentSelections {    pub fn new(        legacy_fallback_cwd: AbsolutePathBuf,        environments: Vec<TurnEnvironmentSelection>,    ) -> Self {        Self {            legacy_fallback_cwd,            environments,        }    }}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)]#[serde(transparent)]#[ts(type = "string")]pub struct GitSha(pub String);impl GitSha {    pub fn new(sha: &str) -> Self {        Self(sha.to_string())    }}/// Submission Queue Entry - requests from user#[derive(Debug, Clone)]pub struct Submission {    /// Unique id for this Submission to correlate with Events    pub id: String,    /// Payload    pub op: Op,    /// Client-provided id for the user message represented by `Op::UserInput`.    pub client_user_message_id: Option<String>,    /// Optional W3C trace carrier propagated across async submission handoffs.    pub trace: Option<W3cTraceContext>,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]pub struct W3cTraceContext {    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub traceparent: Option<String>,    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub tracestate: Option<String>,}/// Config payload for refreshing MCP servers.#[derive(Debug, Clone, PartialEq)]pub struct McpServerRefreshConfig {    /// Complete runtime server map after source and thread-scoped resolution.    pub mcp_servers: Value,    /// OAuth credential store mode to use with this server snapshot.    pub mcp_oauth_credentials_store_mode: Value,    pub auth_keyring_backend_kind: Value,}#[derive(Debug, Clone, PartialEq)]pub struct ConversationStartParams {    /// Overrides the configured realtime architecture for this session only.    pub architecture: Option<RealtimeConversationArchitecture>,    /// Sends automatic Codex responses as realtime conversation items instead of handoff appends.    pub codex_responses_as_items: bool,    /// Optional prefix added to automatic Codex response items when `codex_responses_as_items` is set.    pub codex_response_item_prefix: Option<String>,    /// Overrides the configured realtime model for this session only.    pub model: Option<String>,    /// Selects whether the realtime session should produce text or audio output.    pub output_modality: RealtimeOutputModality,    /// Whether to append Codex's startup context to the realtime backend prompt.    pub include_startup_context: bool,    pub prompt: Option<Option<String>>,    pub realtime_session_id: Option<String>,    pub transport: Option<ConversationStartTransport>,    /// Overrides the configured realtime protocol version for this session only.    pub version: Option<RealtimeConversationVersion>,    pub voice: Option<RealtimeVoice>,}#[derive(Debug, Clone, PartialEq)]pub enum ConversationStartTransport {    Websocket,    Webrtc { sdp: String },}#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub enum RealtimeOutputModality {    Text,    Audio,}#[derive(    Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash, JsonSchema, TS, Ord, PartialOrd,)]#[serde(rename_all = "snake_case")]#[ts(rename_all = "snake_case")]pub enum RealtimeVoice {    Alloy,    Arbor,    Ash,    Ballad,    Breeze,    Cedar,    Coral,    Cove,    Echo,    Ember,    Juniper,    Maple,    Marin,    Sage,    Shimmer,    Sol,    Spruce,    Vale,    Verse,}impl RealtimeVoice {    pub fn wire_name(self) -> &'static str {        match self {            Self::Alloy => "alloy",            Self::Arbor => "arbor",            Self::Ash => "ash",            Self::Ballad => "ballad",            Self::Breeze => "breeze",            Self::Cedar => "cedar",            Self::Coral => "coral",            Self::Cove => "cove",            Self::Echo => "echo",            Self::Ember => "ember",            Self::Juniper => "juniper",            Self::Maple => "maple",            Self::Marin => "marin",            Self::Sage => "sage",            Self::Shimmer => "shimmer",            Self::Sol => "sol",            Self::Spruce => "spruce",            Self::Vale => "vale",            Self::Verse => "verse",        }    }}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "camelCase")]#[ts(rename_all = "camelCase")]pub struct RealtimeVoicesList {    pub v1: Vec<RealtimeVoice>,    pub v2: Vec<RealtimeVoice>,    pub default_v1: RealtimeVoice,    pub default_v2: RealtimeVoice,}impl RealtimeVoicesList {    pub fn builtin() -> Self {        Self {            v1: vec![                RealtimeVoice::Juniper,                RealtimeVoice::Maple,                RealtimeVoice::Spruce,                RealtimeVoice::Ember,                RealtimeVoice::Vale,                RealtimeVoice::Breeze,                RealtimeVoice::Arbor,                RealtimeVoice::Sol,                RealtimeVoice::Cove,            ],            v2: vec![                RealtimeVoice::Alloy,                RealtimeVoice::Ash,                RealtimeVoice::Ballad,                RealtimeVoice::Coral,                RealtimeVoice::Echo,                RealtimeVoice::Sage,                RealtimeVoice::Shimmer,                RealtimeVoice::Verse,                RealtimeVoice::Marin,                RealtimeVoice::Cedar,            ],            default_v1: RealtimeVoice::Cove,            default_v2: RealtimeVoice::Marin,        }    }}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]pub struct RealtimeAudioFrame {    pub data: String,    pub sample_rate: u32,    pub num_channels: u16,    #[serde(skip_serializing_if = "Option::is_none")]    pub samples_per_channel: Option<u32>,    #[serde(skip_serializing_if = "Option::is_none")]    pub item_id: Option<String>,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]pub struct RealtimeTranscriptDelta {    pub delta: String,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]pub struct RealtimeTranscriptDone {    pub text: String,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]pub struct RealtimeTranscriptEntry {    pub role: String,    pub text: String,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]pub struct RealtimeHandoffRequested {    pub handoff_id: String,    pub item_id: String,    pub input_transcript: String,    pub active_transcript: Vec<RealtimeTranscriptEntry>,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]pub struct RealtimeNoopRequested {    pub call_id: String,    pub item_id: String,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]pub struct RealtimeInputAudioSpeechStarted {    pub item_id: Option<String>,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]pub struct RealtimeResponseCancelled {    pub response_id: Option<String>,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]pub struct RealtimeResponseCreated {    pub response_id: Option<String>,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]pub struct RealtimeResponseDone {    pub response_id: Option<String>,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]pub enum RealtimeEvent {    SessionUpdated {        realtime_session_id: String,        instructions: Option<String>,    },    InputAudioSpeechStarted(RealtimeInputAudioSpeechStarted),    InputTranscriptDelta(RealtimeTranscriptDelta),    InputTranscriptDone(RealtimeTranscriptDone),    OutputTranscriptDelta(RealtimeTranscriptDelta),    OutputTranscriptDone(RealtimeTranscriptDone),    AudioOut(RealtimeAudioFrame),    ResponseCreated(RealtimeResponseCreated),    ResponseCancelled(RealtimeResponseCancelled),    ResponseDone(RealtimeResponseDone),    ConversationItemAdded(Value),    ConversationItemDone {        item_id: String,    },    HandoffRequested(RealtimeHandoffRequested),    NoopRequested(RealtimeNoopRequested),    Error(String),}#[derive(Debug, Clone, PartialEq)]pub struct ConversationAudioParams {    pub frame: RealtimeAudioFrame,}#[derive(Debug, Clone, PartialEq)]pub struct ConversationTextParams {    pub text: String,    pub role: ConversationTextRole,}#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize, JsonSchema, TS)]#[serde(rename_all = "snake_case")]#[ts(rename_all = "snake_case")]pub enum ConversationTextRole {    #[default]    User,    Developer,}#[derive(Debug, Clone, PartialEq)]pub struct ConversationSpeechParams {    pub text: String,}/// Persistent thread-settings overrides that can be applied before user input or/// on their own.#[derive(Debug, Clone, Default, PartialEq)]pub struct ThreadSettingsOverrides {    /// Updated fallback `cwd` and environments supplied together as a complete pair.    pub environments: Option<TurnEnvironmentSelections>,    /// Updated runtime workspace roots used to materialize symbolic    /// `:workspace_roots` filesystem permissions.    pub workspace_roots: Option<Vec<AbsolutePathBuf>>,    /// Updated profile-defined workspace roots for status summaries and    /// per-turn config reconstruction.    pub profile_workspace_roots: Option<Vec<AbsolutePathBuf>>,    /// Updated command approval policy.    pub approval_policy: Option<AskForApproval>,    /// Updated approval reviewer for future approval prompts.    pub approvals_reviewer: Option<ApprovalsReviewer>,    /// Updated sandbox policy for tool calls.    pub sandbox_policy: Option<SandboxPolicy>,    /// Updated permissions profile for tool calls.    pub permission_profile: Option<PermissionProfile>,    /// Named or built-in profile that produced `permission_profile`, if the    /// update selected a profile rather than supplying raw permissions.    pub active_permission_profile: Option<ActivePermissionProfile>,    /// Updated Windows sandbox mode for tool execution.    pub windows_sandbox_level: Option<WindowsSandboxLevel>,    /// Updated model slug. When set, the model info is derived automatically.    pub model: Option<String>,    /// Updated reasoning effort (honored only for reasoning-capable models).    ///    /// Use `Some(Some(_))` to set a specific effort, `Some(None)` to clear the    /// effort, or `None` to leave the existing value unchanged.    pub effort: Option<Option<ReasoningEffortConfig>>,    /// Updated reasoning summary preference (honored only for reasoning-capable models).    pub summary: Option<ReasoningSummaryConfig>,    /// Updated service tier preference for future turns.    ///    /// Use `Some(Some(_))` to set a specific tier, `Some(None)` to clear the    /// preference, or `None` to leave the existing value unchanged.    pub service_tier: Option<Option<String>>,    /// EXPERIMENTAL - set a pre-set collaboration mode.    /// Takes precedence over model, effort, and developer instructions if set.    pub collaboration_mode: Option<CollaborationMode>,    /// Updated personality preference.    pub personality: Option<Personality>,}/// Source classification for client-supplied context.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum AdditionalContextKind {    Untrusted,    Application,}/// Client-supplied context keyed by an opaque source identifier.#[derive(Debug, Clone, PartialEq, Eq)]pub struct AdditionalContextEntry {    pub value: String,    pub kind: AdditionalContextKind,}/// Submission operation#[derive(Debug, Clone, PartialEq)]#[allow(clippy::large_enum_variant)]#[non_exhaustive]pub enum Op {    /// Abort current task without terminating background terminal processes.    /// This server sends [`EventMsg::TurnAborted`] in response.    Interrupt,    /// Terminate all running background terminal processes for this thread.    /// Use this when callers intentionally want to stop long-lived background shells.    CleanBackgroundTerminals,    /// Start a realtime conversation stream.    RealtimeConversationStart(ConversationStartParams),    /// Send audio input to the running realtime conversation stream.    RealtimeConversationAudio(ConversationAudioParams),    /// Send text input to the running realtime conversation stream.    RealtimeConversationText(ConversationTextParams),    /// Append speakable text to the running realtime conversation stream.    RealtimeConversationSpeech(ConversationSpeechParams),    /// Close the running realtime conversation stream.    RealtimeConversationClose,    /// Request the list of voices supported by realtime conversation streams.    RealtimeConversationListVoices,    /// User input, optionally with thread-settings overrides applied first.    UserInput {        /// User input items, see `InputItem`        items: Vec<UserInput>,        /// Optional JSON Schema used to constrain the final assistant message for this turn.        final_output_json_schema: Option<Value>,        /// Optional turn-scoped Responses API `client_metadata`.        responsesapi_client_metadata: Option<HashMap<String, String>>,        /// Client-supplied context fragments keyed by an opaque source identifier.        additional_context: BTreeMap<String, AdditionalContextEntry>,        /// Persistent thread-settings overrides to apply before the input.        thread_settings: ThreadSettingsOverrides,    },    /// Apply persistent thread-settings overrides without starting a turn.    ///    /// This uses the same submission queue as turn starts so app-server can    /// preserve caller order between both kinds of mutation.    ThreadSettings {        /// Persistent thread-settings overrides to apply.        thread_settings: ThreadSettingsOverrides,    },    /// Inter-agent communication that should be recorded as agent-message history    /// while still using the normal thread submission lifecycle.    InterAgentCommunication {        communication: InterAgentCommunication,    },    /// Approve a command execution    ExecApproval {        /// The id of the submission we are approving        id: String,        /// Turn id associated with the approval event, when available.        turn_id: Option<String>,        /// The user's decision in response to the request.        decision: ReviewDecision,    },    /// Approve a code patch    PatchApproval {        /// The id of the submission we are approving        id: String,        /// The user's decision in response to the request.        decision: ReviewDecision,    },    /// Resolve an MCP elicitation request.    ResolveElicitation {        /// Name of the MCP server that issued the request.        server_name: String,        /// Request identifier from the MCP server.        request_id: RequestId,        /// User's decision for the request.        decision: ElicitationAction,        /// Structured user input supplied for accepted elicitations.        content: Option<Value>,        /// Optional client metadata associated with the elicitation response.        meta: Option<Value>,    },    /// Resolve a request_user_input tool call.    UserInputAnswer {        /// Turn id for the in-flight request.        id: String,        /// User-provided answers.        response: RequestUserInputResponse,    },    /// Resolve a request_permissions tool call.    RequestPermissionsResponse {        /// Call id for the in-flight request.        id: String,        /// User-granted permissions.        response: RequestPermissionsResponse,    },    /// Resolve a dynamic tool call request.    DynamicToolResponse {        /// Call id for the in-flight request.        id: String,        /// Tool output payload.        response: DynamicToolResponse,    },    /// Request MCP servers to reinitialize and refresh cached tool lists.    RefreshMcpServers { config: McpServerRefreshConfig },    /// Reload user config layer overrides for the active session.    ///    /// This updates runtime config-derived behavior (for example app    /// enable/disable state) without restarting the thread.    ReloadUserConfig,    /// Request the agent to summarize the current conversation context.    /// The agent will use its existing context (either conversation history or previous response id)    /// to generate a summary which will be returned as an AgentMessage event.    Compact,    /// Set whether the thread remains eligible for memory generation.    ///    /// This persists thread-level memory mode metadata without involving the    /// model.    SetThreadMemoryMode { mode: ThreadMemoryMode },    /// Request Codex to drop the last N user turns from in-memory context.    ///    /// This does not attempt to revert local filesystem changes. Clients are    /// responsible for undoing any edits on disk.    ThreadRollback { num_turns: u32 },    /// Request a code review from the agent.    Review { review_request: ReviewRequest },    /// Record that the user approved one retry of a concrete Guardian-denied action.    ApproveGuardianDeniedAction { event: GuardianAssessmentEvent },    /// Request to shut down codex instance.    Shutdown,    /// Execute a user-initiated one-off shell command (triggered by "!cmd").    ///    /// The command string is executed using the user's default shell and may    /// include shell syntax (pipes, redirects, etc.). Output is streamed via    /// `ExecCommand*` events and the UI regains control upon `TurnComplete`.    RunUserShellCommand {        /// The raw command string after '!'        command: String,    },}#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema)]#[serde(rename_all = "lowercase")]pub enum ThreadMemoryMode {    Enabled,    Disabled,}impl From<Vec<UserInput>> for Op {    fn from(value: Vec<UserInput>) -> Self {        Op::UserInput {            items: value,            final_output_json_schema: None,            responsesapi_client_metadata: None,            additional_context: Default::default(),            thread_settings: ThreadSettingsOverrides::default(),        }    }}#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]pub struct InterAgentCommunication {    pub author: AgentPath,    pub recipient: AgentPath,    #[serde(default)]    pub other_recipients: Vec<AgentPath>,    pub content: String,    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub encrypted_content: Option<String>,    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub metadata: Option<ResponseItemMetadata>,    pub trigger_turn: bool,}impl InterAgentCommunication {    pub fn new(        author: AgentPath,        recipient: AgentPath,        other_recipients: Vec<AgentPath>,        content: String,        trigger_turn: bool,    ) -> Self {        Self {            author,            recipient,            other_recipients,            content,            encrypted_content: None,            metadata: None,            trigger_turn,        }    }    pub fn new_encrypted(        author: AgentPath,        recipient: AgentPath,        other_recipients: Vec<AgentPath>,        encrypted_content: String,        trigger_turn: bool,    ) -> Self {        Self {            author,            recipient,            other_recipients,            content: String::new(),            encrypted_content: Some(encrypted_content),            metadata: None,            trigger_turn,        }    }    pub fn to_response_input_item(&self) -> ResponseInputItem {        ResponseInputItem::Message {            role: "assistant".to_string(),            content: vec![ContentItem::OutputText {                text: serde_json::to_string(self).unwrap_or_default(),            }],            phase: Some(MessagePhase::Commentary),        }    }    pub fn to_model_input_item(&self) -> ResponseItem {        let content = match &self.encrypted_content {            Some(encrypted_content) => {                let message_type = if self.trigger_turn {                    "NEW_TASK"                } else {                    "MESSAGE"                };                vec![                    AgentMessageInputContent::InputText {                        text: format!(                            "Message Type: {message_type}\nTask name: {}\nSender: {}\nPayload:\n",                            self.recipient, self.author                        ),                    },                    AgentMessageInputContent::EncryptedContent {                        encrypted_content: encrypted_content.clone(),                    },                ]            }            None => vec![AgentMessageInputContent::InputText {                text: self.content.clone(),            }],        };        ResponseItem::AgentMessage {            author: self.author.to_string(),            recipient: self.recipient.to_string(),            content,            metadata: self.metadata.clone(),        }    }    pub fn is_message_content(content: &[ContentItem]) -> bool {        Self::from_message_content(content).is_some()    }    pub fn from_message_content(content: &[ContentItem]) -> Option<Self> {        match content {            [ContentItem::InputText { text }] | [ContentItem::OutputText { text }] => {                serde_json::from_str(text).ok()            }            _ => None,        }    }}impl Op {    pub fn kind(&self) -> &'static str {        match self {            Self::Interrupt => "interrupt",            Self::CleanBackgroundTerminals => "clean_background_terminals",            Self::RealtimeConversationStart(_) => "realtime_conversation_start",            Self::RealtimeConversationAudio(_) => "realtime_conversation_audio",            Self::RealtimeConversationText(_) => "realtime_conversation_text",            Self::RealtimeConversationSpeech(_) => "realtime_conversation_speech",            Self::RealtimeConversationClose => "realtime_conversation_close",            Self::RealtimeConversationListVoices => "realtime_conversation_list_voices",            Self::UserInput { .. } => "user_input",            Self::ThreadSettings { .. } => "thread_settings",            Self::InterAgentCommunication { .. } => "inter_agent_communication",            Self::ExecApproval { .. } => "exec_approval",            Self::PatchApproval { .. } => "patch_approval",            Self::ResolveElicitation { .. } => "resolve_elicitation",            Self::UserInputAnswer { .. } => "user_input_answer",            Self::RequestPermissionsResponse { .. } => "request_permissions_response",            Self::DynamicToolResponse { .. } => "dynamic_tool_response",            Self::RefreshMcpServers { .. } => "refresh_mcp_servers",            Self::ReloadUserConfig => "reload_user_config",            Self::Compact => "compact",            Self::SetThreadMemoryMode { .. } => "set_thread_memory_mode",            Self::ThreadRollback { .. } => "thread_rollback",            Self::Review { .. } => "review",            Self::ApproveGuardianDeniedAction { .. } => "approve_guardian_denied_action",            Self::Shutdown => "shutdown",            Self::RunUserShellCommand { .. } => "run_user_shell_command",        }    }}/// Determines the conditions under which the user is consulted to approve/// running the command proposed by Codex.#[derive(    Debug,    Clone,    Copy,    Default,    PartialEq,    Eq,    Hash,    Serialize,    Deserialize,    Display,    JsonSchema,    TS,)]#[serde(rename_all = "kebab-case")]#[strum(serialize_all = "kebab-case")]pub enum AskForApproval {    /// Under this policy, only "known safe" commands—as determined by    /// `is_safe_command()`—that **only read files** are auto‑approved.    /// Everything else will ask the user to approve.    #[serde(rename = "untrusted")]    #[strum(serialize = "untrusted")]    UnlessTrusted,    /// DEPRECATED: *All* commands are auto‑approved, but they are expected to    /// run inside a sandbox where network access is disabled and writes are    /// confined to a specific set of paths. If the command fails, it will be    /// escalated to the user to approve execution without a sandbox.    /// Prefer `OnRequest` for interactive runs or `Never` for non-interactive    /// runs.    OnFailure,    /// The model decides when to ask the user for approval.    #[default]    OnRequest,    /// Fine-grained controls for individual approval flows.    ///    /// When a field is `true`, commands in that category are allowed. When it    /// is `false`, those requests are automatically rejected instead of shown    /// to the user.    #[strum(serialize = "granular")]    Granular(GranularApprovalConfig),    /// Never ask the user to approve commands. Failures are immediately returned    /// to the model, and never escalated to the user for approval.    Never,}#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)]pub struct GranularApprovalConfig {    /// Whether to allow shell command approval requests, including inline    /// `with_additional_permissions` and `require_escalated` requests.    pub sandbox_approval: bool,    /// Whether to allow prompts triggered by execpolicy `prompt` rules.    pub rules: bool,    /// Whether to allow approval prompts triggered by skill script execution.    #[serde(default)]    pub skill_approval: bool,    /// Whether to allow prompts triggered by the `request_permissions` tool.    #[serde(default)]    pub request_permissions: bool,    /// Whether to allow MCP elicitation prompts.    pub mcp_elicitations: bool,}impl GranularApprovalConfig {    pub const fn allows_sandbox_approval(self) -> bool {        self.sandbox_approval    }    pub const fn allows_rules_approval(self) -> bool {        self.rules    }    pub const fn allows_skill_approval(self) -> bool {        self.skill_approval    }    pub const fn allows_request_permissions(self) -> bool {        self.request_permissions    }    pub const fn allows_mcp_elicitations(self) -> bool {        self.mcp_elicitations    }}/// Represents whether outbound network access is available to the agent.#[derive(    Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, Default, JsonSchema, TS,)]#[serde(rename_all = "kebab-case")]#[strum(serialize_all = "kebab-case")]pub enum NetworkAccess {    #[default]    Restricted,    Enabled,}impl NetworkAccess {    pub fn is_enabled(self) -> bool {        matches!(self, NetworkAccess::Enabled)    }}/// Determines execution restrictions for model shell commands.#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, JsonSchema, TS)]#[strum(serialize_all = "kebab-case")]#[serde(tag = "type", rename_all = "kebab-case")]pub enum SandboxPolicy {    /// No restrictions whatsoever. Use with caution.    #[serde(rename = "danger-full-access")]    DangerFullAccess,    /// Read-only access configuration.    #[serde(rename = "read-only")]    ReadOnly {        /// When set to `true`, outbound network access is allowed. `false` by        /// default.        #[serde(default, skip_serializing_if = "std::ops::Not::not")]        network_access: bool,    },    /// Indicates the process is already in an external sandbox. Allows full    /// disk access while honoring the provided network setting.    #[serde(rename = "external-sandbox")]    ExternalSandbox {        /// Whether the external sandbox permits outbound network traffic.        #[serde(default)]        network_access: NetworkAccess,    },    /// Same as `ReadOnly` but additionally grants write access to the current    /// working directory ("workspace").    #[serde(rename = "workspace-write")]    WorkspaceWrite {        /// Additional folders (beyond cwd and possibly TMPDIR) that should be        /// writable from within the sandbox.        #[serde(default, skip_serializing_if = "Vec::is_empty")]        writable_roots: Vec<AbsolutePathBuf>,        /// When set to `true`, outbound network access is allowed. `false` by        /// default.        #[serde(default)]        network_access: bool,        /// When set to `true`, will NOT include the per-user `TMPDIR`        /// environment variable among the default writable roots. Defaults to        /// `false`.        #[serde(default)]        exclude_tmpdir_env_var: bool,        /// When set to `true`, will NOT include the `/tmp` among the default        /// writable roots on UNIX. Defaults to `false`.        #[serde(default)]        exclude_slash_tmp: bool,    },}/// A writable root path accompanied by a list of subpaths that should remain/// read‑only even when the root is writable. This is primarily used to ensure/// that folders containing files that could be modified to escalate the/// privileges of the agent (e.g. `.codex`, `.git`, notably `.git/hooks`) under/// a writable root are not modified by the agent.#[derive(Debug, Clone, PartialEq, Eq, JsonSchema)]pub struct WritableRoot {    pub root: AbsolutePathBuf,    /// By construction, these subpaths are all under `root`.    pub read_only_subpaths: Vec<AbsolutePathBuf>,    /// Workspace metadata path names that must not be created or replaced under    /// `root` unless the policy grants an explicit write rule for that metadata    /// path.    pub protected_metadata_names: Vec<String>,}impl WritableRoot {    pub fn is_path_writable(&self, path: &Path) -> bool {        // Check if the path is under the root.        if !path.starts_with(&self.root) {            return false;        }        // Check if the path is under any of the read-only subpaths.        for subpath in &self.read_only_subpaths {            if path.starts_with(subpath) {                return false;            }        }        if self.path_contains_protected_metadata_name(path) {            return false;        }        true    }    fn path_contains_protected_metadata_name(&self, path: &Path) -> bool {        let Ok(relative_path) = path.strip_prefix(&self.root) else {            return false;        };        let Some(first_component) = relative_path.components().next() else {            return false;        };        self.protected_metadata_names            .iter()            .any(|name| first_component.as_os_str() == std::ffi::OsStr::new(name))    }}impl FromStr for SandboxPolicy {    type Err = serde_json::Error;    fn from_str(s: &str) -> Result<Self, Self::Err> {        serde_json::from_str(s)    }}impl FromStr for FileSystemSandboxPolicy {    type Err = serde_json::Error;    fn from_str(s: &str) -> Result<Self, Self::Err> {        serde_json::from_str(s)    }}impl FromStr for NetworkSandboxPolicy {    type Err = serde_json::Error;    fn from_str(s: &str) -> Result<Self, Self::Err> {        serde_json::from_str(s)    }}impl SandboxPolicy {    /// Returns a policy with read-only disk access and no network.    pub fn new_read_only_policy() -> Self {        SandboxPolicy::ReadOnly {            network_access: false,        }    }    /// Returns a policy that can read the entire disk, but can only write to    /// the current working directory and the per-user tmp dir on macOS. It does    /// not allow network access.    pub fn new_workspace_write_policy() -> Self {        SandboxPolicy::WorkspaceWrite {            writable_roots: vec![],            network_access: false,            exclude_tmpdir_env_var: false,            exclude_slash_tmp: false,        }    }    pub fn has_full_disk_read_access(&self) -> bool {        true    }    pub fn has_full_disk_write_access(&self) -> bool {        match self {            SandboxPolicy::DangerFullAccess => true,            SandboxPolicy::ExternalSandbox { .. } => true,            SandboxPolicy::ReadOnly { .. } => false,            SandboxPolicy::WorkspaceWrite { .. } => false,        }    }    pub fn has_full_network_access(&self) -> bool {        match self {            SandboxPolicy::DangerFullAccess => true,            SandboxPolicy::ExternalSandbox { network_access } => network_access.is_enabled(),            SandboxPolicy::ReadOnly { network_access, .. } => *network_access,            SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access,        }    }    /// Returns the list of writable roots (tailored to the current working    /// directory) together with subpaths that should remain read‑only under    /// each writable root.    pub fn get_writable_roots_with_cwd(&self, cwd: &Path) -> Vec<WritableRoot> {        match self {            SandboxPolicy::DangerFullAccess => Vec::new(),            SandboxPolicy::ExternalSandbox { .. } => Vec::new(),            SandboxPolicy::ReadOnly { .. } => Vec::new(),            SandboxPolicy::WorkspaceWrite {                writable_roots,                exclude_tmpdir_env_var,                exclude_slash_tmp,                network_access: _,            } => {                // Start from explicitly configured writable roots.                let mut roots: Vec<AbsolutePathBuf> = writable_roots.clone();                // Always include defaults: cwd, /tmp (if present on Unix), and                // on macOS, the per-user TMPDIR unless explicitly excluded.                // TODO(mbolin): cwd param should be AbsolutePathBuf.                let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd);                match cwd_absolute {                    Ok(cwd) => {                        roots.push(cwd);                    }                    Err(e) => {                        error!(                            "Ignoring invalid cwd {:?} for sandbox writable root: {}",                            cwd, e                        );                    }                }                // Include /tmp on Unix unless explicitly excluded.                if cfg!(unix) && !exclude_slash_tmp {                    match AbsolutePathBuf::from_absolute_path("/tmp") {                        Ok(slash_tmp) => {                            if slash_tmp.as_path().is_dir() {                                roots.push(slash_tmp);                            }                        }                        Err(e) => {                            error!("Ignoring invalid /tmp for sandbox writable root: {e}");                        }                    }                }                // Include $TMPDIR unless explicitly excluded. On macOS, TMPDIR                // is per-user, so writes to TMPDIR should not be readable by                // other users on the system.                //                // By comparison, TMPDIR is not guaranteed to be defined on                // Linux or Windows, but supporting it here gives users a way to                // provide the model with their own temporary directory without                // having to hardcode it in the config.                if !exclude_tmpdir_env_var                    && let Some(tmpdir) = std::env::var_os("TMPDIR")                    && !tmpdir.is_empty()                {                    match AbsolutePathBuf::from_absolute_path(PathBuf::from(&tmpdir)) {                        Ok(tmpdir_path) => {                            roots.push(tmpdir_path);                        }                        Err(e) => {                            error!(                                "Ignoring invalid TMPDIR value {tmpdir:?} for sandbox writable root: {e}",                            );                        }                    }                }                // For each root, compute subpaths that should remain read-only.                let cwd_root = AbsolutePathBuf::from_absolute_path(cwd).ok();                roots                    .into_iter()                    .map(|writable_root| {                        let protect_missing_dot_codex = cwd_root                            .as_ref()                            .is_some_and(|cwd_root| cwd_root == &writable_root);                        WritableRoot {                            read_only_subpaths: default_read_only_subpaths_for_writable_root(                                &writable_root,                                protect_missing_dot_codex,                            ),                            protected_metadata_names: Vec::new(),                            root: writable_root,                        }                    })                    .collect()            }        }    }}/// Event Queue Entry - events from agent#[derive(Debug, Clone, Deserialize, Serialize)]pub struct Event {    /// Submission `id` that this event is correlated with.    pub id: String,    /// Payload    pub msg: EventMsg,}/// Response event from the agent/// NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.#[derive(Debug, Clone, Deserialize, Serialize, Display, JsonSchema, TS)]#[serde(tag = "type", rename_all = "snake_case")]#[ts(tag = "type")]#[strum(serialize_all = "snake_case")]pub enum EventMsg {    /// Error while executing a submission    Error(ErrorEvent),    /// Warning issued while processing a submission. Unlike `Error`, this    /// indicates the turn continued but the user should still be notified.    Warning(WarningEvent),    /// Warning issued by the guardian automatic approval reviewer.    GuardianWarning(WarningEvent),    /// Realtime conversation lifecycle start event.    RealtimeConversationStarted(RealtimeConversationStartedEvent),    /// Realtime conversation streaming payload event.    RealtimeConversationRealtime(RealtimeConversationRealtimeEvent),    /// Realtime conversation lifecycle close event.    RealtimeConversationClosed(RealtimeConversationClosedEvent),    /// Realtime session description protocol payload.    RealtimeConversationSdp(RealtimeConversationSdpEvent),    /// Model routing changed from the requested model to a different model.    ModelReroute(ModelRerouteEvent),    /// Backend recommends additional account verification for this turn.    ModelVerification(ModelVerificationEvent),    /// Backend moderation metadata intended for first-party turn presentation.    TurnModerationMetadata(TurnModerationMetadataEvent),    /// Conversation history was compacted (either automatically or manually).    ContextCompacted(ContextCompactedEvent),    /// Conversation history was rolled back by dropping the last N user turns.    ThreadRolledBack(ThreadRolledBackEvent),    /// Agent has started a turn.    /// v1 wire format uses `task_started`; accept `turn_started` for v2 interop.    #[serde(rename = "task_started", alias = "turn_started")]    TurnStarted(TurnStartedEvent),    /// Persistent thread-settings overrides from the correlated submission have    /// been applied to the session configuration.    ThreadSettingsApplied(ThreadSettingsAppliedEvent),    /// Agent has completed all actions.    /// v1 wire format uses `task_complete`; accept `turn_complete` for v2 interop.    #[serde(rename = "task_complete", alias = "turn_complete")]    TurnComplete(TurnCompleteEvent),    /// Usage update for the current session, including totals and last turn.    /// Optional means unknown — UIs should not display when `None`.    TokenCount(TokenCountEvent),    /// Agent text output message    AgentMessage(AgentMessageEvent),    /// User/system input message (what was sent to the model)    UserMessage(UserMessageEvent),    /// Reasoning event from agent.    AgentReasoning(AgentReasoningEvent),    /// Raw chain-of-thought from agent.    AgentReasoningRawContent(AgentReasoningRawContentEvent),    /// Signaled when the model begins a new reasoning summary section (e.g., a new titled block).    AgentReasoningSectionBreak(AgentReasoningSectionBreakEvent),    /// Ack the client's configure message.    SessionConfigured(SessionConfiguredEvent),    /// Updated long-running goal metadata for the thread.    ThreadGoalUpdated(ThreadGoalUpdatedEvent),    /// Incremental MCP startup progress updates.    McpStartupUpdate(McpStartupUpdateEvent),    /// Aggregate MCP startup completion summary.    McpStartupComplete(McpStartupCompleteEvent),    McpToolCallBegin(McpToolCallBeginEvent),    McpToolCallEnd(McpToolCallEndEvent),    WebSearchBegin(WebSearchBeginEvent),    WebSearchEnd(WebSearchEndEvent),    ImageGenerationBegin(ImageGenerationBeginEvent),    ImageGenerationEnd(ImageGenerationEndEvent),    /// Notification that the server is about to execute a command.    ExecCommandBegin(ExecCommandBeginEvent),    /// Incremental chunk of output from a running command.    ExecCommandOutputDelta(ExecCommandOutputDeltaEvent),    /// Terminal interaction for an in-progress command (stdin sent and stdout observed).    TerminalInteraction(TerminalInteractionEvent),    ExecCommandEnd(ExecCommandEndEvent),    /// Notification that the agent attached a local image via the view_image tool.    ViewImageToolCall(ViewImageToolCallEvent),    ExecApprovalRequest(ExecApprovalRequestEvent),    RequestPermissions(RequestPermissionsEvent),    RequestUserInput(RequestUserInputEvent),    DynamicToolCallRequest(DynamicToolCallRequest),    DynamicToolCallResponse(DynamicToolCallResponseEvent),    ElicitationRequest(ElicitationRequestEvent),    ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent),    /// Structured lifecycle event for a guardian-reviewed approval request.    GuardianAssessment(GuardianAssessmentEvent),    /// Notification advising the user that something they are using has been    /// deprecated and should be phased out.    DeprecationNotice(DeprecationNoticeEvent),    /// Notification that a model stream experienced an error or disconnect    /// and the system is handling it (e.g., retrying with backoff).    StreamError(StreamErrorEvent),    /// Notification that the agent is about to apply a code patch. Mirrors    /// `ExecCommandBegin` so front‑ends can show progress indicators.    PatchApplyBegin(PatchApplyBeginEvent),    /// Latest model-generated structured changes for an `apply_patch` call.    PatchApplyUpdated(PatchApplyUpdatedEvent),    /// Notification that a patch application has finished.    PatchApplyEnd(PatchApplyEndEvent),    TurnDiff(TurnDiffEvent),    /// List of voices supported by realtime conversation streams.    RealtimeConversationListVoicesResponse(RealtimeConversationListVoicesResponseEvent),    PlanUpdate(UpdatePlanArgs),    TurnAborted(TurnAbortedEvent),    /// Notification that the agent is shutting down.    ShutdownComplete,    /// Entered review mode.    EnteredReviewMode(ReviewRequest),    /// Exited review mode with an optional final result to apply.    ExitedReviewMode(ExitedReviewModeEvent),    RawResponseItem(RawResponseItemEvent),    ItemStarted(ItemStartedEvent),    ItemCompleted(ItemCompletedEvent),    HookStarted(HookStartedEvent),    HookCompleted(HookCompletedEvent),    AgentMessageContentDelta(AgentMessageContentDeltaEvent),    PlanDelta(PlanDeltaEvent),    ReasoningContentDelta(ReasoningContentDeltaEvent),    ReasoningRawContentDelta(ReasoningRawContentDeltaEvent),    /// Collab interaction: agent spawn begin.    CollabAgentSpawnBegin(CollabAgentSpawnBeginEvent),    /// Collab interaction: agent spawn end.    CollabAgentSpawnEnd(CollabAgentSpawnEndEvent),    /// Collab interaction: agent interaction begin.    CollabAgentInteractionBegin(CollabAgentInteractionBeginEvent),    /// Collab interaction: agent interaction end.    CollabAgentInteractionEnd(CollabAgentInteractionEndEvent),    /// Collab interaction: waiting begin.    CollabWaitingBegin(CollabWaitingBeginEvent),    /// Collab interaction: waiting end.    CollabWaitingEnd(CollabWaitingEndEvent),    /// Collab interaction: close begin.    CollabCloseBegin(CollabCloseBeginEvent),    /// Collab interaction: close end.    CollabCloseEnd(CollabCloseEndEvent),    /// Collab interaction: resume begin.    CollabResumeBegin(CollabResumeBeginEvent),    /// Collab interaction: resume end.    CollabResumeEnd(CollabResumeEndEvent),    /// Path-based v2 sub-agent activity.    SubAgentActivity(SubAgentActivityEvent),}#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS, EnumIter)]#[serde(rename_all = "snake_case")]pub enum HookEventName {    PreToolUse,    PermissionRequest,    PostToolUse,    PreCompact,    PostCompact,    SessionStart,    UserPromptSubmit,    SubagentStart,    SubagentStop,    Stop,}#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub enum HookHandlerType {    Command,    Prompt,    Agent,}#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub enum HookExecutionMode {    Sync,    Async,}#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub enum HookScope {    Thread,    Turn,}#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub enum HookSource {    System,    User,    Project,    Mdm,    SessionFlags,    Plugin,    CloudRequirements,    CloudManagedConfig,    LegacyManagedConfigFile,    LegacyManagedConfigMdm,    #[default]    Unknown,}#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub enum HookTrustStatus {    Managed,    Untrusted,    Trusted,    Modified,}#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub enum HookRunStatus {    Running,    Completed,    Failed,    Blocked,    Stopped,}#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub enum HookOutputEntryKind {    Warning,    Stop,    Feedback,    Context,    Error,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub struct HookOutputEntry {    pub kind: HookOutputEntryKind,    pub text: String,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub struct HookRunSummary {    pub id: String,    pub event_name: HookEventName,    pub handler_type: HookHandlerType,    pub execution_mode: HookExecutionMode,    pub scope: HookScope,    pub source_path: AbsolutePathBuf,    #[serde(default)]    pub source: HookSource,    pub display_order: i64,    pub status: HookRunStatus,    pub status_message: Option<String>,    #[ts(type = "number")]    pub started_at: i64,    #[ts(type = "number | null")]    pub completed_at: Option<i64>,    #[ts(type = "number | null")]    pub duration_ms: Option<i64>,    pub entries: Vec<HookOutputEntry>,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub struct HookStartedEvent {    pub turn_id: Option<String>,    pub run: HookRunSummary,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub struct HookCompletedEvent {    pub turn_id: Option<String>,    pub run: HookRunSummary,}#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub enum RealtimeConversationVersion {    V1,    #[default]    V2,}#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub enum RealtimeConversationArchitecture {    #[default]    #[serde(rename = "realtimeapi")]    RealtimeApi,    Avas,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct RealtimeConversationStartedEvent {    pub realtime_session_id: Option<String>,    pub version: RealtimeConversationVersion,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct RealtimeConversationRealtimeEvent {    pub payload: RealtimeEvent,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct RealtimeConversationClosedEvent {    #[serde(skip_serializing_if = "Option::is_none")]    pub reason: Option<String>,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct RealtimeConversationSdpEvent {    pub sdp: String,}impl From<CollabAgentSpawnBeginEvent> for EventMsg {    fn from(event: CollabAgentSpawnBeginEvent) -> Self {        EventMsg::CollabAgentSpawnBegin(event)    }}impl From<CollabAgentSpawnEndEvent> for EventMsg {    fn from(event: CollabAgentSpawnEndEvent) -> Self {        EventMsg::CollabAgentSpawnEnd(event)    }}impl From<CollabAgentInteractionBeginEvent> for EventMsg {    fn from(event: CollabAgentInteractionBeginEvent) -> Self {        EventMsg::CollabAgentInteractionBegin(event)    }}impl From<CollabAgentInteractionEndEvent> for EventMsg {    fn from(event: CollabAgentInteractionEndEvent) -> Self {        EventMsg::CollabAgentInteractionEnd(event)    }}impl From<CollabWaitingBeginEvent> for EventMsg {    fn from(event: CollabWaitingBeginEvent) -> Self {        EventMsg::CollabWaitingBegin(event)    }}impl From<CollabWaitingEndEvent> for EventMsg {    fn from(event: CollabWaitingEndEvent) -> Self {        EventMsg::CollabWaitingEnd(event)    }}impl From<CollabCloseBeginEvent> for EventMsg {    fn from(event: CollabCloseBeginEvent) -> Self {        EventMsg::CollabCloseBegin(event)    }}impl From<CollabCloseEndEvent> for EventMsg {    fn from(event: CollabCloseEndEvent) -> Self {        EventMsg::CollabCloseEnd(event)    }}impl From<CollabResumeBeginEvent> for EventMsg {    fn from(event: CollabResumeBeginEvent) -> Self {        EventMsg::CollabResumeBegin(event)    }}impl From<CollabResumeEndEvent> for EventMsg {    fn from(event: CollabResumeEndEvent) -> Self {        EventMsg::CollabResumeEnd(event)    }}impl From<SubAgentActivityEvent> for EventMsg {    fn from(event: SubAgentActivityEvent) -> Self {        EventMsg::SubAgentActivity(event)    }}/// Agent lifecycle status, derived from emitted events.#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS, Default)]#[serde(rename_all = "snake_case")]#[ts(rename_all = "snake_case")]pub enum AgentStatus {    /// Agent is waiting for initialization.    #[default]    PendingInit,    /// Agent is currently running.    Running,    /// Agent's current turn was interrupted and it may receive more input.    Interrupted,    /// Agent is done. Contains the final assistant message.    Completed(Option<String>),    /// Agent encountered an error.    Errored(String),    /// Agent has been shutdown.    Shutdown,    /// Agent is not found.    NotFound,}/// Turn kinds that reject same-turn steering.#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]#[ts(rename_all = "snake_case")]pub enum NonSteerableTurnKind {    Review,    Compact,}/// Codex errors that we expose to clients.#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]#[ts(rename_all = "snake_case")]pub enum CodexErrorInfo {    ContextWindowExceeded,    UsageLimitExceeded,    ServerOverloaded,    CyberPolicy,    HttpConnectionFailed {        http_status_code: Option<u16>,    },    /// Failed to connect to the response SSE stream.    ResponseStreamConnectionFailed {        http_status_code: Option<u16>,    },    InternalServerError,    Unauthorized,    BadRequest,    SandboxError,    /// The response SSE stream disconnected in the middle of a turnbefore completion.    ResponseStreamDisconnected {        http_status_code: Option<u16>,    },    /// Reached the retry limit for responses.    ResponseTooManyFailedAttempts {        http_status_code: Option<u16>,    },    /// Returned when `turn/start` or `turn/steer` is submitted while the current active turn    /// cannot accept same-turn steering, for example `/review` or manual `/compact`.    ActiveTurnNotSteerable {        turn_kind: NonSteerableTurnKind,    },    ThreadRollbackFailed,    Other,}impl CodexErrorInfo {    /// Whether this error should mark the current turn as failed when replaying history.    pub fn affects_turn_status(&self) -> bool {        match self {            Self::ThreadRollbackFailed | Self::ActiveTurnNotSteerable { .. } => false,            Self::ContextWindowExceeded            | Self::UsageLimitExceeded            | Self::ServerOverloaded            | Self::CyberPolicy            | Self::HttpConnectionFailed { .. }            | Self::ResponseStreamConnectionFailed { .. }            | Self::InternalServerError            | Self::Unauthorized            | Self::BadRequest            | Self::SandboxError            | Self::ResponseStreamDisconnected { .. }            | Self::ResponseTooManyFailedAttempts { .. }            | Self::Other => true,        }    }}#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]pub struct RawResponseItemEvent {    pub item: ResponseItem,}#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]pub struct ItemStartedEvent {    pub thread_id: ThreadId,    pub turn_id: String,    pub item: TurnItem,    pub started_at_ms: i64,}impl HasLegacyEvent for ItemStartedEvent {    fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {        match &self.item {            TurnItem::WebSearch(item) => vec![EventMsg::WebSearchBegin(WebSearchBeginEvent {                call_id: item.id.clone(),            })],            TurnItem::ImageView(_) => Vec::new(),            TurnItem::ImageGeneration(item) => {                vec![EventMsg::ImageGenerationBegin(ImageGenerationBeginEvent {                    call_id: item.id.clone(),                })]            }            TurnItem::FileChange(item) => vec![item.as_legacy_begin_event(self.turn_id.clone())],            TurnItem::McpToolCall(item) => vec![item.as_legacy_begin_event()],            _ => Vec::new(),        }    }}#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]pub struct ItemCompletedEvent {    pub thread_id: ThreadId,    pub turn_id: String,    pub item: TurnItem,    // Old rollout files may contain ItemCompleted events for PlanItem without    // this field. Default to 0 so those persisted rollouts still deserialize    // after tightening the core event contract.    #[serde(default = "default_item_completed_at_ms")]    pub completed_at_ms: i64,}const fn default_item_completed_at_ms() -> i64 {    0}pub trait HasLegacyEvent {    fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg>;}impl HasLegacyEvent for ItemCompletedEvent {    fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg> {        match &self.item {            TurnItem::FileChange(item) => item                .as_legacy_end_event(self.turn_id.clone())                .into_iter()                .collect(),            _ => self.item.as_legacy_events(show_raw_agent_reasoning),        }    }}#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]pub struct AgentMessageContentDeltaEvent {    pub thread_id: String,    pub turn_id: String,    pub item_id: String,    pub delta: String,}impl HasLegacyEvent for AgentMessageContentDeltaEvent {    fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {        Vec::new()    }}#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]pub struct PlanDeltaEvent {    pub thread_id: String,    pub turn_id: String,    pub item_id: String,    pub delta: String,}#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]pub struct ReasoningContentDeltaEvent {    pub thread_id: String,    pub turn_id: String,    pub item_id: String,    pub delta: String,    // load with default value so it's backward compatible with the old format.    #[serde(default)]    pub summary_index: i64,}impl HasLegacyEvent for ReasoningContentDeltaEvent {    fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {        Vec::new()    }}#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]pub struct ReasoningRawContentDeltaEvent {    pub thread_id: String,    pub turn_id: String,    pub item_id: String,    pub delta: String,    // load with default value so it's backward compatible with the old format.    #[serde(default)]    pub content_index: i64,}impl HasLegacyEvent for ReasoningRawContentDeltaEvent {    fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {        Vec::new()    }}impl HasLegacyEvent for EventMsg {    fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg> {        match self {            EventMsg::ItemStarted(event) => event.as_legacy_events(show_raw_agent_reasoning),            EventMsg::ItemCompleted(event) => event.as_legacy_events(show_raw_agent_reasoning),            EventMsg::AgentMessageContentDelta(event) => {                event.as_legacy_events(show_raw_agent_reasoning)            }            EventMsg::ReasoningContentDelta(event) => {                event.as_legacy_events(show_raw_agent_reasoning)            }            EventMsg::ReasoningRawContentDelta(event) => {                event.as_legacy_events(show_raw_agent_reasoning)            }            _ => Vec::new(),        }    }}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct ExitedReviewModeEvent {    pub review_output: Option<ReviewOutputEvent>,}// Individual event payload types matching each `EventMsg` variant.#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct ErrorEvent {    pub message: String,    #[serde(default)]    pub codex_error_info: Option<CodexErrorInfo>,}impl ErrorEvent {    /// Whether this error should mark the current turn as failed when replaying history.    pub fn affects_turn_status(&self) -> bool {        self.codex_error_info            .as_ref()            .is_none_or(CodexErrorInfo::affects_turn_status)    }}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct WarningEvent {    pub message: String,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]#[ts(rename_all = "snake_case")]pub enum ModelRerouteReason {    HighRiskCyberActivity,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]pub struct ModelRerouteEvent {    pub from_model: String,    pub to_model: String,    pub reason: ModelRerouteReason,}#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]#[ts(rename_all = "snake_case")]pub enum ModelVerification {    TrustedAccessForCyber,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]pub struct ModelVerificationEvent {    pub verifications: Vec<ModelVerification>,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct TurnModerationMetadataEvent {    pub metadata: Value,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct ContextCompactedEvent;#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct TurnCompleteEvent {    pub turn_id: String,    pub last_agent_message: Option<String>,    /// Unix timestamp (in seconds) when the turn completed.    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(type = "number | null", optional)]    pub completed_at: Option<i64>,    /// Duration between turn start and completion in milliseconds, if known.    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(type = "number | null", optional)]    pub duration_ms: Option<i64>,    /// Duration between turn start and the first model token in milliseconds, if known.    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(type = "number | null", optional)]    pub time_to_first_token_ms: Option<i64>,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct TurnStartedEvent {    pub turn_id: String,    // Persist for rollout consumers that correlate turns with telemetry traces.    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub trace_id: Option<String>,    /// Unix timestamp (in seconds) when the turn started.    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(type = "number | null", optional)]    pub started_at: Option<i64>,    // TODO(aibrahim): make this not optional    pub model_context_window: Option<i64>,    #[serde(default)]    pub collaboration_mode_kind: ModeKind,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct ThreadSettingsAppliedEvent {    pub thread_settings: ThreadSettingsSnapshot,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct ThreadSettingsSnapshot {    pub model: String,    pub model_provider_id: String,    #[serde(skip_serializing_if = "Option::is_none")]    pub service_tier: Option<String>,    pub approval_policy: AskForApproval,    pub approvals_reviewer: ApprovalsReviewer,    pub permission_profile: PermissionProfile,    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub active_permission_profile: Option<ActivePermissionProfile>,    pub cwd: AbsolutePathBuf,    #[serde(skip_serializing_if = "Option::is_none")]    pub reasoning_effort: Option<ReasoningEffortConfig>,    #[serde(skip_serializing_if = "Option::is_none")]    pub reasoning_summary: Option<ReasoningSummaryConfig>,    #[serde(skip_serializing_if = "Option::is_none")]    pub personality: Option<Personality>,    pub collaboration_mode: CollaborationMode,}#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, TS)]pub struct TokenUsage {    #[ts(type = "number")]    pub input_tokens: i64,    #[ts(type = "number")]    pub cached_input_tokens: i64,    #[ts(type = "number")]    pub output_tokens: i64,    #[ts(type = "number")]    pub reasoning_output_tokens: i64,    #[ts(type = "number")]    pub total_tokens: i64,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]pub struct TokenUsageInfo {    pub total_token_usage: TokenUsage,    pub last_token_usage: TokenUsage,    // TODO(aibrahim): make this not optional    #[ts(type = "number | null")]    pub model_context_window: Option<i64>,}impl TokenUsageInfo {    pub fn new_or_append(        info: &Option<TokenUsageInfo>,        last: &Option<TokenUsage>,        model_context_window: Option<i64>,    ) -> Option<Self> {        if info.is_none() && last.is_none() {            return None;        }        let mut info = match info {            Some(info) => info.clone(),            None => Self {                total_token_usage: TokenUsage::default(),                last_token_usage: TokenUsage::default(),                model_context_window,            },        };        if let Some(last) = last {            info.append_last_usage(last);        }        if let Some(model_context_window) = model_context_window {            info.model_context_window = Some(model_context_window);        }        Some(info)    }    pub fn append_last_usage(&mut self, last: &TokenUsage) {        self.total_token_usage.add_assign(last);        self.last_token_usage = last.clone();    }    pub fn fill_to_context_window(&mut self, context_window: i64) {        let previous_total = self.total_token_usage.total_tokens;        let delta = (context_window - previous_total).max(0);        self.model_context_window = Some(context_window);        self.total_token_usage = TokenUsage {            total_tokens: context_window,            ..TokenUsage::default()        };        self.last_token_usage = TokenUsage {            total_tokens: delta,            ..TokenUsage::default()        };    }    pub fn full_context_window(context_window: i64) -> Self {        let mut info = Self {            total_token_usage: TokenUsage::default(),            last_token_usage: TokenUsage::default(),            model_context_window: Some(context_window),        };        info.fill_to_context_window(context_window);        info    }}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct TokenCountEvent {    pub info: Option<TokenUsageInfo>,    pub rate_limits: Option<RateLimitSnapshot>,}#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]pub struct RateLimitSnapshot {    pub limit_id: Option<String>,    pub limit_name: Option<String>,    pub primary: Option<RateLimitWindow>,    pub secondary: Option<RateLimitWindow>,    pub credits: Option<CreditsSnapshot>,    pub individual_limit: Option<SpendControlLimitSnapshot>,    pub plan_type: Option<crate::account::PlanType>,    pub rate_limit_reached_type: Option<RateLimitReachedType>,}#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema, TS)]#[serde(rename_all = "snake_case")]#[ts(rename_all = "snake_case")]pub enum RateLimitReachedType {    RateLimitReached,    WorkspaceOwnerCreditsDepleted,    WorkspaceMemberCreditsDepleted,    WorkspaceOwnerUsageLimitReached,    WorkspaceMemberUsageLimitReached,}impl FromStr for RateLimitReachedType {    type Err = String;    fn from_str(value: &str) -> Result<Self, Self::Err> {        match value {            "rate_limit_reached" => Ok(Self::RateLimitReached),            "workspace_owner_credits_depleted" => Ok(Self::WorkspaceOwnerCreditsDepleted),            "workspace_member_credits_depleted" => Ok(Self::WorkspaceMemberCreditsDepleted),            "workspace_owner_usage_limit_reached" => Ok(Self::WorkspaceOwnerUsageLimitReached),            "workspace_member_usage_limit_reached" => Ok(Self::WorkspaceMemberUsageLimitReached),            other => Err(format!("unknown rate limit reached type: {other}")),        }    }}#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]pub struct RateLimitWindow {    /// Percentage (0-100) of the window that has been consumed.    pub used_percent: f64,    /// Rolling window duration, in minutes.    #[ts(type = "number | null")]    pub window_minutes: Option<i64>,    /// Unix timestamp (seconds since epoch) when the window resets.    #[ts(type = "number | null")]    pub resets_at: Option<i64>,}#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]pub struct CreditsSnapshot {    pub has_credits: bool,    pub unlimited: bool,    pub balance: Option<String>,}#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]pub struct SpendControlLimitSnapshot {    pub limit: String,    pub used: String,    pub remaining_percent: i32,    pub resets_at: i64,}// Includes prompts, tools and space to call compact.const BASELINE_TOKENS: i64 = 12000;impl TokenUsage {    pub fn is_zero(&self) -> bool {        self.total_tokens == 0    }    pub fn cached_input(&self) -> i64 {        self.cached_input_tokens.max(0)    }    pub fn non_cached_input(&self) -> i64 {        (self.input_tokens - self.cached_input()).max(0)    }    /// Primary count for display as a single absolute value: non-cached input + output.    pub fn blended_total(&self) -> i64 {        (self.non_cached_input() + self.output_tokens.max(0)).max(0)    }    pub fn tokens_in_context_window(&self) -> i64 {        self.total_tokens    }    /// Estimate the remaining user-controllable percentage of the model's context window.    ///    /// `context_window` is the total size of the model's context window.    /// `BASELINE_TOKENS` should capture tokens that are always present in    /// the context (e.g., system prompt and fixed tool instructions) so that    /// the percentage reflects the portion the user can influence.    ///    /// This normalizes both the numerator and denominator by subtracting the    /// baseline, so immediately after the first prompt the UI shows 100% left    /// and trends toward 0% as the user fills the effective window.    pub fn percent_of_context_window_remaining(&self, context_window: i64) -> i64 {        if context_window <= BASELINE_TOKENS {            return 0;        }        let effective_window = context_window - BASELINE_TOKENS;        let used = (self.tokens_in_context_window() - BASELINE_TOKENS).max(0);        let remaining = (effective_window - used).max(0);        ((remaining as f64 / effective_window as f64) * 100.0)            .clamp(0.0, 100.0)            .round() as i64    }    /// In-place element-wise sum of token counts.    pub fn add_assign(&mut self, other: &TokenUsage) {        self.input_tokens += other.input_tokens;        self.cached_input_tokens += other.cached_input_tokens;        self.output_tokens += other.output_tokens;        self.reasoning_output_tokens += other.reasoning_output_tokens;        self.total_tokens += other.total_tokens;    }}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]pub struct FinalOutput {    pub token_usage: TokenUsage,}impl From<TokenUsage> for FinalOutput {    fn from(token_usage: TokenUsage) -> Self {        Self { token_usage }    }}impl fmt::Display for FinalOutput {    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {        let token_usage = &self.token_usage;        write!(            f,            "Token usage: total={} input={}{} output={}{}",            format_with_separators(token_usage.blended_total()),            format_with_separators(token_usage.non_cached_input()),            if token_usage.cached_input() > 0 {                format!(                    " (+ {} cached)",                    format_with_separators(token_usage.cached_input())                )            } else {                String::new()            },            format_with_separators(token_usage.output_tokens),            if token_usage.reasoning_output_tokens > 0 {                format!(                    " (reasoning {})",                    format_with_separators(token_usage.reasoning_output_tokens)                )            } else {                String::new()            }        )    }}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct AgentMessageEvent {    pub message: String,    #[serde(default)]    pub phase: Option<MessagePhase>,    #[serde(default)]    pub memory_citation: Option<MemoryCitation>,}#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema, TS)]pub struct UserMessageEvent {    #[serde(default, skip_serializing_if = "Option::is_none")]    pub client_id: Option<String>,    pub message: String,    /// Image URLs sourced from `UserInput::Image`. These are safe    /// to replay in legacy UI history events and correspond to images sent to    /// the model.    #[serde(skip_serializing_if = "Option::is_none")]    pub images: Option<Vec<String>>,    /// Detail hints for `images`, indexed in parallel. Missing entries imply    /// default image detail behavior.    #[serde(default, skip_serializing_if = "Vec::is_empty")]    pub image_details: Vec<Option<ImageDetail>>,    /// Local file paths sourced from `UserInput::LocalImage`. These are kept so    /// the UI can reattach images when editing history. Local image prompts may    /// include a display form of the path, but these should not be treated as    /// API-ready URLs.    #[serde(default)]    pub local_images: Vec<std::path::PathBuf>,    /// Detail hints for `local_images`, indexed in parallel. Missing entries    /// imply default image detail behavior.    #[serde(default, skip_serializing_if = "Vec::is_empty")]    pub local_image_details: Vec<Option<ImageDetail>>,    /// UI-defined spans within `message` used to render or persist special elements.    #[serde(default)]    pub text_elements: Vec<crate::user_input::TextElement>,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct AgentReasoningEvent {    pub text: String,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct AgentReasoningRawContentEvent {    pub text: String,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct AgentReasoningSectionBreakEvent {    // load with default value so it's backward compatible with the old format.    #[serde(default)]    pub item_id: String,    #[serde(default)]    pub summary_index: i64,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]pub struct McpInvocation {    /// Name of the MCP server as defined in the config.    pub server: String,    /// Name of the tool as given by the MCP server.    pub tool: String,    /// Arguments to the tool call.    pub arguments: Option<serde_json::Value>,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]pub struct McpToolCallBeginEvent {    /// Identifier so this can be paired with the McpToolCallEnd event.    pub call_id: String,    pub invocation: McpInvocation,    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub mcp_app_resource_uri: Option<String>,    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub plugin_id: Option<String>,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]pub struct McpToolCallEndEvent {    /// Identifier for the corresponding McpToolCallBegin that finished.    pub call_id: String,    pub invocation: McpInvocation,    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub mcp_app_resource_uri: Option<String>,    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub plugin_id: Option<String>,    #[ts(type = "string")]    pub duration: Duration,    /// Result of the tool call. Note this could be an error.    pub result: Result<CallToolResult, String>,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]pub struct DynamicToolCallResponseEvent {    /// Identifier for the corresponding DynamicToolCallRequest.    pub call_id: String,    /// Turn ID that this dynamic tool call belongs to.    pub turn_id: String,    #[serde(default)]    pub completed_at_ms: i64,    /// Dynamic tool namespace, when one was provided.    #[serde(default)]    pub namespace: Option<String>,    /// Dynamic tool name.    pub tool: String,    /// Dynamic tool call arguments.    pub arguments: serde_json::Value,    /// Dynamic tool response content items.    pub content_items: Vec<DynamicToolCallOutputContentItem>,    /// Whether the tool call succeeded.    pub success: bool,    /// Optional error text when the tool call failed before producing a response.    pub error: Option<String>,    /// The duration of the dynamic tool call.    #[ts(type = "string")]    pub duration: Duration,}impl McpToolCallEndEvent {    pub fn is_success(&self) -> bool {        match &self.result {            Ok(result) => !result.is_error.unwrap_or(false),            Err(_) => false,        }    }}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct WebSearchBeginEvent {    pub call_id: String,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct WebSearchEndEvent {    pub call_id: String,    pub query: String,    pub action: WebSearchAction,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct ImageGenerationBeginEvent {    pub call_id: String,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct ImageGenerationEndEvent {    pub call_id: String,    pub status: String,    #[serde(skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub revised_prompt: Option<String>,    pub result: String,    #[serde(skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub saved_path: Option<AbsolutePathBuf>,}// Conversation kept for backward compatibility./// Response payload for `Op::GetHistory` containing the current session's/// in-memory transcript.#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct ConversationPathResponseEvent {    pub conversation_id: ThreadId,    pub path: PathBuf,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct ResumedHistory {    pub conversation_id: ThreadId,    pub history: Vec<RolloutItem>,    pub rollout_path: Option<PathBuf>,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub enum InitialHistory {    New,    Cleared,    Resumed(ResumedHistory),    Forked(Vec<RolloutItem>),}impl InitialHistory {    pub fn scan_rollout_items(&self, mut predicate: impl FnMut(&RolloutItem) -> bool) -> bool {        match self {            InitialHistory::New | InitialHistory::Cleared => false,            InitialHistory::Resumed(resumed) => resumed.history.iter().any(&mut predicate),            InitialHistory::Forked(items) => items.iter().any(predicate),        }    }    pub fn forked_from_id(&self) -> Option<ThreadId> {        match self {            InitialHistory::New | InitialHistory::Cleared => None,            InitialHistory::Resumed(resumed) => {                resumed.history.iter().find_map(|item| match item {                    RolloutItem::SessionMeta(meta_line) => meta_line.meta.forked_from_id,                    _ => None,                })            }            InitialHistory::Forked(items) => items.iter().find_map(|item| match item {                RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.id),                _ => None,            }),        }    }    pub fn session_cwd(&self) -> Option<PathBuf> {        match self {            InitialHistory::New | InitialHistory::Cleared => None,            InitialHistory::Resumed(resumed) => session_cwd_from_items(&resumed.history),            InitialHistory::Forked(items) => session_cwd_from_items(items),        }    }    pub fn get_rollout_items(&self) -> Vec<RolloutItem> {        match self {            InitialHistory::New | InitialHistory::Cleared => Vec::new(),            InitialHistory::Resumed(resumed) => resumed.history.clone(),            InitialHistory::Forked(items) => items.clone(),        }    }    pub fn get_event_msgs(&self) -> Option<Vec<EventMsg>> {        match self {            InitialHistory::New | InitialHistory::Cleared => None,            InitialHistory::Resumed(resumed) => Some(                resumed                    .history                    .iter()                    .filter_map(|ri| match ri {                        RolloutItem::EventMsg(ev) => Some(ev.clone()),                        _ => None,                    })                    .collect(),            ),            InitialHistory::Forked(items) => Some(                items                    .iter()                    .filter_map(|ri| match ri {                        RolloutItem::EventMsg(ev) => Some(ev.clone()),                        _ => None,                    })                    .collect(),            ),        }    }    pub fn get_base_instructions(&self) -> Option<BaseInstructions> {        // TODO: SessionMeta should (in theory) always be first in the history, so we can probably only check the first item?        match self {            InitialHistory::New | InitialHistory::Cleared => None,            InitialHistory::Resumed(resumed) => {                resumed.history.iter().find_map(|item| match item {                    RolloutItem::SessionMeta(meta_line) => meta_line.meta.base_instructions.clone(),                    _ => None,                })            }            InitialHistory::Forked(items) => items.iter().find_map(|item| match item {                RolloutItem::SessionMeta(meta_line) => meta_line.meta.base_instructions.clone(),                _ => None,            }),        }    }    pub fn get_dynamic_tools(&self) -> Option<Vec<DynamicToolSpec>> {        match self {            InitialHistory::New | InitialHistory::Cleared => None,            InitialHistory::Resumed(resumed) => {                resumed.history.iter().find_map(|item| match item {                    RolloutItem::SessionMeta(meta_line) => meta_line.meta.dynamic_tools.clone(),                    _ => None,                })            }            InitialHistory::Forked(items) => items.iter().find_map(|item| match item {                RolloutItem::SessionMeta(meta_line) => meta_line.meta.dynamic_tools.clone(),                _ => None,            }),        }    }    pub fn get_multi_agent_version(&self) -> Option<MultiAgentVersion> {        match self {            InitialHistory::New | InitialHistory::Cleared => None,            InitialHistory::Resumed(resumed) => {                multi_agent_version_from_items(&resumed.history, Some(resumed.conversation_id))            }            InitialHistory::Forked(items) => {                multi_agent_version_from_items(items, /*thread_id*/ None)            }        }    }    pub fn get_resumed_session_sources(&self) -> Option<(SessionSource, Option<ThreadSource>)> {        let meta = self.get_resumed_session_meta()?;        Some((meta.source.clone(), meta.thread_source.clone()))    }    pub fn get_resumed_thread_source(&self) -> Option<ThreadSource> {        self.get_resumed_session_meta()            .and_then(|meta| meta.thread_source.clone())    }    pub fn get_resumed_parent_thread_id(&self) -> Option<ThreadId> {        self.get_resumed_session_meta()            .and_then(|meta| meta.parent_thread_id)    }    fn get_resumed_session_meta(&self) -> Option<&SessionMeta> {        match self {            InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => None,            InitialHistory::Resumed(resumed) => {                resumed.history.iter().find_map(|item| match item {                    RolloutItem::SessionMeta(meta_line) => Some(&meta_line.meta),                    _ => None,                })            }        }    }}fn session_cwd_from_items(items: &[RolloutItem]) -> Option<PathBuf> {    items.iter().find_map(|item| match item {        RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.cwd.clone()),        _ => None,    })}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]#[serde(rename_all = "lowercase")]#[ts(rename_all = "lowercase")]pub enum SessionSource {    Cli,    #[default]    VSCode,    Exec,    Mcp,    Custom(String),    Internal(InternalSessionSource),    SubAgent(SubAgentSource),    #[serde(other)]    Unknown,}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]#[serde(try_from = "String", into = "String")]#[schemars(with = "String")]#[ts(type = "string")]pub enum ThreadSource {    User,    Subagent,    Feature(String),    MemoryConsolidation,}impl ThreadSource {    pub fn as_str(&self) -> &str {        match self {            ThreadSource::User => "user",            ThreadSource::Subagent => "subagent",            ThreadSource::Feature(feature) => feature,            ThreadSource::MemoryConsolidation => "memory_consolidation",        }    }}impl fmt::Display for ThreadSource {    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {        f.write_str(self.as_str())    }}impl TryFrom<String> for ThreadSource {    type Error = String;    fn try_from(value: String) -> Result<Self, Self::Error> {        value.parse()    }}impl From<ThreadSource> for String {    fn from(value: ThreadSource) -> Self {        value.to_string()    }}impl FromStr for ThreadSource {    type Err = String;    fn from_str(value: &str) -> Result<Self, Self::Err> {        match value {            "user" => Ok(ThreadSource::User),            "subagent" => Ok(ThreadSource::Subagent),            "memory_consolidation" => Ok(ThreadSource::MemoryConsolidation),            other => Ok(ThreadSource::Feature(other.to_string())),        }    }}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]#[ts(rename_all = "snake_case")]pub enum InternalSessionSource {    MemoryConsolidation,}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]#[ts(rename_all = "snake_case")]pub enum SubAgentSource {    Review,    Compact,    ThreadSpawn {        parent_thread_id: ThreadId,        depth: i32,        #[serde(default)]        agent_path: Option<AgentPath>,        #[serde(default)]        agent_nickname: Option<String>,        #[serde(default, alias = "agent_type")]        agent_role: Option<String>,    },    MemoryConsolidation,    Other(String),}impl fmt::Display for SessionSource {    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {        match self {            SessionSource::Cli => f.write_str("cli"),            SessionSource::VSCode => f.write_str("vscode"),            SessionSource::Exec => f.write_str("exec"),            SessionSource::Mcp => f.write_str("mcp"),            SessionSource::Custom(source) => f.write_str(source),            SessionSource::Internal(source) => write!(f, "internal_{source}"),            SessionSource::SubAgent(sub_source) => write!(f, "subagent_{sub_source}"),            SessionSource::Unknown => f.write_str("unknown"),        }    }}impl SessionSource {    pub fn from_startup_arg(value: &str) -> Result<Self, &'static str> {        let trimmed = value.trim();        if trimmed.is_empty() {            return Err("session source must not be empty");        }        let normalized = trimmed.to_ascii_lowercase();        Ok(match normalized.as_str() {            "cli" => SessionSource::Cli,            "vscode" => SessionSource::VSCode,            "exec" => SessionSource::Exec,            "mcp" | "appserver" | "app-server" | "app_server" => SessionSource::Mcp,            "unknown" => SessionSource::Unknown,            _ => SessionSource::Custom(normalized),        })    }    pub fn is_internal(&self) -> bool {        matches!(self, SessionSource::Internal(_))    }    pub fn is_non_root_agent(&self) -> bool {        matches!(            self,            SessionSource::Internal(_) | SessionSource::SubAgent(_)        )    }    pub fn get_nickname(&self) -> Option<String> {        match self {            SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_nickname, .. }) => {                agent_nickname.clone()            }            _ => None,        }    }    pub fn get_agent_role(&self) -> Option<String> {        match self {            SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_role, .. }) => {                agent_role.clone()            }            _ => None,        }    }    pub fn get_agent_path(&self) -> Option<AgentPath> {        match self {            SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_path, .. }) => {                agent_path.clone()            }            _ => None,        }    }    pub fn restriction_product(&self) -> Option<Product> {        match self {            SessionSource::Custom(source) => Product::from_session_source_name(source),            SessionSource::Cli            | SessionSource::VSCode            | SessionSource::Exec            | SessionSource::Mcp            | SessionSource::Unknown => Some(Product::Codex),            SessionSource::Internal(_) | SessionSource::SubAgent(_) => None,        }    }    pub fn matches_product_restriction(&self, products: &[Product]) -> bool {        products.is_empty()            || self                .restriction_product()                .is_some_and(|product| product.matches_product_restriction(products))    }    pub fn parent_thread_id(&self) -> Option<ThreadId> {        match self {            SessionSource::SubAgent(subagent_source) => subagent_source.parent_thread_id(),            SessionSource::Cli            | SessionSource::VSCode            | SessionSource::Exec            | SessionSource::Mcp            | SessionSource::Custom(_)            | SessionSource::Internal(_)            | SessionSource::Unknown => None,        }    }}impl fmt::Display for SubAgentSource {    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {        match self {            SubAgentSource::Review => f.write_str("review"),            SubAgentSource::Compact => f.write_str("compact"),            SubAgentSource::MemoryConsolidation => f.write_str("memory_consolidation"),            SubAgentSource::ThreadSpawn {                parent_thread_id,                depth,                ..            } => {                write!(f, "thread_spawn_{parent_thread_id}_d{depth}")            }            SubAgentSource::Other(other) => f.write_str(other),        }    }}impl SubAgentSource {    pub fn kind(&self) -> &str {        match self {            SubAgentSource::Review => "review",            SubAgentSource::Compact => "compact",            SubAgentSource::ThreadSpawn { .. } => "thread_spawn",            SubAgentSource::MemoryConsolidation => "memory_consolidation",            SubAgentSource::Other(other) => other,        }    }    pub fn parent_thread_id(&self) -> Option<ThreadId> {        match self {            SubAgentSource::ThreadSpawn {                parent_thread_id, ..            } => Some(*parent_thread_id),            SubAgentSource::Review            | SubAgentSource::Compact            | SubAgentSource::MemoryConsolidation            | SubAgentSource::Other(_) => None,        }    }}impl fmt::Display for InternalSessionSource {    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {        match self {            InternalSessionSource::MemoryConsolidation => f.write_str("memory_consolidation"),        }    }}fn multi_agent_version_from_items(    items: &[RolloutItem],    thread_id: Option<ThreadId>,) -> Option<MultiAgentVersion> {    let session_meta_version = items.iter().rev().find_map(|item| match item {        RolloutItem::SessionMeta(meta_line)            if thread_id.is_none_or(|thread_id| meta_line.meta.id == thread_id) =>        {            meta_line.meta.multi_agent_version        }        _ => None,    });    session_meta_version.or_else(|| {        items.iter().rev().find_map(|item| match item {            RolloutItem::TurnContext(turn_context) => turn_context.multi_agent_version,            RolloutItem::SessionMeta(_)            | RolloutItem::ResponseItem(_)            | RolloutItem::InterAgentCommunication(_)            | RolloutItem::Compacted(_)            | RolloutItem::EventMsg(_) => None,        })    })}#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]#[ts(rename_all = "snake_case")]pub enum MultiAgentVersion {    Disabled,    V1,    V2,}/// SessionMeta contains session-level data that doesn't correspond to a specific turn.////// NOTE: There used to be an `instructions` field here, which stored user_instructions, but we/// now save that on TurnContext. base_instructions stores the base instructions for the session,/// and should be used when there is no config override.#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]pub struct SessionMeta {    pub id: ThreadId,    #[serde(skip_serializing_if = "Option::is_none")]    pub forked_from_id: Option<ThreadId>,    #[serde(skip_serializing_if = "Option::is_none")]    pub parent_thread_id: Option<ThreadId>,    pub timestamp: String,    pub cwd: PathBuf,    pub originator: String,    pub cli_version: String,    #[serde(default)]    pub source: SessionSource,    /// Optional analytics source classification for this thread.    #[serde(default, skip_serializing_if = "Option::is_none")]    pub thread_source: Option<ThreadSource>,    /// Optional random unique nickname assigned to an AgentControl-spawned sub-agent.    #[serde(skip_serializing_if = "Option::is_none")]    pub agent_nickname: Option<String>,    /// Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.    #[serde(default, alias = "agent_type", skip_serializing_if = "Option::is_none")]    pub agent_role: Option<String>,    /// Optional canonical agent path assigned to an AgentControl-spawned sub-agent.    #[serde(skip_serializing_if = "Option::is_none")]    pub agent_path: Option<String>,    pub model_provider: Option<String>,    /// base_instructions for the session. This *should* always be present when creating a new session,    /// but may be missing for older sessions. If not present, fall back to rendering the base_instructions    /// from ModelsManager.    pub base_instructions: Option<BaseInstructions>,    #[serde(        default,        deserialize_with = "crate::dynamic_tools::deserialize_dynamic_tool_specs",        skip_serializing_if = "Option::is_none"    )]    pub dynamic_tools: Option<Vec<DynamicToolSpec>>,    #[serde(skip_serializing_if = "Option::is_none")]    pub memory_mode: Option<String>,    #[serde(skip_serializing_if = "Option::is_none")]    pub multi_agent_version: Option<MultiAgentVersion>,}impl Default for SessionMeta {    fn default() -> Self {        SessionMeta {            id: ThreadId::default(),            forked_from_id: None,            parent_thread_id: None,            timestamp: String::new(),            cwd: PathBuf::new(),            originator: String::new(),            cli_version: String::new(),            source: SessionSource::default(),            thread_source: None,            agent_nickname: None,            agent_role: None,            agent_path: None,            model_provider: None,            base_instructions: None,            dynamic_tools: None,            memory_mode: None,            multi_agent_version: None,        }    }}#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]pub struct SessionMetaLine {    #[serde(flatten)]    pub meta: SessionMeta,    #[serde(skip_serializing_if = "Option::is_none")]    pub git: Option<GitInfo>,}#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]#[serde(tag = "type", content = "payload", rename_all = "snake_case")]pub enum RolloutItem {    SessionMeta(SessionMetaLine),    ResponseItem(ResponseItem),    /// Durable delivery metadata reconstructed as a model-visible `agent_message`.    InterAgentCommunication(InterAgentCommunication),    Compacted(CompactedItem),    TurnContext(TurnContextItem),    EventMsg(EventMsg),}#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]pub struct CompactedItem {    pub message: String,    #[serde(default, skip_serializing_if = "Option::is_none")]    pub replacement_history: Option<Vec<ResponseItem>>,    #[serde(default, skip_serializing_if = "Option::is_none")]    pub window_id: Option<u64>,}impl From<CompactedItem> for ResponseItem {    fn from(value: CompactedItem) -> Self {        ResponseItem::Message {            id: None,            role: "assistant".to_string(),            content: vec![ContentItem::OutputText {                text: value.message,            }],            phase: None,            metadata: None,        }    }}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]pub struct TurnContextNetworkItem {    pub allowed_domains: Vec<String>,    pub denied_domains: Vec<String>,}/// Persist once per real user turn after computing that turn's model-visible/// context updates, and again after mid-turn compaction when replacement/// history re-establishes full context, so resume/fork replay can recover the/// latest durable baseline.#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]pub struct TurnContextItem {    #[serde(default, skip_serializing_if = "Option::is_none")]    pub turn_id: Option<String>,    pub cwd: PathBuf,    /// Effective workspace roots used to materialize symbolic    /// `:workspace_roots` filesystem permissions in `permission_profile`.    #[serde(default, skip_serializing_if = "Option::is_none")]    pub workspace_roots: Option<Vec<AbsolutePathBuf>>,    #[serde(default, skip_serializing_if = "Option::is_none")]    pub current_date: Option<String>,    #[serde(default, skip_serializing_if = "Option::is_none")]    pub timezone: Option<String>,    pub approval_policy: AskForApproval,    pub sandbox_policy: SandboxPolicy,    #[serde(default, skip_serializing_if = "Option::is_none")]    pub permission_profile: Option<PermissionProfile>,    #[serde(skip_serializing_if = "Option::is_none")]    pub network: Option<TurnContextNetworkItem>,    #[serde(default, skip_serializing_if = "Option::is_none")]    pub file_system_sandbox_policy: Option<FileSystemSandboxPolicy>,    pub model: String,    #[serde(default, skip_serializing_if = "Option::is_none")]    pub comp_hash: Option<String>,    #[serde(skip_serializing_if = "Option::is_none")]    pub personality: Option<Personality>,    #[serde(default, skip_serializing_if = "Option::is_none")]    pub collaboration_mode: Option<CollaborationMode>,    #[serde(default, skip_serializing_if = "Option::is_none")]    pub multi_agent_version: Option<MultiAgentVersion>,    #[serde(default, skip_serializing_if = "Option::is_none")]    pub realtime_active: Option<bool>,    #[serde(skip_serializing_if = "Option::is_none")]    pub effort: Option<ReasoningEffortConfig>,    // Compatibility-only field written with a default value so older Codex    // versions can deserialize turn-context rollout items. It is no longer    // read by context reconstruction and should be removed in a future schema    // cleanup.    pub summary: ReasoningSummaryConfig,}impl TurnContextItem {    pub fn permission_profile(&self) -> PermissionProfile {        self.permission_profile.clone().unwrap_or_else(|| {            let file_system_sandbox_policy =                self.file_system_sandbox_policy.clone().unwrap_or_else(|| {                    FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(                        &self.sandbox_policy,                        &self.cwd,                    )                });            PermissionProfile::from_runtime_permissions_with_enforcement(                SandboxEnforcement::from_legacy_sandbox_policy(&self.sandbox_policy),                &file_system_sandbox_policy,                NetworkSandboxPolicy::from(&self.sandbox_policy),            )        })    }}#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(tag = "mode", content = "limit", rename_all = "snake_case")]pub enum TruncationPolicy {    Bytes(usize),    Tokens(usize),}impl From<crate::openai_models::TruncationPolicyConfig> for TruncationPolicy {    fn from(config: crate::openai_models::TruncationPolicyConfig) -> Self {        match config.mode {            crate::openai_models::TruncationMode::Bytes => Self::Bytes(config.limit as usize),            crate::openai_models::TruncationMode::Tokens => Self::Tokens(config.limit as usize),        }    }}impl TruncationPolicy {    pub fn token_budget(&self) -> usize {        match self {            TruncationPolicy::Bytes(bytes) => {                usize::try_from(codex_utils_string::approx_tokens_from_byte_count(*bytes))                    .unwrap_or(usize::MAX)            }            TruncationPolicy::Tokens(tokens) => *tokens,        }    }    pub fn byte_budget(&self) -> usize {        match self {            TruncationPolicy::Bytes(bytes) => *bytes,            TruncationPolicy::Tokens(tokens) => {                codex_utils_string::approx_bytes_for_tokens(*tokens)            }        }    }}impl Mul<f64> for TruncationPolicy {    type Output = Self;    fn mul(self, multiplier: f64) -> Self::Output {        match self {            TruncationPolicy::Bytes(bytes) => {                TruncationPolicy::Bytes((bytes as f64 * multiplier).ceil() as usize)            }            TruncationPolicy::Tokens(tokens) => {                TruncationPolicy::Tokens((tokens as f64 * multiplier).ceil() as usize)            }        }    }}#[derive(Serialize, Deserialize, Clone, JsonSchema)]pub struct RolloutLine {    pub timestamp: String,    #[serde(flatten)]    pub item: RolloutItem,}#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]pub struct GitInfo {    /// Current commit hash (SHA)    #[serde(skip_serializing_if = "Option::is_none")]    pub commit_hash: Option<GitSha>,    /// Current branch name    #[serde(skip_serializing_if = "Option::is_none")]    pub branch: Option<String>,    /// Repository URL (if available from remote)    #[serde(skip_serializing_if = "Option::is_none")]    pub repository_url: Option<String>,}#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub enum ReviewDelivery {    Inline,    Detached,}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)]#[serde(tag = "type", rename_all = "camelCase")]#[ts(tag = "type")]pub enum ReviewTarget {    /// Review the working tree: staged, unstaged, and untracked files.    UncommittedChanges,    /// Review changes between the current branch and the given base branch.    #[serde(rename_all = "camelCase")]    #[ts(rename_all = "camelCase")]    BaseBranch { branch: String },    /// Review the changes introduced by a specific commit.    #[serde(rename_all = "camelCase")]    #[ts(rename_all = "camelCase")]    Commit {        sha: String,        /// Optional human-readable label (e.g., commit subject) for UIs.        title: Option<String>,    },    /// Arbitrary instructions provided by the user.    #[serde(rename_all = "camelCase")]    #[ts(rename_all = "camelCase")]    Custom { instructions: String },}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]/// Review request sent to the review session.pub struct ReviewRequest {    pub target: ReviewTarget,    #[serde(skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub user_facing_hint: Option<String>,}/// Structured review result produced by a child review session.#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct ReviewOutputEvent {    pub findings: Vec<ReviewFinding>,    pub overall_correctness: String,    pub overall_explanation: String,    pub overall_confidence_score: f32,}impl Default for ReviewOutputEvent {    fn default() -> Self {        Self {            findings: Vec::new(),            overall_correctness: String::default(),            overall_explanation: String::default(),            overall_confidence_score: 0.0,        }    }}/// A single review finding describing an observed issue or recommendation.#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct ReviewFinding {    pub title: String,    pub body: String,    pub confidence_score: f32,    pub priority: i32,    pub code_location: ReviewCodeLocation,}/// Location of the code related to a review finding.#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct ReviewCodeLocation {    pub absolute_file_path: PathBuf,    pub line_range: ReviewLineRange,}/// Inclusive line range in a file associated with the finding.#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct ReviewLineRange {    pub start: u32,    pub end: u32,}#[derive(    Debug, Clone, Copy, Display, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS, Default,)]#[serde(rename_all = "snake_case")]pub enum ExecCommandSource {    #[default]    Agent,    UserShell,    UnifiedExecStartup,    UnifiedExecInteraction,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub enum ExecCommandStatus {    Completed,    Failed,    Declined,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct ExecCommandBeginEvent {    /// Identifier so this can be paired with the ExecCommandEnd event.    pub call_id: String,    /// Identifier for the underlying PTY process (when available).    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub process_id: Option<String>,    /// Turn ID that this command belongs to.    pub turn_id: String,    #[serde(default)]    pub started_at_ms: i64,    /// The command to be executed.    pub command: Vec<String>,    /// The command's working directory if not the default cwd for the agent.    pub cwd: AbsolutePathBuf,    pub parsed_cmd: Vec<ParsedCommand>,    /// Where the command originated. Defaults to Agent for backward compatibility.    #[serde(default)]    pub source: ExecCommandSource,    /// Raw input sent to a unified exec session (if this is an interaction event).    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub interaction_input: Option<String>,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct ExecCommandEndEvent {    /// Identifier for the ExecCommandBegin that finished.    pub call_id: String,    /// Identifier for the underlying PTY process (when available).    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub process_id: Option<String>,    /// Turn ID that this command belongs to.    pub turn_id: String,    #[serde(default)]    pub completed_at_ms: i64,    /// The command that was executed.    pub command: Vec<String>,    /// The command's working directory if not the default cwd for the agent.    pub cwd: AbsolutePathBuf,    pub parsed_cmd: Vec<ParsedCommand>,    /// Where the command originated. Defaults to Agent for backward compatibility.    #[serde(default)]    pub source: ExecCommandSource,    /// Raw input sent to a unified exec session (if this is an interaction event).    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub interaction_input: Option<String>,    /// Captured stdout    pub stdout: String,    /// Captured stderr    pub stderr: String,    /// Captured aggregated output    #[serde(default)]    pub aggregated_output: String,    /// The command's exit code.    pub exit_code: i32,    /// The duration of the command execution.    #[ts(type = "string")]    pub duration: Duration,    /// Formatted output from the command, as seen by the model.    pub formatted_output: String,    /// Completion status for this command execution.    pub status: ExecCommandStatus,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct ViewImageToolCallEvent {    /// Identifier for the originating tool call.    pub call_id: String,    /// Local filesystem path provided to the tool.    pub path: AbsolutePathBuf,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub enum ExecOutputStream {    Stdout,    Stderr,}#[serde_as]#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct ExecCommandOutputDeltaEvent {    /// Identifier for the ExecCommandBegin that produced this chunk.    pub call_id: String,    /// Which stream produced this chunk.    pub stream: ExecOutputStream,    /// Raw bytes from the stream (may not be valid UTF-8).    #[serde_as(as = "serde_with::base64::Base64")]    #[schemars(with = "String")]    #[ts(type = "string")]    pub chunk: Vec<u8>,}#[serde_as]#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct TerminalInteractionEvent {    /// Identifier for the ExecCommandBegin that produced this chunk.    pub call_id: String,    /// Process id associated with the running command.    pub process_id: String,    /// Stdin sent to the running session.    pub stdin: String,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct DeprecationNoticeEvent {    /// Concise summary of what is deprecated.    pub summary: String,    /// Optional extra guidance, such as migration steps or rationale.    #[serde(skip_serializing_if = "Option::is_none")]    pub details: Option<String>,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct ThreadRolledBackEvent {    /// Number of user turns that were removed from context.    pub num_turns: u32,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct StreamErrorEvent {    pub message: String,    #[serde(default)]    pub codex_error_info: Option<CodexErrorInfo>,    /// Optional details about the underlying stream failure (often the same    /// human-readable message that is surfaced as the terminal error if retries    /// are exhausted).    #[serde(default)]    pub additional_details: Option<String>,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct StreamInfoEvent {    pub message: String,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct PatchApplyBeginEvent {    /// Identifier so this can be paired with the PatchApplyEnd event.    pub call_id: String,    /// Turn ID that this patch belongs to.    /// Uses `#[serde(default)]` for backwards compatibility.    #[serde(default)]    pub turn_id: String,    /// If true, there was no ApplyPatchApprovalRequest for this patch.    pub auto_approved: bool,    /// The changes to be applied.    pub changes: HashMap<PathBuf, FileChange>,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct PatchApplyUpdatedEvent {    /// Identifier for the originating `apply_patch` tool call.    pub call_id: String,    /// Structured file changes parsed from the model-generated patch input so far.    pub changes: HashMap<PathBuf, FileChange>,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct PatchApplyEndEvent {    /// Identifier for the PatchApplyBegin that finished.    pub call_id: String,    /// Turn ID that this patch belongs to.    /// Uses `#[serde(default)]` for backwards compatibility.    #[serde(default)]    pub turn_id: String,    /// Captured stdout (summary printed by apply_patch).    pub stdout: String,    /// Captured stderr (parser errors, IO failures, etc.).    pub stderr: String,    /// Whether the patch was applied successfully.    pub success: bool,    /// The changes that were applied (mirrors PatchApplyBeginEvent::changes).    #[serde(default)]    pub changes: HashMap<PathBuf, FileChange>,    /// Completion status for this patch application.    pub status: PatchApplyStatus,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub enum PatchApplyStatus {    Completed,    Failed,    Declined,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct TurnDiffEvent {    pub unified_diff: String,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct McpStartupUpdateEvent {    /// Server name being started.    pub server: String,    /// Current startup status.    pub status: McpStartupStatus,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]#[serde(rename_all = "snake_case", tag = "state")]#[ts(rename_all = "snake_case", tag = "state")]pub enum McpStartupStatus {    Starting,    Ready,    Failed { error: String },    Cancelled,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, Default)]pub struct McpStartupCompleteEvent {    pub ready: Vec<String>,    pub failed: Vec<McpStartupFailure>,    pub cancelled: Vec<String>,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct McpStartupFailure {    pub server: String,    pub error: String,}#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]#[serde(rename_all = "snake_case")]#[ts(rename_all = "snake_case")]pub enum McpAuthStatus {    Unsupported,    NotLoggedIn,    BearerToken,    OAuth,}impl fmt::Display for McpAuthStatus {    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {        let text = match self {            McpAuthStatus::Unsupported => "Unsupported",            McpAuthStatus::NotLoggedIn => "Not logged in",            McpAuthStatus::BearerToken => "Bearer token",            McpAuthStatus::OAuth => "OAuth",        };        f.write_str(text)    }}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]pub struct RealtimeConversationListVoicesResponseEvent {    pub voices: RealtimeVoicesList,}#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)]#[serde(rename_all = "lowercase")]#[ts(rename_all = "lowercase")]pub enum Product {    #[serde(alias = "CHATGPT")]    Chatgpt,    #[serde(alias = "CODEX")]    Codex,    #[serde(alias = "ATLAS")]    Atlas,}impl Product {    pub fn to_app_platform(self) -> &'static str {        match self {            Self::Chatgpt => "chat",            Self::Codex => "codex",            Self::Atlas => "atlas",        }    }    pub fn from_session_source_name(value: &str) -> Option<Self> {        let normalized = value.trim().to_ascii_lowercase();        match normalized.as_str() {            "chatgpt" => Some(Self::Chatgpt),            "codex" => Some(Self::Codex),            "atlas" => Some(Self::Atlas),            _ => None,        }    }    pub fn matches_product_restriction(&self, products: &[Product]) -> bool {        products.is_empty() || products.contains(self)    }}#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]#[serde(rename_all = "snake_case")]#[ts(rename_all = "snake_case")]pub enum SkillScope {    User,    Repo,    System,    Admin,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct SkillMetadata {    pub name: String,    pub description: String,    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    /// Legacy short_description from SKILL.md. Prefer SKILL.json interface.short_description.    pub short_description: Option<String>,    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub interface: Option<SkillInterface>,    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub dependencies: Option<SkillDependencies>,    pub path: AbsolutePathBuf,    pub scope: SkillScope,    pub enabled: bool,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)]pub struct SkillInterface {    #[ts(optional)]    pub display_name: Option<String>,    #[ts(optional)]    pub short_description: Option<String>,    #[ts(optional)]    pub icon_small: Option<AbsolutePathBuf>,    #[ts(optional)]    pub icon_large: Option<AbsolutePathBuf>,    #[ts(optional)]    pub brand_color: Option<String>,    #[ts(optional)]    pub default_prompt: Option<String>,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)]pub struct SkillDependencies {    pub tools: Vec<SkillToolDependency>,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)]pub struct SkillToolDependency {    #[serde(rename = "type")]    #[ts(rename = "type")]    pub r#type: String,    pub value: String,    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub description: Option<String>,    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub transport: Option<String>,    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub command: Option<String>,    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub url: Option<String>,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)]pub struct SessionNetworkProxyRuntime {    pub http_addr: String,    pub socks_addr: String,}#[derive(Debug, Clone, Serialize, JsonSchema, TS)]pub struct SessionConfiguredEvent {    pub session_id: SessionId,    pub thread_id: ThreadId,    #[serde(skip_serializing_if = "Option::is_none")]    pub forked_from_id: Option<ThreadId>,    #[serde(skip_serializing_if = "Option::is_none")]    pub parent_thread_id: Option<ThreadId>,    /// Optional analytics source classification for this thread.    #[serde(default, skip_serializing_if = "Option::is_none")]    pub thread_source: Option<ThreadSource>,    /// Optional user-facing thread name (may be unset).    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub thread_name: Option<String>,    /// Tell the client what model is being queried.    pub model: String,    pub model_provider_id: String,    #[serde(skip_serializing_if = "Option::is_none")]    pub service_tier: Option<String>,    /// When to escalate for approval for execution    pub approval_policy: AskForApproval,    /// Configures who approval requests are routed to for review once they have    /// been escalated. This does not disable separate safety checks such as    /// ARC.    #[serde(default)]    pub approvals_reviewer: ApprovalsReviewer,    /// Canonical effective permissions for commands executed in the session.    pub permission_profile: PermissionProfile,    /// Named or implicit built-in profile that produced `permission_profile`,    /// when known.    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub active_permission_profile: Option<ActivePermissionProfile>,    /// Working directory that should be treated as the *root* of the    /// session.    pub cwd: AbsolutePathBuf,    /// The effort the model is putting into reasoning about the user's request.    #[serde(skip_serializing_if = "Option::is_none")]    pub reasoning_effort: Option<ReasoningEffortConfig>,    /// Optional initial messages (as events) for resumed sessions.    /// When present, UIs can use these to seed the history.    #[serde(skip_serializing_if = "Option::is_none")]    pub initial_messages: Option<Vec<EventMsg>>,    /// Runtime proxy bind addresses, when the managed proxy was started for this session.    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub network_proxy: Option<SessionNetworkProxyRuntime>,    /// Path in which the rollout is stored. Can be `None` for ephemeral threads    #[serde(skip_serializing_if = "Option::is_none")]    pub rollout_path: Option<PathBuf>,}impl<'de> Deserialize<'de> for SessionConfiguredEvent {    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>    where        D: serde::Deserializer<'de>,    {        #[derive(Deserialize)]        struct Wire {            session_id: SessionId,            #[serde(default)]            thread_id: Option<ThreadId>,            forked_from_id: Option<ThreadId>,            parent_thread_id: Option<ThreadId>,            #[serde(default)]            thread_source: Option<ThreadSource>,            #[serde(default)]            thread_name: Option<String>,            model: String,            model_provider_id: String,            service_tier: Option<String>,            approval_policy: AskForApproval,            #[serde(default)]            approvals_reviewer: ApprovalsReviewer,            // `SessionConfiguredEvent` is persisted into rollout history. Older            // rollouts only have `sandbox_policy`, so accept it on deserialize            // and immediately project it into the canonical `permission_profile`.            sandbox_policy: Option<SandboxPolicy>,            permission_profile: Option<PermissionProfile>,            #[serde(default)]            active_permission_profile: Option<ActivePermissionProfile>,            cwd: AbsolutePathBuf,            reasoning_effort: Option<ReasoningEffortConfig>,            initial_messages: Option<Vec<EventMsg>>,            network_proxy: Option<SessionNetworkProxyRuntime>,            rollout_path: Option<PathBuf>,        }        let wire = Wire::deserialize(deserializer)?;        let permission_profile = match (wire.permission_profile, wire.sandbox_policy) {            (Some(permission_profile), _) => permission_profile,            (None, Some(sandbox_policy)) => PermissionProfile::from_legacy_sandbox_policy_for_cwd(                &sandbox_policy,                wire.cwd.as_path(),            ),            (None, None) => {                return Err(serde::de::Error::missing_field("permission_profile"));            }        };        Ok(Self {            session_id: wire.session_id,            thread_id: wire.thread_id.unwrap_or_else(|| wire.session_id.into()),            forked_from_id: wire.forked_from_id,            parent_thread_id: wire.parent_thread_id,            thread_source: wire.thread_source,            thread_name: wire.thread_name,            model: wire.model,            model_provider_id: wire.model_provider_id,            service_tier: wire.service_tier,            approval_policy: wire.approval_policy,            approvals_reviewer: wire.approvals_reviewer,            permission_profile,            active_permission_profile: wire.active_permission_profile,            cwd: wire.cwd,            reasoning_effort: wire.reasoning_effort,            initial_messages: wire.initial_messages,            network_proxy: wire.network_proxy,            rollout_path: wire.rollout_path,        })    }}#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "camelCase")]#[ts(export_to = "protocol/")]pub enum ThreadGoalStatus {    Active,    Paused,    Blocked,    UsageLimited,    BudgetLimited,    Complete,}pub const MAX_THREAD_GOAL_OBJECTIVE_CHARS: usize = 4_000;pub fn validate_thread_goal_objective(value: &str) -> Result<(), String> {    if value.is_empty() {        return Err("goal objective must not be empty".to_string());    }    if value.chars().count() > MAX_THREAD_GOAL_OBJECTIVE_CHARS {        return Err(format!(            "goal objective must be at most {MAX_THREAD_GOAL_OBJECTIVE_CHARS} characters"        ));    }    Ok(())}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "camelCase")]#[ts(export_to = "protocol/")]pub struct ThreadGoal {    pub thread_id: ThreadId,    pub objective: String,    pub status: ThreadGoalStatus,    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub token_budget: Option<i64>,    pub tokens_used: i64,    pub time_used_seconds: i64,    pub created_at: i64,    pub updated_at: i64,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "camelCase")]#[ts(export_to = "protocol/")]pub struct ThreadGoalUpdatedEvent {    pub thread_id: ThreadId,    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub turn_id: Option<String>,    pub goal: ThreadGoal,}/// User's decision in response to an ExecApprovalRequest.#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, Display, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub enum ReviewDecision {    /// User has approved this command and the agent should execute it.    Approved,    /// User has approved this command and wants to apply the proposed execpolicy    /// amendment so future matching commands are permitted.    ApprovedExecpolicyAmendment {        proposed_execpolicy_amendment: ExecPolicyAmendment,    },    /// User has approved this request and wants future prompts in the same    /// session-scoped approval cache to be automatically approved for the    /// remainder of the session.    ApprovedForSession,    /// User chose to persist a network policy rule (allow/deny) for future    /// requests to the same host.    NetworkPolicyAmendment {        network_policy_amendment: NetworkPolicyAmendment,    },    /// User has denied this command and the agent should not execute it, but    /// it should continue the session and try something else.    #[default]    Denied,    /// Automatic approval review timed out before reaching a decision.    TimedOut,    /// User has denied this command and the agent should not do anything until    /// the user's next command.    Abort,}impl ReviewDecision {    /// Returns an opaque version of the decision without PII. We can't use an ignored flag    /// on `serde` because the serialization is required by some surfaces.    pub fn to_opaque_string(&self) -> &'static str {        match self {            ReviewDecision::Approved => "approved",            ReviewDecision::ApprovedExecpolicyAmendment { .. } => "approved_with_amendment",            ReviewDecision::ApprovedForSession => "approved_for_session",            ReviewDecision::NetworkPolicyAmendment {                network_policy_amendment,            } => match network_policy_amendment.action {                NetworkPolicyRuleAction::Allow => "approved_with_network_policy_allow",                NetworkPolicyRuleAction::Deny => "denied_with_network_policy_deny",            },            ReviewDecision::Denied => "denied",            ReviewDecision::TimedOut => "timed_out",            ReviewDecision::Abort => "abort",        }    }}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]#[serde(tag = "type", rename_all = "snake_case")]#[ts(tag = "type")]pub enum FileChange {    Add {        content: String,    },    Delete {        content: String,    },    Update {        unified_diff: String,        move_path: Option<PathBuf>,    },}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct Chunk {    /// 1-based line index of the first line in the original file    pub orig_index: u32,    pub deleted_lines: Vec<String>,    pub inserted_lines: Vec<String>,}#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]pub struct TurnAbortedEvent {    pub turn_id: Option<String>,    pub reason: TurnAbortReason,    /// Unix timestamp (in seconds) when the turn was aborted.    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(type = "number | null", optional)]    pub completed_at: Option<i64>,    /// Duration between turn start and abort in milliseconds, if known.    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(type = "number | null", optional)]    pub duration_ms: Option<i64>,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub enum TurnAbortReason {    Interrupted,    Replaced,    ReviewEnded,    BudgetLimited,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct CollabAgentSpawnBeginEvent {    /// Identifier for the collab tool call.    pub call_id: String,    #[serde(default)]    pub started_at_ms: i64,    /// Thread ID of the sender.    pub sender_thread_id: ThreadId,    /// Initial prompt sent to the agent. Can be empty to prevent CoT leaking at the    /// beginning.    pub prompt: String,    pub model: String,    pub reasoning_effort: ReasoningEffortConfig,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]pub struct CollabAgentRef {    /// Thread ID of the receiver/new agent.    pub thread_id: ThreadId,    /// Optional nickname assigned to an AgentControl-spawned sub-agent.    #[serde(default, skip_serializing_if = "Option::is_none")]    pub agent_nickname: Option<String>,    /// Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.    #[serde(default, alias = "agent_type", skip_serializing_if = "Option::is_none")]    pub agent_role: Option<String>,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]pub struct CollabAgentStatusEntry {    /// Thread ID of the receiver/new agent.    pub thread_id: ThreadId,    /// Optional nickname assigned to an AgentControl-spawned sub-agent.    #[serde(default, skip_serializing_if = "Option::is_none")]    pub agent_nickname: Option<String>,    /// Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.    #[serde(default, alias = "agent_type", skip_serializing_if = "Option::is_none")]    pub agent_role: Option<String>,    /// Last known status of the agent.    pub status: AgentStatus,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct CollabAgentSpawnEndEvent {    /// Identifier for the collab tool call.    pub call_id: String,    #[serde(default)]    pub completed_at_ms: i64,    /// Thread ID of the sender.    pub sender_thread_id: ThreadId,    /// Thread ID of the newly spawned agent, if it was created.    pub new_thread_id: Option<ThreadId>,    /// Optional nickname assigned to the new agent.    #[serde(default, skip_serializing_if = "Option::is_none")]    pub new_agent_nickname: Option<String>,    /// Optional role assigned to the new agent.    #[serde(default, skip_serializing_if = "Option::is_none")]    pub new_agent_role: Option<String>,    /// Initial prompt sent to the agent. Can be empty to prevent CoT leaking at the    /// beginning.    pub prompt: String,    /// Effective model used by the spawned agent after inheritance and role overrides.    pub model: String,    /// Effective reasoning effort used by the spawned agent after inheritance and role overrides.    pub reasoning_effort: ReasoningEffortConfig,    /// Last known status of the new agent reported to the sender agent.    pub status: AgentStatus,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct CollabAgentInteractionBeginEvent {    /// Identifier for the collab tool call.    pub call_id: String,    #[serde(default)]    pub started_at_ms: i64,    /// Thread ID of the sender.    pub sender_thread_id: ThreadId,    /// Thread ID of the receiver.    pub receiver_thread_id: ThreadId,    /// Prompt sent from the sender to the receiver. Can be empty to prevent CoT    /// leaking at the beginning.    pub prompt: String,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct CollabAgentInteractionEndEvent {    /// Identifier for the collab tool call.    pub call_id: String,    #[serde(default)]    pub completed_at_ms: i64,    /// Thread ID of the sender.    pub sender_thread_id: ThreadId,    /// Thread ID of the receiver.    pub receiver_thread_id: ThreadId,    /// Optional nickname assigned to the receiver agent.    #[serde(default, skip_serializing_if = "Option::is_none")]    pub receiver_agent_nickname: Option<String>,    /// Optional role assigned to the receiver agent.    #[serde(default, skip_serializing_if = "Option::is_none")]    pub receiver_agent_role: Option<String>,    /// Prompt sent from the sender to the receiver. Can be empty to prevent CoT    /// leaking at the beginning.    pub prompt: String,    /// Last known status of the receiver agent reported to the sender agent.    pub status: AgentStatus,}#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]#[ts(rename_all = "snake_case")]pub enum SubAgentActivityKind {    Started,    Interacted,    Interrupted,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]pub struct SubAgentActivityEvent {    pub event_id: String,    #[serde(default)]    pub occurred_at_ms: i64,    /// Thread ID of the affected sub-agent.    pub agent_thread_id: ThreadId,    /// Canonical v2 path of the affected sub-agent.    pub agent_path: AgentPath,    pub kind: SubAgentActivityKind,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct CollabWaitingBeginEvent {    #[serde(default)]    pub started_at_ms: i64,    /// Thread ID of the sender.    pub sender_thread_id: ThreadId,    /// Thread ID of the receivers.    pub receiver_thread_ids: Vec<ThreadId>,    /// Optional nicknames/roles for receivers.    #[serde(default, skip_serializing_if = "Vec::is_empty")]    pub receiver_agents: Vec<CollabAgentRef>,    /// ID of the waiting call.    pub call_id: String,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct CollabWaitingEndEvent {    /// Thread ID of the sender.    pub sender_thread_id: ThreadId,    /// ID of the waiting call.    pub call_id: String,    #[serde(default)]    pub completed_at_ms: i64,    /// Optional receiver metadata paired with final statuses.    #[serde(default, skip_serializing_if = "Vec::is_empty")]    pub agent_statuses: Vec<CollabAgentStatusEntry>,    /// Last known status of the receiver agents reported to the sender agent.    pub statuses: HashMap<ThreadId, AgentStatus>,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct CollabCloseBeginEvent {    /// Identifier for the collab tool call.    pub call_id: String,    #[serde(default)]    pub started_at_ms: i64,    /// Thread ID of the sender.    pub sender_thread_id: ThreadId,    /// Thread ID of the receiver.    pub receiver_thread_id: ThreadId,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct CollabCloseEndEvent {    /// Identifier for the collab tool call.    pub call_id: String,    #[serde(default)]    pub completed_at_ms: i64,    /// Thread ID of the sender.    pub sender_thread_id: ThreadId,    /// Thread ID of the receiver.    pub receiver_thread_id: ThreadId,    /// Optional nickname assigned to the receiver agent.    #[serde(default, skip_serializing_if = "Option::is_none")]    pub receiver_agent_nickname: Option<String>,    /// Optional role assigned to the receiver agent.    #[serde(default, skip_serializing_if = "Option::is_none")]    pub receiver_agent_role: Option<String>,    /// Last known status of the receiver agent reported to the sender agent before    /// the close.    pub status: AgentStatus,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct CollabResumeBeginEvent {    /// Identifier for the collab tool call.    pub call_id: String,    #[serde(default)]    pub started_at_ms: i64,    /// Thread ID of the sender.    pub sender_thread_id: ThreadId,    /// Thread ID of the receiver.    pub receiver_thread_id: ThreadId,    /// Optional nickname assigned to the receiver agent.    #[serde(default, skip_serializing_if = "Option::is_none")]    pub receiver_agent_nickname: Option<String>,    /// Optional role assigned to the receiver agent.    #[serde(default, skip_serializing_if = "Option::is_none")]    pub receiver_agent_role: Option<String>,}#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]pub struct CollabResumeEndEvent {    /// Identifier for the collab tool call.    pub call_id: String,    #[serde(default)]    pub completed_at_ms: i64,    /// Thread ID of the sender.    pub sender_thread_id: ThreadId,    /// Thread ID of the receiver.    pub receiver_thread_id: ThreadId,    /// Optional nickname assigned to the receiver agent.    #[serde(default, skip_serializing_if = "Option::is_none")]    pub receiver_agent_nickname: Option<String>,    /// Optional role assigned to the receiver agent.    #[serde(default, skip_serializing_if = "Option::is_none")]    pub receiver_agent_role: Option<String>,    /// Last known status of the receiver agent reported to the sender agent after    /// resume.    pub status: AgentStatus,}#[cfg(test)]mod tests {    use super::*;    use crate::items::FileChangeItem;    use crate::items::ImageGenerationItem;    use crate::items::McpToolCallItem;    use crate::items::McpToolCallStatus;    use crate::items::UserMessageItem;    use crate::items::WebSearchItem;    use crate::mcp::CallToolResult;    use crate::permissions::FileSystemAccessMode;    use crate::permissions::FileSystemPath;    use crate::permissions::FileSystemSandboxEntry;    use crate::permissions::FileSystemSandboxPolicy;    use crate::permissions::FileSystemSpecialPath;    use crate::permissions::NetworkSandboxPolicy;    use anyhow::Result;    use codex_utils_absolute_path::AbsolutePathBuf;    use codex_utils_absolute_path::test_support::PathBufExt;    use codex_utils_absolute_path::test_support::test_path_buf;    use pretty_assertions::assert_eq;    use serde_json::json;    use std::path::PathBuf;    use tempfile::NamedTempFile;    use tempfile::TempDir;    #[test]    fn feature_thread_source_serializes_as_its_app_owned_label() -> Result<()> {        let source = ThreadSource::Feature("automation".to_string());        assert_eq!(serde_json::to_value(&source)?, json!("automation"));        assert_eq!(            serde_json::from_value::<ThreadSource>(json!("automation"))?,            source        );        Ok(())    }    #[test]    fn session_meta_normalizes_legacy_dynamic_tools() -> Result<()> {        let mut value = serde_json::to_value(SessionMeta::default())?;        value["dynamic_tools"] = json!([            {                "namespace": "legacy_app",                "name": "lookup_ticket",                "description": "Look up a ticket",                "inputSchema": {"type": "object", "properties": {}},                "exposeToContext": false            },            {                "namespace": "legacy_app",                "name": "update_ticket",                "description": "Update a ticket",                "inputSchema": {"type": "object", "properties": {}},                "deferLoading": false,                "exposeToContext": false            }        ]);        let meta: SessionMeta = serde_json::from_value(value)?;        assert_eq!(            meta.dynamic_tools,            Some(vec![DynamicToolSpec::Namespace(                crate::dynamic_tools::DynamicToolNamespaceSpec {                    name: "legacy_app".to_string(),                    description: String::new(),                    tools: vec![                        crate::dynamic_tools::DynamicToolNamespaceTool::Function(                            crate::dynamic_tools::DynamicToolFunctionSpec {                                name: "lookup_ticket".to_string(),                                description: "Look up a ticket".to_string(),                                input_schema: json!({"type": "object", "properties": {}}),                                defer_loading: true,                            },                        ),                        crate::dynamic_tools::DynamicToolNamespaceTool::Function(                            crate::dynamic_tools::DynamicToolFunctionSpec {                                name: "update_ticket".to_string(),                                description: "Update a ticket".to_string(),                                input_schema: json!({"type": "object", "properties": {}}),                                defer_loading: false,                            },                        ),                    ],                },            )])        );        Ok(())    }    fn sorted_writable_roots(roots: Vec<WritableRoot>) -> Vec<(PathBuf, Vec<PathBuf>)> {        let mut sorted_roots: Vec<(PathBuf, Vec<PathBuf>)> = roots            .into_iter()            .map(|root| {                let mut read_only_subpaths: Vec<PathBuf> = root                    .read_only_subpaths                    .into_iter()                    .map(|path| path.to_path_buf())                    .collect();                read_only_subpaths.sort();                (root.root.to_path_buf(), read_only_subpaths)            })            .collect();        sorted_roots.sort_by(|left, right| left.0.cmp(&right.0));        sorted_roots    }    fn sandbox_policy_allows_read(policy: &SandboxPolicy, _path: &Path, _cwd: &Path) -> bool {        policy.has_full_disk_read_access()    }    fn sandbox_policy_allows_write(policy: &SandboxPolicy, path: &Path, cwd: &Path) -> bool {        if policy.has_full_disk_write_access() {            return true;        }        policy            .get_writable_roots_with_cwd(cwd)            .iter()            .any(|root| root.is_path_writable(path))    }    #[test]    fn session_source_from_startup_arg_maps_known_values() {        assert_eq!(            SessionSource::from_startup_arg("vscode").unwrap(),            SessionSource::VSCode        );        assert_eq!(            SessionSource::from_startup_arg("app-server").unwrap(),            SessionSource::Mcp        );    }    #[test]    fn inter_agent_communication_response_input_item_preserves_commentary_phase() {        let communication = InterAgentCommunication {            author: AgentPath::root(),            recipient: AgentPath::root().join("reviewer").expect("recipient path"),            other_recipients: vec![AgentPath::root().join("worker").expect("recipient path")],            content: "review the diff".to_string(),            encrypted_content: None,            metadata: None,            trigger_turn: true,        };        assert_eq!(            communication.to_response_input_item(),            ResponseInputItem::Message {                role: "assistant".to_string(),                content: vec![ContentItem::OutputText {                    text: serde_json::to_string(&communication).expect("serialize communication"),                }],                phase: Some(MessagePhase::Commentary),            }        );    }    #[test]    fn queued_encrypted_inter_agent_communication_renders_message_envelope() {        let communication = InterAgentCommunication::new_encrypted(            AgentPath::root().join("worker").expect("author path"),            AgentPath::root(),            Vec::new(),            "encrypted payload".to_string(),            /*trigger_turn*/ false,        );        assert_eq!(            communication.to_model_input_item(),            ResponseItem::AgentMessage {                author: "/root/worker".to_string(),                recipient: "/root".to_string(),                content: vec![                    AgentMessageInputContent::InputText {                        text: "Message Type: MESSAGE\nTask name: /root\nSender: /root/worker\nPayload:\n"                            .to_string(),                    },                    AgentMessageInputContent::EncryptedContent {                        encrypted_content: "encrypted payload".to_string(),                    },                ],                metadata: None,            }        );    }    #[test]    fn session_source_from_startup_arg_normalizes_custom_values() {        assert_eq!(            SessionSource::from_startup_arg("atlas").unwrap(),            SessionSource::Custom("atlas".to_string())        );        assert_eq!(            SessionSource::from_startup_arg(" Atlas ").unwrap(),            SessionSource::Custom("atlas".to_string())        );    }    #[test]    fn session_source_restriction_product_defaults_non_subagent_sources_to_codex() {        assert_eq!(            SessionSource::Cli.restriction_product(),            Some(Product::Codex)        );        assert_eq!(            SessionSource::VSCode.restriction_product(),            Some(Product::Codex)        );        assert_eq!(            SessionSource::Exec.restriction_product(),            Some(Product::Codex)        );        assert_eq!(            SessionSource::Mcp.restriction_product(),            Some(Product::Codex)        );        assert_eq!(            SessionSource::Unknown.restriction_product(),            Some(Product::Codex)        );    }    #[test]    fn session_source_restriction_product_does_not_guess_subagent_products() {        assert_eq!(            SessionSource::SubAgent(SubAgentSource::Review).restriction_product(),            None        );        assert_eq!(            SessionSource::Internal(InternalSessionSource::MemoryConsolidation)                .restriction_product(),            None        );    }    #[test]    fn session_source_restriction_product_maps_custom_sources_to_products() {        assert_eq!(            SessionSource::Custom("chatgpt".to_string()).restriction_product(),            Some(Product::Chatgpt)        );        assert_eq!(            SessionSource::Custom("ATLAS".to_string()).restriction_product(),            Some(Product::Atlas)        );        assert_eq!(            SessionSource::Custom("codex".to_string()).restriction_product(),            Some(Product::Codex)        );        assert_eq!(            SessionSource::Custom("atlas-dev".to_string()).restriction_product(),            None        );    }    #[test]    fn session_source_matches_product_restriction() {        assert!(            SessionSource::Custom("chatgpt".to_string())                .matches_product_restriction(&[Product::Chatgpt])        );        assert!(            !SessionSource::Custom("chatgpt".to_string())                .matches_product_restriction(&[Product::Codex])        );        assert!(SessionSource::VSCode.matches_product_restriction(&[Product::Codex]));        assert!(            !SessionSource::Custom("atlas-dev".to_string())                .matches_product_restriction(&[Product::Atlas])        );        assert!(SessionSource::Custom("atlas-dev".to_string()).matches_product_restriction(&[]));    }    fn sandbox_policy_probe_paths(policy: &SandboxPolicy, cwd: &Path) -> Vec<PathBuf> {        let mut paths = vec![cwd.to_path_buf()];        for root in policy.get_writable_roots_with_cwd(cwd) {            paths.push(root.root.to_path_buf());            paths.extend(                root.read_only_subpaths                    .into_iter()                    .map(|path| path.to_path_buf()),            );        }        paths.sort();        paths.dedup();        paths    }    fn assert_same_sandbox_policy_semantics(        expected: &SandboxPolicy,        actual: &SandboxPolicy,        cwd: &Path,    ) {        assert_eq!(            actual.has_full_disk_read_access(),            expected.has_full_disk_read_access()        );        assert_eq!(            actual.has_full_disk_write_access(),            expected.has_full_disk_write_access()        );        assert_eq!(            actual.has_full_network_access(),            expected.has_full_network_access()        );        let mut probe_paths = sandbox_policy_probe_paths(expected, cwd);        probe_paths.extend(sandbox_policy_probe_paths(actual, cwd));        probe_paths.sort();        probe_paths.dedup();        for path in probe_paths {            assert_eq!(                sandbox_policy_allows_read(actual, &path, cwd),                sandbox_policy_allows_read(expected, &path, cwd),                "read access mismatch for {}",                path.display()            );            assert_eq!(                sandbox_policy_allows_write(actual, &path, cwd),                sandbox_policy_allows_write(expected, &path, cwd),                "write access mismatch for {}",                path.display()            );        }    }    #[test]    fn external_sandbox_reports_full_access_flags() {        let restricted = SandboxPolicy::ExternalSandbox {            network_access: NetworkAccess::Restricted,        };        assert!(restricted.has_full_disk_write_access());        assert!(!restricted.has_full_network_access());        let enabled = SandboxPolicy::ExternalSandbox {            network_access: NetworkAccess::Enabled,        };        assert!(enabled.has_full_disk_write_access());        assert!(enabled.has_full_network_access());    }    #[test]    fn read_only_reports_network_access_flags() {        let restricted = SandboxPolicy::new_read_only_policy();        assert!(!restricted.has_full_network_access());        let enabled = SandboxPolicy::ReadOnly {            network_access: true,        };        assert!(enabled.has_full_network_access());    }    #[test]    fn granular_approval_config_mcp_elicitation_flag_is_field_driven() {        assert!(            GranularApprovalConfig {                sandbox_approval: false,                rules: false,                skill_approval: false,                request_permissions: false,                mcp_elicitations: true,            }            .allows_mcp_elicitations()        );        assert!(            !GranularApprovalConfig {                sandbox_approval: false,                rules: false,                skill_approval: false,                request_permissions: false,                mcp_elicitations: false,            }            .allows_mcp_elicitations()        );    }    #[test]    fn granular_approval_config_skill_approval_flag_is_field_driven() {        assert!(            GranularApprovalConfig {                sandbox_approval: false,                rules: false,                skill_approval: true,                request_permissions: false,                mcp_elicitations: false,            }            .allows_skill_approval()        );        assert!(            !GranularApprovalConfig {                sandbox_approval: false,                rules: false,                skill_approval: false,                request_permissions: false,                mcp_elicitations: false,            }            .allows_skill_approval()        );    }    #[test]    fn granular_approval_config_request_permissions_flag_is_field_driven() {        assert!(            GranularApprovalConfig {                sandbox_approval: false,                rules: false,                skill_approval: false,                request_permissions: true,                mcp_elicitations: false,            }            .allows_request_permissions()        );        assert!(            !GranularApprovalConfig {                sandbox_approval: false,                rules: false,                skill_approval: false,                request_permissions: false,                mcp_elicitations: false,            }            .allows_request_permissions()        );    }    #[test]    fn granular_approval_config_defaults_missing_optional_flags_to_false() {        let decoded = serde_json::from_value::<GranularApprovalConfig>(serde_json::json!({            "sandbox_approval": true,            "rules": false,            "mcp_elicitations": true,        }))        .expect("granular approval config should deserialize");        assert_eq!(            decoded,            GranularApprovalConfig {                sandbox_approval: true,                rules: false,                skill_approval: false,                request_permissions: false,                mcp_elicitations: true,            }        );    }    #[test]    fn restricted_file_system_policy_reports_full_access_from_root_entries() {        let read_only = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {            path: FileSystemPath::Special {                value: FileSystemSpecialPath::Root,            },            access: FileSystemAccessMode::Read,        }]);        assert!(read_only.has_full_disk_read_access());        assert!(!read_only.has_full_disk_write_access());        assert!(!read_only.include_platform_defaults());        let writable = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {            path: FileSystemPath::Special {                value: FileSystemSpecialPath::Root,            },            access: FileSystemAccessMode::Write,        }]);        assert!(writable.has_full_disk_read_access());        assert!(writable.has_full_disk_write_access());    }    #[test]    fn restricted_file_system_policy_treats_root_with_carveouts_as_scoped_access() {        let cwd = TempDir::new().expect("tempdir");        let canonical_cwd = codex_utils_absolute_path::canonicalize_preserving_symlinks(cwd.path())            .expect("canonicalize cwd");        let root = AbsolutePathBuf::from_absolute_path(&canonical_cwd)            .expect("absolute canonical tempdir")            .as_path()            .ancestors()            .last()            .and_then(|path| AbsolutePathBuf::from_absolute_path(path).ok())            .expect("filesystem root");        let blocked = AbsolutePathBuf::resolve_path_against_base("blocked", cwd.path());        let expected_blocked = AbsolutePathBuf::from_absolute_path(            codex_utils_absolute_path::canonicalize_preserving_symlinks(cwd.path())                .expect("canonicalize cwd")                .join("blocked"),        )        .expect("canonical blocked");        let policy = FileSystemSandboxPolicy::restricted(vec![            FileSystemSandboxEntry {                path: FileSystemPath::Special {                    value: FileSystemSpecialPath::Root,                },                access: FileSystemAccessMode::Write,            },            FileSystemSandboxEntry {                path: FileSystemPath::Path { path: blocked },                access: FileSystemAccessMode::Deny,            },        ]);        assert!(!policy.has_full_disk_read_access());        assert!(!policy.has_full_disk_write_access());        assert_eq!(            policy.get_readable_roots_with_cwd(cwd.path()),            vec![root.clone()]        );        assert_eq!(            policy.get_unreadable_roots_with_cwd(cwd.path()),            vec![expected_blocked.clone()]        );        let writable_roots = policy.get_writable_roots_with_cwd(cwd.path());        assert_eq!(writable_roots.len(), 1);        assert_eq!(writable_roots[0].root, root);        assert!(            writable_roots[0]                .read_only_subpaths                .iter()                .any(|path| path.as_path() == expected_blocked.as_path())        );    }    #[test]    fn restricted_file_system_policy_derives_effective_paths() {        let cwd = TempDir::new().expect("tempdir");        std::fs::create_dir_all(cwd.path().join(".agents")).expect("create .agents");        std::fs::create_dir_all(cwd.path().join(".codex")).expect("create .codex");        let canonical_cwd = codex_utils_absolute_path::canonicalize_preserving_symlinks(cwd.path())            .expect("canonicalize cwd");        let cwd_absolute =            AbsolutePathBuf::from_absolute_path(&canonical_cwd).expect("absolute tempdir");        let secret = AbsolutePathBuf::resolve_path_against_base("secret", cwd.path());        let expected_secret = AbsolutePathBuf::from_absolute_path(canonical_cwd.join("secret"))            .expect("canonical secret");        let expected_agents = AbsolutePathBuf::from_absolute_path(canonical_cwd.join(".agents"))            .expect("canonical .agents");        let expected_codex = AbsolutePathBuf::from_absolute_path(canonical_cwd.join(".codex"))            .expect("canonical .codex");        let policy = FileSystemSandboxPolicy::restricted(vec![            FileSystemSandboxEntry {                path: FileSystemPath::Special {                    value: FileSystemSpecialPath::Minimal,                },                access: FileSystemAccessMode::Read,            },            FileSystemSandboxEntry {                path: FileSystemPath::Special {                    value: FileSystemSpecialPath::project_roots(/*subpath*/ None),                },                access: FileSystemAccessMode::Write,            },            FileSystemSandboxEntry {                path: FileSystemPath::Path { path: secret },                access: FileSystemAccessMode::Deny,            },        ]);        assert!(!policy.has_full_disk_read_access());        assert!(!policy.has_full_disk_write_access());        assert!(policy.include_platform_defaults());        assert_eq!(            policy.get_readable_roots_with_cwd(cwd.path()),            vec![cwd_absolute.clone()]        );        assert_eq!(            policy.get_unreadable_roots_with_cwd(cwd.path()),            vec![expected_secret.clone()]        );        let writable_roots = policy.get_writable_roots_with_cwd(cwd.path());        assert_eq!(writable_roots.len(), 1);        assert_eq!(writable_roots[0].root, cwd_absolute);        assert!(            writable_roots[0]                .read_only_subpaths                .iter()                .any(|path| path.as_path() == expected_secret.as_path())        );        assert!(            writable_roots[0]                .read_only_subpaths                .iter()                .any(|path| path.as_path() == expected_agents.as_path())        );        assert!(            writable_roots[0]                .read_only_subpaths                .iter()                .any(|path| path.as_path() == expected_codex.as_path())        );    }    #[test]    fn restricted_file_system_policy_treats_read_entries_as_read_only_subpaths() {        let cwd = TempDir::new().expect("tempdir");        let canonical_cwd = codex_utils_absolute_path::canonicalize_preserving_symlinks(cwd.path())            .expect("canonicalize cwd");        let docs = AbsolutePathBuf::resolve_path_against_base("docs", cwd.path());        let docs_public = AbsolutePathBuf::resolve_path_against_base("docs/public", cwd.path());        let expected_docs = AbsolutePathBuf::from_absolute_path(canonical_cwd.join("docs"))            .expect("canonical docs");        let expected_docs_public =            AbsolutePathBuf::from_absolute_path(canonical_cwd.join("docs/public"))                .expect("canonical docs/public");        let expected_dot_codex = AbsolutePathBuf::from_absolute_path(canonical_cwd.join(".codex"))            .expect("canonical .codex");        let policy = FileSystemSandboxPolicy::restricted(vec![            FileSystemSandboxEntry {                path: FileSystemPath::Special {                    value: FileSystemSpecialPath::project_roots(/*subpath*/ None),                },                access: FileSystemAccessMode::Write,            },            FileSystemSandboxEntry {                path: FileSystemPath::Path { path: docs },                access: FileSystemAccessMode::Read,            },            FileSystemSandboxEntry {                path: FileSystemPath::Path { path: docs_public },                access: FileSystemAccessMode::Write,            },        ]);        assert!(!policy.has_full_disk_write_access());        assert_eq!(            sorted_writable_roots(policy.get_writable_roots_with_cwd(cwd.path())),            vec![                (                    canonical_cwd,                    vec![                        expected_dot_codex.to_path_buf(),                        expected_docs.to_path_buf()                    ],                ),                (expected_docs_public.to_path_buf(), Vec::new()),            ]        );    }    #[test]    fn file_system_policy_rejects_legacy_bridge_for_non_workspace_writes() {        let cwd = if cfg!(windows) {            Path::new(r"C:\workspace")        } else {            Path::new("/tmp/workspace")        };        let external_write_path = if cfg!(windows) {            AbsolutePathBuf::from_absolute_path(r"C:\temp").expect("absolute windows temp path")        } else {            AbsolutePathBuf::from_absolute_path("/tmp").expect("absolute tmp path")        };        let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {            path: FileSystemPath::Path {                path: external_write_path,            },            access: FileSystemAccessMode::Write,        }]);        let err = policy            .to_legacy_sandbox_policy(NetworkSandboxPolicy::Restricted, cwd)            .expect_err("non-workspace writes should be rejected");        assert!(            err.to_string()                .contains("filesystem writes outside the workspace root"),            "{err}"        );    }    #[test]    fn legacy_sandbox_policy_semantics_survive_split_bridge() {        let cwd = TempDir::new().expect("tempdir");        let writable_root = AbsolutePathBuf::resolve_path_against_base("writable", cwd.path());        let policies = [            SandboxPolicy::DangerFullAccess,            SandboxPolicy::ExternalSandbox {                network_access: NetworkAccess::Restricted,            },            SandboxPolicy::ExternalSandbox {                network_access: NetworkAccess::Enabled,            },            SandboxPolicy::ReadOnly {                network_access: false,            },            SandboxPolicy::WorkspaceWrite {                writable_roots: vec![],                network_access: false,                exclude_tmpdir_env_var: true,                exclude_slash_tmp: true,            },            SandboxPolicy::WorkspaceWrite {                writable_roots: vec![writable_root],                network_access: true,                exclude_tmpdir_env_var: false,                exclude_slash_tmp: true,            },        ];        for expected in policies {            let actual =                FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&expected, cwd.path())                    .to_legacy_sandbox_policy(NetworkSandboxPolicy::from(&expected), cwd.path())                    .expect("legacy bridge should preserve legacy policy semantics");            assert_same_sandbox_policy_semantics(&expected, &actual, cwd.path());        }    }    #[test]    fn item_started_event_from_web_search_emits_begin_event() {        let event = ItemStartedEvent {            thread_id: ThreadId::new(),            turn_id: "turn-1".into(),            item: TurnItem::WebSearch(WebSearchItem {                id: "search-1".into(),                query: "find docs".into(),                action: WebSearchAction::Search {                    query: Some("find docs".into()),                    queries: None,                },            }),            started_at_ms: 0,        };        let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false);        assert_eq!(legacy_events.len(), 1);        match &legacy_events[0] {            EventMsg::WebSearchBegin(event) => assert_eq!(event.call_id, "search-1"),            _ => panic!("expected WebSearchBegin event"),        }    }    #[test]    fn item_started_event_from_non_web_search_emits_no_legacy_events() {        let event = ItemStartedEvent {            thread_id: ThreadId::new(),            turn_id: "turn-1".into(),            item: TurnItem::UserMessage(UserMessageItem::new(&[])),            started_at_ms: 0,        };        assert!(            event                .as_legacy_events(/*show_raw_agent_reasoning*/ false)                .is_empty()        );    }    #[test]    fn item_started_event_from_image_generation_emits_begin_event() {        let event = ItemStartedEvent {            thread_id: ThreadId::new(),            turn_id: "turn-1".into(),            item: TurnItem::ImageGeneration(ImageGenerationItem {                id: "ig-1".into(),                status: "in_progress".into(),                revised_prompt: None,                result: String::new(),                saved_path: None,            }),            started_at_ms: 0,        };        let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false);        assert_eq!(legacy_events.len(), 1);        match &legacy_events[0] {            EventMsg::ImageGenerationBegin(event) => assert_eq!(event.call_id, "ig-1"),            _ => panic!("expected ImageGenerationBegin event"),        }    }    #[test]    fn item_started_event_from_file_change_emits_patch_begin_event() {        let event = ItemStartedEvent {            thread_id: ThreadId::new(),            turn_id: "turn-1".into(),            started_at_ms: 0,            item: TurnItem::FileChange(FileChangeItem {                id: "patch-1".into(),                changes: [(                    PathBuf::from("new.txt"),                    FileChange::Add {                        content: "hello".into(),                    },                )]                .into_iter()                .collect(),                status: None,                auto_approved: Some(true),                stdout: None,                stderr: None,            }),        };        let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false);        assert_eq!(legacy_events.len(), 1);        match &legacy_events[0] {            EventMsg::PatchApplyBegin(event) => {                assert_eq!(event.call_id, "patch-1");                assert_eq!(event.turn_id, "turn-1");                assert!(event.auto_approved);                assert!(event.changes.contains_key(&PathBuf::from("new.txt")));            }            _ => panic!("expected PatchApplyBegin event"),        }    }    #[test]    fn item_started_event_from_mcp_tool_call_emits_begin_event() {        let event = ItemStartedEvent {            thread_id: ThreadId::new(),            turn_id: "turn-1".into(),            started_at_ms: 0,            item: TurnItem::McpToolCall(McpToolCallItem {                id: "mcp-1".into(),                server: "server".into(),                tool: "tool".into(),                arguments: json!({"arg": "value"}),                mcp_app_resource_uri: Some("app://connector".into()),                plugin_id: Some("sample@test".into()),                status: McpToolCallStatus::InProgress,                result: None,                error: None,                duration: None,            }),        };        let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false);        assert_eq!(legacy_events.len(), 1);        match &legacy_events[0] {            EventMsg::McpToolCallBegin(event) => {                assert_eq!(event.call_id, "mcp-1");                assert_eq!(event.invocation.server, "server");                assert_eq!(event.invocation.tool, "tool");                assert_eq!(                    event.mcp_app_resource_uri.as_deref(),                    Some("app://connector")                );                assert_eq!(event.plugin_id.as_deref(), Some("sample@test"));            }            _ => panic!("expected McpToolCallBegin event"),        }    }    #[test]    fn item_completed_event_from_image_generation_emits_end_event() {        let event = ItemCompletedEvent {            thread_id: ThreadId::new(),            turn_id: "turn-1".into(),            item: TurnItem::ImageGeneration(ImageGenerationItem {                id: "ig-1".into(),                status: "completed".into(),                revised_prompt: Some("A tiny blue square".into()),                result: "Zm9v".into(),                saved_path: Some(test_path_buf("/tmp/ig-1.png").abs()),            }),            completed_at_ms: 0,        };        let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false);        assert_eq!(legacy_events.len(), 1);        match &legacy_events[0] {            EventMsg::ImageGenerationEnd(event) => {                assert_eq!(event.call_id, "ig-1");                assert_eq!(event.status, "completed");                assert_eq!(event.revised_prompt.as_deref(), Some("A tiny blue square"));                assert_eq!(event.result, "Zm9v");                assert_eq!(                    event.saved_path.as_ref().map(AbsolutePathBuf::as_path),                    Some(test_path_buf("/tmp/ig-1.png").as_path())                );            }            _ => panic!("expected ImageGenerationEnd event"),        }    }    #[test]    fn item_completed_event_from_file_change_emits_patch_end_event() {        let event = ItemCompletedEvent {            thread_id: ThreadId::new(),            turn_id: "turn-1".into(),            completed_at_ms: 0,            item: TurnItem::FileChange(FileChangeItem {                id: "patch-1".into(),                changes: [(                    PathBuf::from("new.txt"),                    FileChange::Add {                        content: "hello".into(),                    },                )]                .into_iter()                .collect(),                status: Some(PatchApplyStatus::Completed),                auto_approved: None,                stdout: Some("Done!".into()),                stderr: Some(String::new()),            }),        };        let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false);        assert_eq!(legacy_events.len(), 1);        match &legacy_events[0] {            EventMsg::PatchApplyEnd(event) => {                assert_eq!(event.call_id, "patch-1");                assert_eq!(event.turn_id, "turn-1");                assert_eq!(event.stdout, "Done!");                assert!(event.success);                assert_eq!(event.status, PatchApplyStatus::Completed);                assert!(event.changes.contains_key(&PathBuf::from("new.txt")));            }            _ => panic!("expected PatchApplyEnd event"),        }    }    #[test]    fn item_completed_event_from_mcp_tool_call_emits_end_event() {        let event = ItemCompletedEvent {            thread_id: ThreadId::new(),            turn_id: "turn-1".into(),            completed_at_ms: 0,            item: TurnItem::McpToolCall(McpToolCallItem {                id: "mcp-1".into(),                server: "server".into(),                tool: "tool".into(),                arguments: json!({"arg": "value"}),                mcp_app_resource_uri: Some("app://connector".into()),                plugin_id: Some("sample@test".into()),                status: McpToolCallStatus::Completed,                result: Some(CallToolResult {                    content: vec![json!({"type": "text", "text": "ok"})],                    structured_content: None,                    is_error: Some(false),                    meta: None,                }),                error: None,                duration: Some(Duration::from_millis(42)),            }),        };        let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false);        assert_eq!(legacy_events.len(), 1);        match &legacy_events[0] {            EventMsg::McpToolCallEnd(event) => {                assert_eq!(event.call_id, "mcp-1");                assert_eq!(event.invocation.server, "server");                assert_eq!(event.invocation.tool, "tool");                assert_eq!(                    event.mcp_app_resource_uri.as_deref(),                    Some("app://connector")                );                assert_eq!(event.plugin_id.as_deref(), Some("sample@test"));                assert_eq!(event.duration, Duration::from_millis(42));                assert!(event.is_success());            }            _ => panic!("expected McpToolCallEnd event"),        }    }    #[test]    fn item_started_event_requires_started_at_ms() {        let mut value = serde_json::to_value(ItemStartedEvent {            thread_id: ThreadId::new(),            turn_id: "turn-1".into(),            item: TurnItem::UserMessage(UserMessageItem::new(&[])),            started_at_ms: 123,        })        .unwrap();        value.as_object_mut().unwrap().remove("started_at_ms");        assert!(serde_json::from_value::<ItemStartedEvent>(value).is_err());    }    #[test]    fn item_completed_event_defaults_missing_completed_at_ms() {        let mut value = serde_json::to_value(ItemCompletedEvent {            thread_id: ThreadId::new(),            turn_id: "turn-1".into(),            item: TurnItem::UserMessage(UserMessageItem::new(&[])),            completed_at_ms: 123,        })        .unwrap();        value.as_object_mut().unwrap().remove("completed_at_ms");        let event = serde_json::from_value::<ItemCompletedEvent>(value).unwrap();        assert_eq!(event.completed_at_ms, 0);    }    #[test]    fn rollback_failed_error_does_not_affect_turn_status() {        let event = ErrorEvent {            message: "rollback failed".into(),            codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),        };        assert!(!event.affects_turn_status());    }    #[test]    fn active_turn_not_steerable_error_does_not_affect_turn_status() {        let event = ErrorEvent {            message: "cannot steer a review turn".into(),            codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable {                turn_kind: NonSteerableTurnKind::Review,            }),        };        assert!(!event.affects_turn_status());    }    #[test]    fn generic_error_affects_turn_status() {        let event = ErrorEvent {            message: "generic".into(),            codex_error_info: Some(CodexErrorInfo::Other),        };        assert!(event.affects_turn_status());    }    #[test]    fn realtime_conversation_started_event_uses_realtime_session_id() {        let event = RealtimeConversationStartedEvent {            realtime_session_id: Some("conv_1".to_string()),            version: RealtimeConversationVersion::V2,        };        assert_eq!(            serde_json::to_value(&event).unwrap(),            json!({                "realtime_session_id": "conv_1",                "version": "v2"            })        );    }    #[test]    fn realtime_voice_list_is_stable() {        assert_eq!(            RealtimeVoicesList::builtin(),            RealtimeVoicesList {                v1: vec![                    RealtimeVoice::Juniper,                    RealtimeVoice::Maple,                    RealtimeVoice::Spruce,                    RealtimeVoice::Ember,                    RealtimeVoice::Vale,                    RealtimeVoice::Breeze,                    RealtimeVoice::Arbor,                    RealtimeVoice::Sol,                    RealtimeVoice::Cove,                ],                v2: vec![                    RealtimeVoice::Alloy,                    RealtimeVoice::Ash,                    RealtimeVoice::Ballad,                    RealtimeVoice::Coral,                    RealtimeVoice::Echo,                    RealtimeVoice::Sage,                    RealtimeVoice::Shimmer,                    RealtimeVoice::Verse,                    RealtimeVoice::Marin,                    RealtimeVoice::Cedar,                ],                default_v1: RealtimeVoice::Cove,                default_v2: RealtimeVoice::Marin,            }        );    }    #[test]    fn user_input_text_serializes_empty_text_elements() -> Result<()> {        let input = UserInput::Text {            text: "hello".to_string(),            text_elements: Vec::new(),        };        let json_input = serde_json::to_value(input)?;        assert_eq!(            json_input,            json!({                "type": "text",                "text": "hello",                "text_elements": [],            })        );        Ok(())    }    #[test]    fn user_message_event_serializes_empty_metadata_vectors() -> Result<()> {        let event = UserMessageEvent {            client_id: None,            message: "hello".to_string(),            images: None,            local_images: Vec::new(),            text_elements: Vec::new(),            ..Default::default()        };        let json_event = serde_json::to_value(event)?;        assert_eq!(            json_event,            json!({                "message": "hello",                "local_images": [],                "text_elements": [],            })        );        Ok(())    }    #[test]    fn user_message_event_deserializes_without_image_detail_fields() -> Result<()> {        let event: UserMessageEvent = serde_json::from_value(json!({            "message": "hello",            "images": ["https://example.com/image.png"],            "local_images": ["/tmp/local.png"],            "text_elements": [],        }))?;        assert_eq!(event.message, "hello");        assert_eq!(            event.images,            Some(vec!["https://example.com/image.png".to_string()])        );        assert_eq!(event.image_details, Vec::<Option<ImageDetail>>::new());        assert_eq!(event.local_images, vec![PathBuf::from("/tmp/local.png")]);        assert_eq!(event.local_image_details, Vec::<Option<ImageDetail>>::new());        assert_eq!(event.text_elements, Vec::new());        Ok(())    }    #[test]    fn user_message_item_legacy_event_preserves_image_details() {        let local_path = PathBuf::from("/tmp/local.png");        let mut item = UserMessageItem::new(&[            crate::user_input::UserInput::Image {                image_url: "https://example.com/first.png".to_string(),                detail: Some(ImageDetail::Original),            },            crate::user_input::UserInput::Image {                image_url: "https://example.com/second.png".to_string(),                detail: None,            },            crate::user_input::UserInput::LocalImage {                path: local_path.clone(),                detail: Some(ImageDetail::Original),            },        ]);        item.client_id = Some("client-message-1".to_string());        let EventMsg::UserMessage(event) = item.as_legacy_event() else {            panic!("expected user message event");        };        assert_eq!(            event.images,            Some(vec![                "https://example.com/first.png".to_string(),                "https://example.com/second.png".to_string(),            ])        );        assert_eq!(event.client_id, Some("client-message-1".to_string()));        assert_eq!(event.image_details, vec![Some(ImageDetail::Original)]);        assert_eq!(event.local_images, vec![local_path]);        assert_eq!(event.local_image_details, vec![Some(ImageDetail::Original)]);    }    #[test]    fn turn_aborted_event_deserializes_without_turn_id() -> Result<()> {        let event: EventMsg = serde_json::from_value(json!({            "type": "turn_aborted",            "reason": "interrupted",        }))?;        match event {            EventMsg::TurnAborted(TurnAbortedEvent {                turn_id, reason, ..            }) => {                assert_eq!(turn_id, None);                assert_eq!(reason, TurnAbortReason::Interrupted);            }            _ => panic!("expected turn_aborted event"),        }        Ok(())    }    #[test]    fn turn_context_item_deserializes_without_network() -> Result<()> {        let item: TurnContextItem = serde_json::from_value(json!({            "cwd": test_path_buf("/tmp"),            "approval_policy": "never",            "sandbox_policy": { "type": "danger-full-access" },            "model": "gpt-5",            "summary": "auto",        }))?;        assert_eq!(item.network, None);        assert_eq!(item.file_system_sandbox_policy, None);        assert_eq!(item.comp_hash, None);        Ok(())    }    #[test]    fn multi_agent_version_uses_newest_present_session_meta_value() -> Result<()> {        let thread_id = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;        let older_meta = SessionMetaLine {            meta: SessionMeta {                id: thread_id,                multi_agent_version: Some(MultiAgentVersion::V2),                ..Default::default()            },            git: None,        };        let newer_meta_without_version = SessionMetaLine {            meta: SessionMeta {                id: thread_id,                multi_agent_version: None,                ..Default::default()            },            git: None,        };        assert_eq!(            multi_agent_version_from_items(                &[                    RolloutItem::SessionMeta(older_meta),                    RolloutItem::SessionMeta(newer_meta_without_version),                ],                Some(thread_id),            ),            Some(MultiAgentVersion::V2)        );        Ok(())    }    #[test]    fn turn_context_item_serializes_network_when_present() -> Result<()> {        let item = TurnContextItem {            turn_id: None,            cwd: test_path_buf("/tmp"),            workspace_roots: None,            current_date: None,            timezone: None,            approval_policy: AskForApproval::Never,            sandbox_policy: SandboxPolicy::DangerFullAccess,            permission_profile: None,            network: Some(TurnContextNetworkItem {                allowed_domains: vec!["api.example.com".to_string()],                denied_domains: vec!["blocked.example.com".to_string()],            }),            file_system_sandbox_policy: Some(FileSystemSandboxPolicy::restricted(vec![                FileSystemSandboxEntry {                    path: FileSystemPath::GlobPattern {                        pattern: "/tmp/private/**/*.txt".to_string(),                    },                    access: FileSystemAccessMode::Deny,                },            ])),            model: "gpt-5".to_string(),            comp_hash: None,            personality: None,            collaboration_mode: None,            multi_agent_version: None,            realtime_active: None,            effort: None,            summary: ReasoningSummaryConfig::Auto,        };        let value = serde_json::to_value(item)?;        assert_eq!(            value["network"],            json!({                "allowed_domains": ["api.example.com"],                "denied_domains": ["blocked.example.com"],            })        );        assert_eq!(            value["file_system_sandbox_policy"],            json!({                "kind": "restricted",                "entries": [{                    "path": {                        "type": "glob_pattern",                        "pattern": "/tmp/private/**/*.txt"                    },                    "access": "deny"                }]            })        );        assert_eq!(value["summary"], json!("auto"));        Ok(())    }    /// Serialize Event to verify that its JSON representation has the expected    /// amount of nesting.    #[test]    fn serialize_event() -> Result<()> {        let session_id = SessionId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c7")?;        let thread_id = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;        let rollout_file = NamedTempFile::new()?;        let permission_profile = PermissionProfile::read_only();        let event = Event {            id: "1234".to_string(),            msg: EventMsg::SessionConfigured(SessionConfiguredEvent {                session_id,                thread_id,                forked_from_id: None,                parent_thread_id: None,                thread_source: None,                thread_name: None,                model: "codex-mini-latest".to_string(),                model_provider_id: "openai".to_string(),                service_tier: None,                approval_policy: AskForApproval::Never,                approvals_reviewer: ApprovalsReviewer::User,                permission_profile: permission_profile.clone(),                active_permission_profile: None,                cwd: test_path_buf("/home/user/project").abs(),                reasoning_effort: Some(ReasoningEffortConfig::default()),                initial_messages: None,                network_proxy: None,                rollout_path: Some(rollout_file.path().to_path_buf()),            }),        };        let expected = json!({            "id": "1234",            "msg": {                "type": "session_configured",                "session_id": "67e55044-10b1-426f-9247-bb680e5fe0c7",                "thread_id": "67e55044-10b1-426f-9247-bb680e5fe0c8",                "model": "codex-mini-latest",                "model_provider_id": "openai",                "approval_policy": "never",                "approvals_reviewer": "user",                "permission_profile": permission_profile,                "cwd": test_path_buf("/home/user/project"),                "reasoning_effort": "medium",                "rollout_path": format!("{}", rollout_file.path().display()),            }        });        assert_eq!(expected, serde_json::to_value(&event)?);        Ok(())    }    #[test]    fn deserialize_legacy_session_configured_event_uses_sandbox_policy() -> Result<()> {        let cwd = test_path_buf("/home/user/project");        let value = json!({            "session_id": "67e55044-10b1-426f-9247-bb680e5fe0c8",            "model": "codex-mini-latest",            "model_provider_id": "openai",            "approval_policy": "never",            "approvals_reviewer": "user",            "sandbox_policy": {                "type": "read-only"            },            "cwd": cwd,        });        let event: SessionConfiguredEvent = serde_json::from_value(value)?;        assert_eq!(event.permission_profile, PermissionProfile::read_only());        Ok(())    }    #[test]    fn vec_u8_as_base64_serialization_and_deserialization() -> Result<()> {        let event = ExecCommandOutputDeltaEvent {            call_id: "call21".to_string(),            stream: ExecOutputStream::Stdout,            chunk: vec![1, 2, 3, 4, 5],        };        let serialized = serde_json::to_string(&event)?;        assert_eq!(            r#"{"call_id":"call21","stream":"stdout","chunk":"AQIDBAU="}"#,            serialized,        );        let deserialized: ExecCommandOutputDeltaEvent = serde_json::from_str(&serialized)?;        assert_eq!(deserialized, event);        Ok(())    }    #[test]    fn serialize_mcp_startup_update_event() -> Result<()> {        let event = Event {            id: "init".to_string(),            msg: EventMsg::McpStartupUpdate(McpStartupUpdateEvent {                server: "srv".to_string(),                status: McpStartupStatus::Failed {                    error: "boom".to_string(),                },            }),        };        let value = serde_json::to_value(&event)?;        assert_eq!(value["msg"]["type"], "mcp_startup_update");        assert_eq!(value["msg"]["server"], "srv");        assert_eq!(value["msg"]["status"]["state"], "failed");        assert_eq!(value["msg"]["status"]["error"], "boom");        Ok(())    }    #[test]    fn serialize_mcp_startup_complete_event() -> Result<()> {        let event = Event {            id: "init".to_string(),            msg: EventMsg::McpStartupComplete(McpStartupCompleteEvent {                ready: vec!["a".to_string()],                failed: vec![McpStartupFailure {                    server: "b".to_string(),                    error: "bad".to_string(),                }],                cancelled: vec!["c".to_string()],            }),        };        let value = serde_json::to_value(&event)?;        assert_eq!(value["msg"]["type"], "mcp_startup_complete");        assert_eq!(value["msg"]["ready"][0], "a");        assert_eq!(value["msg"]["failed"][0]["server"], "b");        assert_eq!(value["msg"]["failed"][0]["error"], "bad");        assert_eq!(value["msg"]["cancelled"][0], "c");        Ok(())    }    #[test]    fn token_usage_info_new_or_append_updates_context_window_when_provided() {        let initial = Some(TokenUsageInfo {            total_token_usage: TokenUsage::default(),            last_token_usage: TokenUsage::default(),            model_context_window: Some(258_400),        });        let last = Some(TokenUsage {            input_tokens: 10,            cached_input_tokens: 0,            output_tokens: 0,            reasoning_output_tokens: 0,            total_tokens: 10,        });        let info = TokenUsageInfo::new_or_append(&initial, &last, Some(128_000))            .expect("new_or_append should return info");        assert_eq!(info.model_context_window, Some(128_000));    }    #[test]    fn token_usage_info_new_or_append_preserves_context_window_when_not_provided() {        let initial = Some(TokenUsageInfo {            total_token_usage: TokenUsage::default(),            last_token_usage: TokenUsage::default(),            model_context_window: Some(258_400),        });        let last = Some(TokenUsage {            input_tokens: 10,            cached_input_tokens: 0,            output_tokens: 0,            reasoning_output_tokens: 0,            total_tokens: 10,        });        let info =            TokenUsageInfo::new_or_append(&initial, &last, /*model_context_window*/ None)                .expect("new_or_append should return info");        assert_eq!(info.model_context_window, Some(258_400));    }}