//! 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)); }}