2026-04-23 23:20:16 +01:00
//! 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 ::HashMap ;
use std ::collections ::HashSet ;
use std ::ffi ::OsStr ;
use std ::fmt ;
use std ::ops ::Mul ;
use std ::path ::Path ;
use std ::path ::PathBuf ;
use std ::str ::FromStr ;
use std ::time ::Duration ;
use crate ::AgentPath ;
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 ::ServiceTier ;
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 ::mcp ::Resource as McpResource ;
use crate ::mcp ::ResourceTemplate as McpResourceTemplate ;
use crate ::mcp ::Tool as McpTool ;
use crate ::memory_citation ::MemoryCitation ;
use crate ::message_history ::HistoryEntry ;
use crate ::models ::BaseInstructions ;
use crate ::models ::ContentItem ;
use crate ::models ::MessagePhase ;
use crate ::models ::PermissionProfile ;
use crate ::models ::ResponseInputItem ;
use crate ::models ::ResponseItem ;
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 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 ;
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 COLLABORATION_MODE_OPEN_TAG : & str = " <collaboration_mode> " ;
pub const COLLABORATION_MODE_CLOSE_TAG : & str = " </collaboration_mode> " ;
2026-04-23 23:48:52 +01:00
pub const EPIPHANY_STATE_OPEN_TAG : & str = " <epiphany_state> " ;
pub const EPIPHANY_STATE_CLOSE_TAG : & str = " </epiphany_state> " ;
2026-04-23 23:20:16 +01:00
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: " ;
#[ derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema) ]
pub struct TurnEnvironmentSelection {
pub environment_id : String ,
pub cwd : AbsolutePathBuf ,
}
#[ 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, Deserialize, Serialize, JsonSchema) ]
pub struct Submission {
/// Unique id for this Submission to correlate with Events
pub id : String ,
/// Payload
pub op : Op ,
/// Optional W3C trace carrier propagated across async submission handoffs.
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
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, Deserialize, Serialize, PartialEq, JsonSchema) ]
pub struct McpServerRefreshConfig {
pub mcp_servers : Value ,
pub mcp_oauth_credentials_store_mode : Value ,
}
#[ derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS) ]
pub struct ConversationStartParams {
/// Selects whether the realtime session should produce text or audio output.
pub output_modality : RealtimeOutputModality ,
#[ serde(
default ,
deserialize_with = " conversation_start_prompt_serde::deserialize " ,
serialize_with = " conversation_start_prompt_serde::serialize " ,
skip_serializing_if = " Option::is_none "
) ]
pub prompt : Option < Option < String > > ,
#[ serde(skip_serializing_if = " Option::is_none " ) ]
pub session_id : Option < String > ,
#[ serde(skip_serializing_if = " Option::is_none " ) ]
pub transport : Option < ConversationStartTransport > ,
#[ serde(skip_serializing_if = " Option::is_none " ) ]
pub voice : Option < RealtimeVoice > ,
}
#[ derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS) ]
#[ serde(tag = " type " , rename_all = " snake_case " ) ]
#[ ts(tag = " type " ) ]
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 ,
}
mod conversation_start_prompt_serde {
use serde ::Deserializer ;
use serde ::Serializer ;
pub ( crate ) fn deserialize < ' de , D > ( deserializer : D ) -> Result < Option < Option < String > > , D ::Error >
where
D : Deserializer < ' de > ,
{
serde_with ::rust ::double_option ::deserialize ( deserializer )
}
pub ( crate ) fn serialize < S > (
value : & Option < Option < String > > ,
serializer : S ,
) -> Result < S ::Ok , S ::Error >
where
S : Serializer ,
{
serde_with ::rust ::double_option ::serialize ( value , serializer )
}
}
#[ 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 {
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, Deserialize, Serialize, PartialEq, JsonSchema, TS) ]
pub struct ConversationAudioParams {
pub frame : RealtimeAudioFrame ,
}
#[ derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS) ]
pub struct ConversationTextParams {
pub text : String ,
}
/// Submission operation
#[ derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema) ]
#[ serde(tag = " type " , rename_all = " snake_case " ) ]
#[ 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 ) ,
/// Close the running realtime conversation stream.
RealtimeConversationClose ,
/// Request the list of voices supported by realtime conversation streams.
RealtimeConversationListVoices ,
/// Legacy user input.
///
/// Prefer [`Op::UserTurn`] so the caller provides full turn context
/// (cwd/approval/sandbox/model/etc.) for each turn.
UserInput {
/// User input items, see `InputItem`
items : Vec < UserInput > ,
/// Optional turn-scoped environment selections.
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
environments : Option < Vec < TurnEnvironmentSelection > > ,
/// Optional JSON Schema used to constrain the final assistant message for this turn.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
final_output_json_schema : Option < Value > ,
/// Optional turn-scoped Responses API `client_metadata`.
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
responsesapi_client_metadata : Option < HashMap < String , String > > ,
} ,
/// Similar to [`Op::UserInput`], but first applies persistent turn-context
/// overrides in the same queued operation. This preserves submission order
/// and prevents the input from starting if the overrides are rejected.
UserInputWithTurnContext {
/// User input items, see `InputItem`
items : Vec < UserInput > ,
/// Optional turn-scoped environment selections.
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
environments : Option < Vec < TurnEnvironmentSelection > > ,
/// Optional JSON Schema used to constrain the final assistant message for this turn.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
final_output_json_schema : Option < Value > ,
/// Optional turn-scoped Responses API `client_metadata`.
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
responsesapi_client_metadata : Option < HashMap < String , String > > ,
/// Updated `cwd` for sandbox/tool calls.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
cwd : Option < PathBuf > ,
/// Updated command approval policy.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
approval_policy : Option < AskForApproval > ,
/// Updated approval reviewer for future approval prompts.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
approvals_reviewer : Option < ApprovalsReviewer > ,
/// Updated sandbox policy for tool calls.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
sandbox_policy : Option < SandboxPolicy > ,
/// Updated permissions profile for tool calls.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
permission_profile : Option < PermissionProfile > ,
/// Updated Windows sandbox mode for tool execution.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
windows_sandbox_level : Option < WindowsSandboxLevel > ,
/// Updated model slug. When set, the model info is derived
/// automatically.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
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.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
effort : Option < Option < ReasoningEffortConfig > > ,
/// Updated reasoning summary preference (honored only for reasoning-capable models).
#[ serde(skip_serializing_if = " Option::is_none " ) ]
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.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
service_tier : Option < Option < ServiceTier > > ,
/// EXPERIMENTAL - set a pre-set collaboration mode.
/// Takes precedence over model, effort, and developer instructions if set.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
collaboration_mode : Option < CollaborationMode > ,
/// Updated personality preference.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
personality : Option < Personality > ,
} ,
/// Similar to [`Op::UserInput`], but contains additional context required
/// for a turn of a [`crate::codex_thread::CodexThread`].
UserTurn {
/// User input items, see `InputItem`
items : Vec < UserInput > ,
/// `cwd` to use with the [`SandboxPolicy`] and potentially tool calls
/// such as `local_shell`.
cwd : PathBuf ,
/// Policy to use for command approval.
approval_policy : AskForApproval ,
/// Reviewer to use for approval requests raised during this turn.
///
/// When omitted, the session keeps the current setting
approvals_reviewer : Option < ApprovalsReviewer > ,
/// Policy to use for tool calls such as `local_shell`.
sandbox_policy : SandboxPolicy ,
/// Must be a valid model slug for the configured client session
/// associated with this conversation.
model : String ,
/// Will only be honored if the model is configured to use reasoning.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
effort : Option < ReasoningEffortConfig > ,
/// Will only be honored if the model is configured to use reasoning.
///
/// When omitted, the session keeps the current setting (which allows core to
/// fall back to the selected model's default on new sessions).
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
summary : Option < ReasoningSummaryConfig > ,
/// Optional service tier override for this turn.
///
/// Use `Some(Some(_))` to set a specific tier for this turn, `Some(None)` to
/// explicitly clear the tier for this turn, or `None` to keep the existing
/// session preference.
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
service_tier : Option < Option < ServiceTier > > ,
// The JSON schema to use for the final assistant message
final_output_json_schema : Option < Value > ,
/// EXPERIMENTAL - set a pre-set collaboration mode.
/// Takes precedence over model, effort, and developer instructions if set.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
collaboration_mode : Option < CollaborationMode > ,
/// Optional personality override for this turn.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
personality : Option < Personality > ,
/// Optional turn-scoped environment selections.
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
environments : Option < Vec < TurnEnvironmentSelection > > ,
} ,
/// Inter-agent communication that should be recorded as assistant history
/// while still using the normal thread submission lifecycle.
InterAgentCommunication {
communication : InterAgentCommunication ,
} ,
/// Override parts of the persistent turn context for subsequent turns.
///
/// All fields are optional; when omitted, the existing value is preserved.
/// This does not enqueue any input – it only updates defaults used for
/// turns that rely on persistent session-level context (for example,
/// [`Op::UserInput`]).
OverrideTurnContext {
/// Updated `cwd` for sandbox/tool calls.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
cwd : Option < PathBuf > ,
/// Updated command approval policy.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
approval_policy : Option < AskForApproval > ,
/// Updated approval reviewer for future approval prompts.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
approvals_reviewer : Option < ApprovalsReviewer > ,
/// Updated sandbox policy for tool calls.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
sandbox_policy : Option < SandboxPolicy > ,
/// Updated permissions profile for tool calls.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
permission_profile : Option < PermissionProfile > ,
/// Updated Windows sandbox mode for tool execution.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
windows_sandbox_level : Option < WindowsSandboxLevel > ,
/// Updated model slug. When set, the model info is derived
/// automatically.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
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.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
effort : Option < Option < ReasoningEffortConfig > > ,
/// Updated reasoning summary preference (honored only for reasoning-capable models).
#[ serde(skip_serializing_if = " Option::is_none " ) ]
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.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
service_tier : Option < Option < ServiceTier > > ,
/// EXPERIMENTAL - set a pre-set collaboration mode.
/// Takes precedence over model, effort, and developer instructions if set.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
collaboration_mode : Option < CollaborationMode > ,
/// Updated personality preference.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
personality : Option < Personality > ,
} ,
/// Approve a command execution
ExecApproval {
/// The id of the submission we are approving
id : String ,
/// Turn id associated with the approval event, when available.
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
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.
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
content : Option < Value > ,
/// Optional client metadata associated with the elicitation response.
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
meta : Option < Value > ,
} ,
/// Resolve a request_user_input tool call.
#[ serde(rename = " user_input_answer " , alias = " request_user_input_response " ) ]
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 ,
} ,
/// Append an entry to the persistent cross-session message history.
///
/// Note the entry is not guaranteed to be logged if the user has
/// history disabled, it matches the list of "sensitive" patterns, etc.
AddToHistory {
/// The message text to be stored.
text : String ,
} ,
/// Request a single history entry identified by `log_id` + `offset`.
GetHistoryEntryRequest { offset : usize , log_id : u64 } ,
/// Request the list of MCP tools available across all configured servers.
/// Reply is delivered via `EventMsg::McpListToolsResponse`.
ListMcpTools ,
/// 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 ,
/// Drop all persisted memory artifacts and memory-tracking DB rows.
DropMemories ,
/// Trigger a single pass of the startup memory pipeline.
UpdateMemories ,
/// Set a user-facing thread name in the persisted rollout metadata.
/// This is a local-only operation handled by codex-core; it does not
/// involve the model.
SetThreadName { name : String } ,
/// 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 undo a turn (turn are stacked so it is the same effect as CMD + Z).
Undo ,
/// 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 ,
} ,
/// Request the list of available models.
ListModels ,
}
#[ 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 {
environments : None ,
items : value ,
final_output_json_schema : None ,
responsesapi_client_metadata : None ,
}
}
}
#[ 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 ,
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 ,
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 ( ) ,
} ] ,
}
}
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 ::RealtimeConversationClose = > " realtime_conversation_close " ,
Self ::RealtimeConversationListVoices = > " realtime_conversation_list_voices " ,
Self ::UserInput { .. } = > " user_input " ,
Self ::UserInputWithTurnContext { .. } = > " user_input_with_turn_context " ,
Self ::UserTurn { .. } = > " user_turn " ,
Self ::InterAgentCommunication { .. } = > " inter_agent_communication " ,
Self ::OverrideTurnContext { .. } = > " override_turn_context " ,
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 ::AddToHistory { .. } = > " add_to_history " ,
Self ::GetHistoryEntryRequest { .. } = > " get_history_entry_request " ,
Self ::ListMcpTools = > " list_mcp_tools " ,
Self ::RefreshMcpServers { .. } = > " refresh_mcp_servers " ,
Self ::ReloadUserConfig = > " reload_user_config " ,
Self ::Compact = > " compact " ,
Self ::DropMemories = > " drop_memories " ,
Self ::UpdateMemories = > " update_memories " ,
Self ::SetThreadName { .. } = > " set_thread_name " ,
Self ::SetThreadMemoryMode { .. } = > " set_thread_memory_mode " ,
Self ::Undo = > " undo " ,
Self ::ThreadRollback { .. } = > " thread_rollback " ,
Self ::Review { .. } = > " review " ,
Self ::ApproveGuardianDeniedAction { .. } = > " approve_guardian_denied_action " ,
Self ::Shutdown = > " shutdown " ,
Self ::RunUserShellCommand { .. } = > " run_user_shell_command " ,
Self ::ListModels = > " list_models " ,
}
}
}
/// 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 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_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 )
}
}
fn default_include_platform_defaults ( ) -> bool {
true
}
/// Determines how read-only file access is granted inside a restricted
/// sandbox.
#[ derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, Default, JsonSchema, TS) ]
#[ strum(serialize_all = " kebab-case " ) ]
#[ serde(tag = " type " , rename_all = " kebab-case " ) ]
#[ ts(tag = " type " ) ]
pub enum ReadOnlyAccess {
/// Restrict reads to an explicit set of roots.
///
/// When `include_platform_defaults` is `true`, platform defaults required
/// for basic execution are included in addition to `readable_roots`.
Restricted {
/// Include built-in platform read roots required for basic process
/// execution.
#[ serde(default = " default_include_platform_defaults " ) ]
include_platform_defaults : bool ,
/// Additional absolute roots that should be readable.
#[ serde(default, skip_serializing_if = " Vec::is_empty " ) ]
readable_roots : Vec < AbsolutePathBuf > ,
} ,
/// Allow unrestricted file reads.
#[ default ]
FullAccess ,
}
impl ReadOnlyAccess {
pub fn has_full_disk_read_access ( & self ) -> bool {
matches! ( self , ReadOnlyAccess ::FullAccess )
}
/// Returns true if platform defaults should be included for restricted read access.
pub fn include_platform_defaults ( & self ) -> bool {
matches! (
self ,
ReadOnlyAccess ::Restricted {
include_platform_defaults : true ,
..
}
)
}
/// Returns the readable roots for restricted read access.
///
/// For [`ReadOnlyAccess::FullAccess`], returns an empty list because
/// callers should grant blanket read access instead.
pub fn get_readable_roots_with_cwd ( & self , cwd : & Path ) -> Vec < AbsolutePathBuf > {
let mut roots : Vec < AbsolutePathBuf > = match self {
ReadOnlyAccess ::FullAccess = > return Vec ::new ( ) ,
ReadOnlyAccess ::Restricted { readable_roots , .. } = > {
let mut roots = readable_roots . clone ( ) ;
match AbsolutePathBuf ::from_absolute_path ( cwd ) {
Ok ( cwd_root ) = > roots . push ( cwd_root ) ,
Err ( err ) = > {
error! ( " Ignoring invalid cwd {cwd:?} for sandbox readable root: {err} " ) ;
}
}
roots
}
} ;
let mut seen = HashSet ::new ( ) ;
roots . retain ( | root | seen . insert ( root . to_path_buf ( ) ) ) ;
roots
}
}
/// 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 {
/// Read access granted while running under this policy.
#[ serde(
default ,
skip_serializing_if = " ReadOnlyAccess::has_full_disk_read_access "
) ]
access : ReadOnlyAccess ,
/// 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 > ,
/// Read access granted while running under this policy.
#[ serde(
default ,
skip_serializing_if = " ReadOnlyAccess::has_full_disk_read_access "
) ]
read_only_access : ReadOnlyAccess ,
/// 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 > ,
}
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 ;
}
}
true
}
}
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 {
access : ReadOnlyAccess ::FullAccess ,
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 ! [ ] ,
read_only_access : ReadOnlyAccess ::FullAccess ,
network_access : false ,
exclude_tmpdir_env_var : false ,
exclude_slash_tmp : false ,
}
}
pub fn has_full_disk_read_access ( & self ) -> bool {
match self {
SandboxPolicy ::DangerFullAccess = > true ,
SandboxPolicy ::ExternalSandbox { .. } = > true ,
SandboxPolicy ::ReadOnly { access , .. } = > access . has_full_disk_read_access ( ) ,
SandboxPolicy ::WorkspaceWrite {
read_only_access , ..
} = > read_only_access . has_full_disk_read_access ( ) ,
}
}
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 true if platform defaults should be included for restricted read access.
pub fn include_platform_defaults ( & self ) -> bool {
if self . has_full_disk_read_access ( ) {
return false ;
}
match self {
SandboxPolicy ::ReadOnly { access , .. } = > access . include_platform_defaults ( ) ,
SandboxPolicy ::WorkspaceWrite {
read_only_access , ..
} = > read_only_access . include_platform_defaults ( ) ,
SandboxPolicy ::DangerFullAccess | SandboxPolicy ::ExternalSandbox { .. } = > false ,
}
}
/// Returns the list of readable roots (tailored to the current working
/// directory) when read access is restricted.
///
/// For policies with full read access, this returns an empty list because
/// callers should grant blanket reads.
pub fn get_readable_roots_with_cwd ( & self , cwd : & Path ) -> Vec < AbsolutePathBuf > {
let mut roots = match self {
SandboxPolicy ::DangerFullAccess | SandboxPolicy ::ExternalSandbox { .. } = > Vec ::new ( ) ,
SandboxPolicy ::ReadOnly { access , .. } = > access . get_readable_roots_with_cwd ( cwd ) ,
SandboxPolicy ::WorkspaceWrite {
read_only_access , ..
} = > {
let mut roots = read_only_access . get_readable_roots_with_cwd ( cwd ) ;
roots . extend (
self . get_writable_roots_with_cwd ( cwd )
. into_iter ( )
. map ( | root | root . root ) ,
) ;
roots
}
} ;
let mut seen = HashSet ::new ( ) ;
roots . retain ( | root | seen . insert ( root . to_path_buf ( ) ) ) ;
roots
}
/// 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 ,
read_only_access : _ ,
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 ,
) ,
root : writable_root ,
}
} )
. collect ( )
}
}
}
}
fn default_read_only_subpaths_for_writable_root (
writable_root : & AbsolutePathBuf ,
protect_missing_dot_codex : bool ,
) -> Vec < AbsolutePathBuf > {
let mut subpaths : Vec < AbsolutePathBuf > = Vec ::new ( ) ;
let top_level_git = writable_root . join ( " .git " ) ;
// This applies to typical repos (directory .git), worktrees/submodules
// (file .git with gitdir pointer), and bare repos when the gitdir is the
// writable root itself.
let top_level_git_is_file = top_level_git . as_path ( ) . is_file ( ) ;
let top_level_git_is_dir = top_level_git . as_path ( ) . is_dir ( ) ;
if top_level_git_is_dir | | top_level_git_is_file {
if top_level_git_is_file
& & is_git_pointer_file ( & top_level_git )
& & let Some ( gitdir ) = resolve_gitdir_from_file ( & top_level_git )
{
subpaths . push ( gitdir ) ;
}
subpaths . push ( top_level_git ) ;
}
let top_level_agents = writable_root . join ( " .agents " ) ;
if top_level_agents . as_path ( ) . is_dir ( ) {
subpaths . push ( top_level_agents ) ;
}
// Keep top-level project metadata under .codex read-only to the agent by
// default. For the workspace root itself, protect it even before the
// directory exists so first-time creation still goes through the
// protected-path approval flow.
let top_level_codex = writable_root . join ( " .codex " ) ;
if protect_missing_dot_codex | | top_level_codex . as_path ( ) . is_dir ( ) {
subpaths . push ( top_level_codex ) ;
}
let mut deduped = Vec ::with_capacity ( subpaths . len ( ) ) ;
let mut seen = HashSet ::new ( ) ;
for path in subpaths {
if seen . insert ( path . to_path_buf ( ) ) {
deduped . push ( path ) ;
}
}
deduped
}
fn is_git_pointer_file ( path : & AbsolutePathBuf ) -> bool {
path . as_path ( ) . is_file ( ) & & path . as_path ( ) . file_name ( ) = = Some ( OsStr ::new ( " .git " ) )
}
fn resolve_gitdir_from_file ( dot_git : & AbsolutePathBuf ) -> Option < AbsolutePathBuf > {
let contents = match std ::fs ::read_to_string ( dot_git . as_path ( ) ) {
Ok ( contents ) = > contents ,
Err ( err ) = > {
error! (
" Failed to read {path} for gitdir pointer: {err} " ,
path = dot_git . as_path ( ) . display ( )
) ;
return None ;
}
} ;
let trimmed = contents . trim ( ) ;
let ( _ , gitdir_raw ) = match trimmed . split_once ( ':' ) {
Some ( parts ) = > parts ,
None = > {
error! (
" Expected {path} to contain a gitdir pointer, but it did not match `gitdir: <path>`. " ,
path = dot_git . as_path ( ) . display ( )
) ;
return None ;
}
} ;
let gitdir_raw = gitdir_raw . trim ( ) ;
if gitdir_raw . is_empty ( ) {
error! (
" Expected {path} to contain a gitdir pointer, but it was empty. " ,
path = dot_git . as_path ( ) . display ( )
) ;
return None ;
}
let base = match dot_git . as_path ( ) . parent ( ) {
Some ( base ) = > base ,
None = > {
error! (
" Unable to resolve parent directory for {path}. " ,
path = dot_git . as_path ( ) . display ( )
) ;
return None ;
}
} ;
let gitdir_path = AbsolutePathBuf ::resolve_path_against_base ( gitdir_raw , base ) ;
if ! gitdir_path . as_path ( ) . exists ( ) {
error! (
" Resolved gitdir path {path} does not exist. " ,
path = gitdir_path . as_path ( ) . display ( )
) ;
return None ;
}
Some ( gitdir_path )
}
/// 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 ) ,
/// 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 ) ,
/// 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 ) ,
/// Agent text output delta message
AgentMessageDelta ( AgentMessageDeltaEvent ) ,
/// Reasoning event from agent.
AgentReasoning ( AgentReasoningEvent ) ,
/// Agent reasoning delta event from agent.
AgentReasoningDelta ( AgentReasoningDeltaEvent ) ,
/// Raw chain-of-thought from agent.
AgentReasoningRawContent ( AgentReasoningRawContentEvent ) ,
/// Agent reasoning content delta event from agent.
AgentReasoningRawContentDelta ( AgentReasoningRawContentDeltaEvent ) ,
/// 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 session metadata (e.g., thread name changes).
ThreadNameUpdated ( ThreadNameUpdatedEvent ) ,
/// 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 ) ,
BackgroundEvent ( BackgroundEventEvent ) ,
UndoStarted ( UndoStartedEvent ) ,
UndoCompleted ( UndoCompletedEvent ) ,
/// 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 ) ,
/// Response to GetHistoryEntryRequest.
GetHistoryEntryResponse ( GetHistoryEntryResponseEvent ) ,
/// List of MCP tools available to the agent.
McpListToolsResponse ( McpListToolsResponseEvent ) ,
/// 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 ) ,
}
#[ derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS) ]
#[ serde(rename_all = " snake_case " ) ]
pub enum HookEventName {
PreToolUse ,
PermissionRequest ,
PostToolUse ,
SessionStart ,
UserPromptSubmit ,
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 ,
LegacyManagedConfigFile ,
LegacyManagedConfigMdm ,
#[ default ]
Unknown ,
}
#[ 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, Deserialize, Serialize, PartialEq, JsonSchema, TS) ]
pub struct RealtimeConversationStartedEvent {
pub 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 )
}
}
/// 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 ,
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 ::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 ,
}
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 ::ImageGeneration ( item ) = > {
vec! [ EventMsg ::ImageGenerationBegin ( ImageGenerationBeginEvent {
call_id : item . id . clone ( ) ,
} ) ]
}
_ = > Vec ::new ( ) ,
}
}
}
#[ derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema) ]
pub struct ItemCompletedEvent {
pub thread_id : ThreadId ,
pub turn_id : String ,
pub item : TurnItem ,
}
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 > {
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! [ EventMsg ::AgentMessageDelta ( AgentMessageDeltaEvent {
delta : self . delta . clone ( ) ,
} ) ]
}
}
#[ 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! [ EventMsg ::AgentReasoningDelta ( AgentReasoningDeltaEvent {
delta : self . delta . clone ( ) ,
} ) ]
}
}
#[ 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! [ EventMsg ::AgentReasoningRawContentDelta (
AgentReasoningRawContentDeltaEvent {
delta : self . delta . clone ( ) ,
} ,
) ]
}
}
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, 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 ,
/// 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, 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 > ,
2026-04-26 04:24:55 +01:00
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
#[ ts(type = " number | null " , optional) ]
pub model_auto_compact_token_limit : Option < i64 > ,
2026-04-23 23:20:16 +01:00
}
impl TokenUsageInfo {
pub fn new_or_append (
info : & Option < TokenUsageInfo > ,
last : & Option < TokenUsage > ,
model_context_window : Option < i64 > ,
2026-04-26 04:24:55 +01:00
model_auto_compact_token_limit : Option < i64 > ,
2026-04-23 23:20:16 +01:00
) -> 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 ,
2026-04-26 04:24:55 +01:00
model_auto_compact_token_limit ,
2026-04-23 23:20:16 +01:00
} ,
} ;
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 ) ;
}
2026-04-26 04:24:55 +01:00
if let Some ( model_auto_compact_token_limit ) = model_auto_compact_token_limit {
info . model_auto_compact_token_limit = Some ( model_auto_compact_token_limit ) ;
}
2026-04-23 23:20:16 +01:00
Some ( info )
}
pub fn append_last_usage ( & mut self , last : & TokenUsage ) {
self . total_token_usage . add_assign ( last ) ;
self . last_token_usage = last . clone ( ) ;
}
2026-04-26 04:24:55 +01:00
pub fn fill_to_context_window (
& mut self ,
context_window : i64 ,
model_auto_compact_token_limit : Option < i64 > ,
) {
2026-04-23 23:20:16 +01:00
let previous_total = self . total_token_usage . total_tokens ;
let delta = ( context_window - previous_total ) . max ( 0 ) ;
self . model_context_window = Some ( context_window ) ;
2026-04-26 04:24:55 +01:00
if let Some ( model_auto_compact_token_limit ) = model_auto_compact_token_limit {
self . model_auto_compact_token_limit = Some ( model_auto_compact_token_limit ) ;
}
2026-04-23 23:20:16 +01:00
self . total_token_usage = TokenUsage {
total_tokens : context_window ,
.. TokenUsage ::default ( )
} ;
self . last_token_usage = TokenUsage {
total_tokens : delta ,
.. TokenUsage ::default ( )
} ;
}
2026-04-26 04:24:55 +01:00
pub fn full_context_window (
context_window : i64 ,
model_auto_compact_token_limit : Option < i64 > ,
) -> Self {
2026-04-23 23:20:16 +01:00
let mut info = Self {
total_token_usage : TokenUsage ::default ( ) ,
last_token_usage : TokenUsage ::default ( ) ,
model_context_window : Some ( context_window ) ,
2026-04-26 04:24:55 +01:00
model_auto_compact_token_limit ,
2026-04-23 23:20:16 +01:00
} ;
2026-04-26 04:24:55 +01:00
info . fill_to_context_window ( context_window , model_auto_compact_token_limit ) ;
2026-04-23 23:20:16 +01:00
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 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 ,
}
#[ 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 > ,
}
// 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, Deserialize, Serialize, JsonSchema, TS) ]
pub struct UserMessageEvent {
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 > > ,
/// Local file paths sourced from `UserInput::LocalImage`. These are kept so
/// the UI can reattach images when editing history, and should not be sent
/// to the model or treated as API-ready URLs.
#[ serde(default) ]
pub local_images : Vec < std ::path ::PathBuf > ,
/// 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 AgentMessageDeltaEvent {
pub delta : String ,
}
#[ 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 AgentReasoningRawContentDeltaEvent {
pub delta : 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) ]
pub struct AgentReasoningDeltaEvent {
pub delta : String ,
}
#[ 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 > ,
}
#[ 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 > ,
#[ 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 ,
/// 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 : 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 ,
} ) ,
}
}
}
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 ) ,
SubAgent ( SubAgentSource ) ,
#[ serde(other) ]
Unknown ,
}
#[ 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 ::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 ) ,
} )
}
/// Low cardinality thread source label for analytics.
pub fn thread_source_name ( & self ) -> Option < & 'static str > {
match self {
SessionSource ::Cli | SessionSource ::VSCode | SessionSource ::Exec = > Some ( " user " ) ,
SessionSource ::SubAgent ( _ ) = > Some ( " subagent " ) ,
SessionSource ::Mcp | SessionSource ::Custom ( _ ) | SessionSource ::Unknown = > None ,
}
}
pub fn get_nickname ( & self ) -> Option < String > {
match self {
SessionSource ::SubAgent ( SubAgentSource ::ThreadSpawn { agent_nickname , .. } ) = > {
agent_nickname . clone ( )
}
SessionSource ::SubAgent ( SubAgentSource ::MemoryConsolidation ) = > {
Some ( " Morpheus " . to_string ( ) )
}
_ = > None ,
}
}
pub fn get_agent_role ( & self ) -> Option < String > {
match self {
SessionSource ::SubAgent ( SubAgentSource ::ThreadSpawn { agent_role , .. } ) = > {
agent_role . clone ( )
}
SessionSource ::SubAgent ( SubAgentSource ::MemoryConsolidation ) = > {
Some ( " memory builder " . to_string ( ) )
}
_ = > None ,
}
}
pub fn get_agent_path ( & self ) -> Option < AgentPath > {
match self {
SessionSource ::SubAgent ( SubAgentSource ::ThreadSpawn { agent_path , .. } ) = > {
agent_path . clone ( )
}
SessionSource ::SubAgent ( SubAgentSource ::MemoryConsolidation ) = > {
Some ( AgentPath ::morpheus ( ) )
}
_ = > 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 ::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 ) )
}
}
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 ) ,
}
}
}
/// 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 > ,
pub timestamp : String ,
pub cwd : PathBuf ,
pub originator : String ,
pub cli_version : String ,
#[ serde(default) ]
pub source : SessionSource ,
/// 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(skip_serializing_if = " Option::is_none " ) ]
pub dynamic_tools : Option < Vec < DynamicToolSpec > > ,
#[ serde(skip_serializing_if = " Option::is_none " ) ]
pub memory_mode : Option < String > ,
}
impl Default for SessionMeta {
fn default ( ) -> Self {
SessionMeta {
id : ThreadId ::default ( ) ,
forked_from_id : None ,
timestamp : String ::new ( ) ,
cwd : PathBuf ::new ( ) ,
originator : String ::new ( ) ,
cli_version : String ::new ( ) ,
source : SessionSource ::default ( ) ,
agent_nickname : None ,
agent_role : None ,
agent_path : None ,
model_provider : None ,
base_instructions : None ,
dynamic_tools : None ,
memory_mode : 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 ) ,
Compacted ( CompactedItem ) ,
TurnContext ( TurnContextItem ) ,
EpiphanyState ( EpiphanyStateItem ) ,
EventMsg ( EventMsg ) ,
}
2026-05-13 11:04:39 +01:00
pub use epiphany_state_model ::EpiphanyAcceptanceReceipt ;
pub use epiphany_state_model ::EpiphanyBacklogItem ;
pub use epiphany_state_model ::EpiphanyChurnState ;
pub use epiphany_state_model ::EpiphanyCodeRef ;
pub use epiphany_state_model ::EpiphanyEvidenceRecord ;
pub use epiphany_state_model ::EpiphanyGraph ;
pub use epiphany_state_model ::EpiphanyGraphCheckpoint ;
pub use epiphany_state_model ::EpiphanyGraphEdge ;
pub use epiphany_state_model ::EpiphanyGraphFrontier ;
pub use epiphany_state_model ::EpiphanyGraphLink ;
pub use epiphany_state_model ::EpiphanyGraphNode ;
pub use epiphany_state_model ::EpiphanyGraphs ;
pub use epiphany_state_model ::EpiphanyInvariant ;
pub use epiphany_state_model ::EpiphanyInvestigationCheckpoint ;
pub use epiphany_state_model ::EpiphanyInvestigationDisposition ;
pub use epiphany_state_model ::EpiphanyJobBinding ;
pub use epiphany_state_model ::EpiphanyJobKind ;
pub use epiphany_state_model ::EpiphanyModeKind ;
pub use epiphany_state_model ::EpiphanyModeState ;
pub use epiphany_state_model ::EpiphanyObjectiveDraft ;
pub use epiphany_state_model ::EpiphanyObjectiveDraftLanePlan ;
pub use epiphany_state_model ::EpiphanyObjectiveDraftScope ;
pub use epiphany_state_model ::EpiphanyObservation ;
pub use epiphany_state_model ::EpiphanyPlanningCapture ;
pub use epiphany_state_model ::EpiphanyPlanningPriority ;
pub use epiphany_state_model ::EpiphanyPlanningSourceRef ;
pub use epiphany_state_model ::EpiphanyPlanningState ;
pub use epiphany_state_model ::EpiphanyRetrievalShardSummary ;
pub use epiphany_state_model ::EpiphanyRetrievalState ;
pub use epiphany_state_model ::EpiphanyRetrievalStatus ;
pub use epiphany_state_model ::EpiphanyRoadmapStream ;
pub use epiphany_state_model ::EpiphanyRuntimeLink ;
pub use epiphany_state_model ::EpiphanyScratchPad ;
pub use epiphany_state_model ::EpiphanyStateItem ;
pub use epiphany_state_model ::EpiphanySubgoal ;
pub use epiphany_state_model ::EpiphanyThreadState ;
2026-05-19 09:40:48 +01:00
pub use epiphany_state_model ::render_epiphany_state ;
2026-05-01 07:34:09 +01:00
2026-04-23 23:20:16 +01:00
#[ 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 > > ,
}
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 ,
} ] ,
end_turn : None ,
phase : 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 > ,
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
pub trace_id : Option < String > ,
pub cwd : PathBuf ,
#[ 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(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 realtime_active : Option < bool > ,
#[ serde(skip_serializing_if = " Option::is_none " ) ]
pub effort : Option < ReasoningEffortConfig > ,
pub summary : ReasoningSummaryConfig ,
#[ serde(skip_serializing_if = " Option::is_none " ) ]
pub user_instructions : Option < String > ,
#[ serde(skip_serializing_if = " Option::is_none " ) ]
pub developer_instructions : Option < String > ,
#[ serde(skip_serializing_if = " Option::is_none " ) ]
pub final_output_json_schema : Option < Value > ,
#[ serde(skip_serializing_if = " Option::is_none " ) ]
pub truncation_policy : Option < TruncationPolicy > ,
}
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 (
& self . sandbox_policy ,
& self . cwd ,
)
} ) ;
PermissionProfile ::from_runtime_permissions (
& 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 ,
/// 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 ,
/// 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 BackgroundEventEvent {
pub message : 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 UndoStartedEvent {
#[ serde(skip_serializing_if = " Option::is_none " ) ]
pub message : Option < String > ,
}
#[ derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS) ]
pub struct UndoCompletedEvent {
pub success : bool ,
#[ serde(skip_serializing_if = " Option::is_none " ) ]
pub message : 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 GetHistoryEntryResponseEvent {
pub offset : usize ,
pub log_id : u64 ,
/// The entry at the requested offset, if available and parseable.
#[ serde(skip_serializing_if = " Option::is_none " ) ]
pub entry : Option < HistoryEntry > ,
}
#[ derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS) ]
pub struct McpListToolsResponseEvent {
/// Fully qualified tool name -> tool definition.
pub tools : std ::collections ::HashMap < String , McpTool > ,
/// Known resources grouped by server name.
pub resources : std ::collections ::HashMap < String , Vec < McpResource > > ,
/// Known resource templates grouped by server name.
pub resource_templates : std ::collections ::HashMap < String , Vec < McpResourceTemplate > > ,
/// Authentication status for each configured MCP server.
pub auth_statuses : std ::collections ::HashMap < String , McpAuthStatus > ,
}
#[ 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, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq) ]
pub struct SessionNetworkProxyRuntime {
pub http_addr : String ,
pub socks_addr : String ,
}
#[ derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS) ]
pub struct SessionConfiguredEvent {
pub session_id : ThreadId ,
#[ serde(skip_serializing_if = " Option::is_none " ) ]
pub forked_from_id : Option < ThreadId > ,
/// 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 < ServiceTier > ,
/// 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 ,
/// How to sandbox commands executed in the system
pub sandbox_policy : SandboxPolicy ,
/// 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 > ,
/// Identifier of the history log file (inode on Unix, 0 otherwise).
pub history_log_id : u64 ,
/// Current number of entries in the history log.
pub history_entry_count : usize ,
/// 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 > ,
}
#[ derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS) ]
pub struct ThreadNameUpdatedEvent {
pub thread_id : ThreadId ,
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
#[ ts(optional) ]
pub thread_name : Option < String > ,
}
/// 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 ,
}
#[ derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS) ]
pub struct CollabAgentSpawnBeginEvent {
/// Identifier for the collab tool call.
pub call_id : String ,
/// 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 ,
/// 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 ,
/// 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 ,
/// 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, Deserialize, Serialize, PartialEq, JsonSchema, TS) ]
pub struct CollabWaitingBeginEvent {
/// 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 ,
/// 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 ,
/// 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 ,
/// 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 ,
/// 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 ,
/// 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 ::ImageGenerationItem ;
use crate ::items ::UserMessageItem ;
use crate ::items ::WebSearchItem ;
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 ;
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 {
if policy . has_full_disk_read_access ( ) {
return true ;
}
policy
. get_readable_roots_with_cwd ( cwd )
. iter ( )
. any ( | root | path . starts_with ( root . as_path ( ) ) )
| | policy
. get_writable_roots_with_cwd ( cwd )
. iter ( )
. any ( | root | path . starts_with ( root . root . as_path ( ) ) )
}
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 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_thread_source_name_classifies_user_and_subagent_sources ( ) {
for ( source , expected ) in [
( SessionSource ::Cli , Some ( " user " ) ) ,
( SessionSource ::VSCode , Some ( " user " ) ) ,
( SessionSource ::Exec , Some ( " user " ) ) ,
(
SessionSource ::SubAgent ( SubAgentSource ::Review ) ,
Some ( " subagent " ) ,
) ,
( SessionSource ::Mcp , None ) ,
( SessionSource ::Custom ( " atlas " . to_string ( ) ) , None ) ,
( SessionSource ::Unknown , None ) ,
] {
assert_eq! ( source . thread_source_name ( ) , expected ) ;
}
}
#[ 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
) ;
}
#[ 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 ( ) ] ;
paths . extend (
policy
. get_readable_roots_with_cwd ( cwd )
. into_iter ( )
. map ( | path | path . 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 ( )
) ;
assert_eq! (
actual . include_platform_defaults ( ) ,
expected . include_platform_defaults ( )
) ;
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 {
access : ReadOnlyAccess ::FullAccess ,
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 ,
request_permissions : false ,
mcp_elicitations : true ,
}
. allows_mcp_elicitations ( )
) ;
assert! (
! GranularApprovalConfig {
sandbox_approval : false ,
rules : false ,
request_permissions : false ,
mcp_elicitations : false ,
}
. allows_mcp_elicitations ( )
) ;
}
#[ test ]
fn granular_approval_config_request_permissions_flag_is_field_driven ( ) {
assert! (
GranularApprovalConfig {
sandbox_approval : false ,
rules : false ,
request_permissions : true ,
mcp_elicitations : false ,
}
. allows_request_permissions ( )
) ;
assert! (
! GranularApprovalConfig {
sandbox_approval : false ,
rules : 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 ,
request_permissions : false ,
mcp_elicitations : true ,
}
) ;
}
#[ test ]
fn workspace_write_restricted_read_access_includes_effective_writable_roots ( ) {
let cwd = if cfg! ( windows ) {
Path ::new ( r "C:\workspace" )
} else {
Path ::new ( " /tmp/workspace " )
} ;
let policy = SandboxPolicy ::WorkspaceWrite {
writable_roots : vec ! [ ] ,
read_only_access : ReadOnlyAccess ::Restricted {
include_platform_defaults : false ,
readable_roots : vec ! [ ] ,
} ,
network_access : false ,
exclude_tmpdir_env_var : true ,
exclude_slash_tmp : false ,
} ;
let readable_roots = policy . get_readable_roots_with_cwd ( cwd ) ;
let writable_roots = policy . get_writable_roots_with_cwd ( cwd ) ;
for writable_root in writable_roots {
assert! (
readable_roots
. iter ( )
. any ( | root | root . as_path ( ) = = writable_root . root . as_path ( ) ) ,
" expected writable root {} to also be readable " ,
writable_root . root . as_path ( ) . display ( )
) ;
}
}
#[ 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 ::None ,
} ,
] ) ;
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 ::CurrentWorkingDirectory ,
} ,
access : FileSystemAccessMode ::Write ,
} ,
FileSystemSandboxEntry {
path : FileSystemPath ::Path { path : secret } ,
access : FileSystemAccessMode ::None ,
} ,
] ) ;
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 ::CurrentWorkingDirectory ,
} ,
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 legacy_workspace_write_nested_readable_root_stays_writable ( ) {
let cwd = TempDir ::new ( ) . expect ( " tempdir " ) ;
let docs = AbsolutePathBuf ::resolve_path_against_base ( " docs " , cwd . path ( ) ) ;
let canonical_cwd = codex_utils_absolute_path ::canonicalize_preserving_symlinks ( cwd . path ( ) )
. expect ( " canonicalize cwd " ) ;
let expected_dot_codex = AbsolutePathBuf ::from_absolute_path ( canonical_cwd . join ( " .codex " ) )
. expect ( " canonical .codex " ) ;
let policy = SandboxPolicy ::WorkspaceWrite {
writable_roots : vec ! [ ] ,
read_only_access : ReadOnlyAccess ::Restricted {
include_platform_defaults : true ,
readable_roots : vec ! [ docs ] ,
} ,
network_access : false ,
exclude_tmpdir_env_var : true ,
exclude_slash_tmp : true ,
} ;
assert_eq! (
sorted_writable_roots (
FileSystemSandboxPolicy ::from_legacy_sandbox_policy ( & policy , cwd . path ( ) )
. get_writable_roots_with_cwd ( cwd . path ( ) )
) ,
vec! [ ( canonical_cwd , vec! [ expected_dot_codex . to_path_buf ( ) ] ) ]
) ;
}
#[ 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 readable_root = AbsolutePathBuf ::resolve_path_against_base ( " readable " , cwd . path ( ) ) ;
let writable_root = AbsolutePathBuf ::resolve_path_against_base ( " writable " , cwd . path ( ) ) ;
let nested_readable_root = AbsolutePathBuf ::resolve_path_against_base ( " docs " , cwd . path ( ) ) ;
let policies = [
SandboxPolicy ::DangerFullAccess ,
SandboxPolicy ::ExternalSandbox {
network_access : NetworkAccess ::Restricted ,
} ,
SandboxPolicy ::ExternalSandbox {
network_access : NetworkAccess ::Enabled ,
} ,
SandboxPolicy ::ReadOnly {
access : ReadOnlyAccess ::FullAccess ,
network_access : false ,
} ,
SandboxPolicy ::ReadOnly {
access : ReadOnlyAccess ::Restricted {
include_platform_defaults : true ,
readable_roots : vec ! [ readable_root . clone ( ) ] ,
} ,
network_access : true ,
} ,
SandboxPolicy ::WorkspaceWrite {
writable_roots : vec ! [ ] ,
read_only_access : ReadOnlyAccess ::FullAccess ,
network_access : false ,
exclude_tmpdir_env_var : true ,
exclude_slash_tmp : true ,
} ,
SandboxPolicy ::WorkspaceWrite {
writable_roots : vec ! [ writable_root ] ,
read_only_access : ReadOnlyAccess ::Restricted {
include_platform_defaults : true ,
readable_roots : vec ! [ readable_root ] ,
} ,
network_access : true ,
exclude_tmpdir_env_var : false ,
exclude_slash_tmp : true ,
} ,
SandboxPolicy ::WorkspaceWrite {
writable_roots : vec ! [ ] ,
read_only_access : ReadOnlyAccess ::Restricted {
include_platform_defaults : true ,
readable_roots : vec ! [ nested_readable_root ] ,
} ,
network_access : false ,
exclude_tmpdir_env_var : true ,
exclude_slash_tmp : true ,
} ,
] ;
for expected in policies {
let actual = FileSystemSandboxPolicy ::from_legacy_sandbox_policy ( & 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 ,
} ,
} ) ,
} ;
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 ( & [ ] ) ) ,
} ;
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 ,
} ) ,
} ;
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_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 ( ) ) ,
} ) ,
} ;
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 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 conversation_op_serializes_as_unnested_variants ( ) {
let audio = Op ::RealtimeConversationAudio ( ConversationAudioParams {
frame : RealtimeAudioFrame {
data : " AQID " . to_string ( ) ,
sample_rate : 24_000 ,
num_channels : 1 ,
samples_per_channel : Some ( 480 ) ,
item_id : None ,
} ,
} ) ;
let start = Op ::RealtimeConversationStart ( ConversationStartParams {
output_modality : RealtimeOutputModality ::Audio ,
prompt : Some ( Some ( " be helpful " . to_string ( ) ) ) ,
session_id : Some ( " conv_1 " . to_string ( ) ) ,
transport : None ,
voice : None ,
} ) ;
let webrtc_start = Op ::RealtimeConversationStart ( ConversationStartParams {
output_modality : RealtimeOutputModality ::Audio ,
prompt : Some ( Some ( " be helpful " . to_string ( ) ) ) ,
session_id : Some ( " conv_1 " . to_string ( ) ) ,
transport : Some ( ConversationStartTransport ::Webrtc {
sdp : " v=offer \r \n " . to_string ( ) ,
} ) ,
voice : Some ( RealtimeVoice ::Cove ) ,
} ) ;
let text = Op ::RealtimeConversationText ( ConversationTextParams {
text : " hello " . to_string ( ) ,
} ) ;
let close = Op ::RealtimeConversationClose ;
let default_prompt_start = Op ::RealtimeConversationStart ( ConversationStartParams {
output_modality : RealtimeOutputModality ::Audio ,
prompt : None ,
session_id : None ,
transport : None ,
voice : None ,
} ) ;
let null_prompt_start = Op ::RealtimeConversationStart ( ConversationStartParams {
output_modality : RealtimeOutputModality ::Audio ,
prompt : Some ( None ) ,
session_id : None ,
transport : None ,
voice : None ,
} ) ;
let list_voices = Op ::RealtimeConversationListVoices ;
assert_eq! (
serde_json ::to_value ( & start ) . unwrap ( ) ,
json! ( {
" type " : " realtime_conversation_start " ,
" output_modality " : " audio " ,
" prompt " : " be helpful " ,
" session_id " : " conv_1 "
} )
) ;
assert_eq! (
serde_json ::to_value ( & default_prompt_start ) . unwrap ( ) ,
json! ( {
" type " : " realtime_conversation_start " ,
" output_modality " : " audio "
} )
) ;
assert_eq! (
serde_json ::to_value ( & null_prompt_start ) . unwrap ( ) ,
json! ( {
" type " : " realtime_conversation_start " ,
" output_modality " : " audio " ,
" prompt " : null
} )
) ;
assert_eq! (
serde_json ::from_value ::< Op > ( json! ( {
" type " : " realtime_conversation_start " ,
" output_modality " : " audio "
} ) )
. unwrap ( ) ,
default_prompt_start
) ;
assert_eq! (
serde_json ::from_value ::< Op > ( json! ( {
" type " : " realtime_conversation_start " ,
" output_modality " : " audio " ,
" prompt " : null
} ) )
. unwrap ( ) ,
null_prompt_start
) ;
assert_eq! (
serde_json ::to_value ( & audio ) . unwrap ( ) ,
json! ( {
" type " : " realtime_conversation_audio " ,
" frame " : {
" data " : " AQID " ,
" sample_rate " : 24000 ,
" num_channels " : 1 ,
" samples_per_channel " : 480
}
} )
) ;
assert_eq! (
serde_json ::from_value ::< Op > ( serde_json ::to_value ( & text ) . unwrap ( ) ) . unwrap ( ) ,
text
) ;
assert_eq! (
serde_json ::to_value ( & close ) . unwrap ( ) ,
json! ( {
" type " : " realtime_conversation_close "
} )
) ;
assert_eq! (
serde_json ::from_value ::< Op > ( serde_json ::to_value ( & close ) . unwrap ( ) ) . unwrap ( ) ,
close
) ;
assert_eq! (
serde_json ::to_value ( & list_voices ) . unwrap ( ) ,
json! ( {
" type " : " realtime_conversation_list_voices "
} )
) ;
assert_eq! (
serde_json ::from_value ::< Op > ( serde_json ::to_value ( & list_voices ) . unwrap ( ) ) . unwrap ( ) ,
list_voices
) ;
assert_eq! (
serde_json ::to_value ( & webrtc_start ) . unwrap ( ) ,
json! ( {
" type " : " realtime_conversation_start " ,
" output_modality " : " audio " ,
" prompt " : " be helpful " ,
" session_id " : " conv_1 " ,
" transport " : {
" type " : " webrtc " ,
" sdp " : " v=offer \r \n "
} ,
" voice " : " cove "
} )
) ;
}
#[ 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_serialization_omits_final_output_json_schema_when_none ( ) -> Result < ( ) > {
let op = Op ::UserInput {
environments : None ,
items : Vec ::new ( ) ,
final_output_json_schema : None ,
responsesapi_client_metadata : None ,
} ;
let json_op = serde_json ::to_value ( op ) ? ;
assert_eq! ( json_op , json! ( { " type " : " user_input " , " items " : [ ] } ) ) ;
Ok ( ( ) )
}
#[ test ]
fn user_input_deserializes_without_final_output_json_schema_field ( ) -> Result < ( ) > {
let op : Op = serde_json ::from_value ( json! ( { " type " : " user_input " , " items " : [ ] } ) ) ? ;
assert_eq! (
op ,
Op ::UserInput {
environments : None ,
items : Vec ::new ( ) ,
final_output_json_schema : None ,
responsesapi_client_metadata : None ,
}
) ;
Ok ( ( ) )
}
#[ test ]
fn user_input_serialization_includes_final_output_json_schema_when_some ( ) -> Result < ( ) > {
let schema = json! ( {
" type " : " object " ,
" properties " : {
" answer " : { " type " : " string " }
} ,
" required " : [ " answer " ] ,
" additionalProperties " : false
} ) ;
let op = Op ::UserInput {
environments : None ,
items : Vec ::new ( ) ,
final_output_json_schema : Some ( schema . clone ( ) ) ,
responsesapi_client_metadata : None ,
} ;
let json_op = serde_json ::to_value ( op ) ? ;
assert_eq! (
json_op ,
json! ( {
" type " : " user_input " ,
" items " : [ ] ,
" final_output_json_schema " : schema ,
} )
) ;
Ok ( ( ) )
}
#[ test ]
fn user_input_with_responsesapi_client_metadata_round_trips ( ) -> Result < ( ) > {
let op = Op ::UserInput {
environments : None ,
items : Vec ::new ( ) ,
final_output_json_schema : None ,
responsesapi_client_metadata : Some ( HashMap ::from ( [ (
" fiber_run_id " . to_string ( ) ,
" fiber-123 " . to_string ( ) ,
) ] ) ) ,
} ;
let json_op = serde_json ::to_value ( & op ) ? ;
assert_eq! (
json_op ,
json! ( {
" type " : " user_input " ,
" items " : [ ] ,
" responsesapi_client_metadata " : {
" fiber_run_id " : " fiber-123 " ,
}
} )
) ;
assert_eq! ( serde_json ::from_value ::< Op > ( json_op ) ? , op ) ;
Ok ( ( ) )
}
#[ 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 {
message : " hello " . to_string ( ) ,
images : None ,
local_images : Vec ::new ( ) ,
text_elements : Vec ::new ( ) ,
} ;
let json_event = serde_json ::to_value ( event ) ? ;
assert_eq! (
json_event ,
json! ( {
" message " : " hello " ,
" local_images " : [ ] ,
" text_elements " : [ ] ,
} )
) ;
Ok ( ( ) )
}
#[ 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 . trace_id , None ) ;
assert_eq! ( item . network , None ) ;
assert_eq! ( item . file_system_sandbox_policy , None ) ;
Ok ( ( ) )
}
#[ test ]
fn turn_context_item_serializes_network_when_present ( ) -> Result < ( ) > {
let item = TurnContextItem {
turn_id : None ,
trace_id : None ,
cwd : test_path_buf ( " /tmp " ) ,
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 ::None ,
} ,
] ) ) ,
model : " gpt-5 " . to_string ( ) ,
personality : None ,
collaboration_mode : None ,
realtime_active : None ,
effort : None ,
summary : ReasoningSummaryConfig ::Auto ,
user_instructions : None ,
developer_instructions : None ,
final_output_json_schema : None ,
truncation_policy : None ,
} ;
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 " : " none "
} ]
} )
) ;
Ok ( ( ) )
}
fn sample_epiphany_thread_state ( ) -> EpiphanyThreadState {
EpiphanyThreadState {
revision : 7 ,
objective : Some ( " Map the rollout seam before touching prompts " . to_string ( ) ) ,
active_subgoal_id : Some ( " phase1 " . to_string ( ) ) ,
subgoals : vec ! [ EpiphanySubgoal {
id : " phase1 " . to_string ( ) ,
title : " Persist baseline " . to_string ( ) ,
status : " active " . to_string ( ) ,
summary : Some ( " Wire a durable rollout snapshot into core " . to_string ( ) ) ,
} ] ,
invariants : vec ! [ EpiphanyInvariant {
id : " durable-state " . to_string ( ) ,
description : " Resume must rebuild the latest surviving Epiphany baseline "
. to_string ( ) ,
status : " enforced " . to_string ( ) ,
rationale : Some ( " Otherwise compaction turns the machine into soup " . to_string ( ) ) ,
} ] ,
graphs : EpiphanyGraphs {
architecture : EpiphanyGraph {
nodes : vec ! [ EpiphanyGraphNode {
id : " session " . to_string ( ) ,
title : " Session " . to_string ( ) ,
purpose : " Owns the mutable thread state " . to_string ( ) ,
mechanism : Some ( " Reads and writes rollout-backed baselines " . to_string ( ) ) ,
metaphor : Some (
" The machine room where the levers actually live " . to_string ( ) ,
) ,
status : Some ( " verified " . to_string ( ) ) ,
code_refs : vec ! [ EpiphanyCodeRef {
path : test_path_buf ( " /repo/core/src/session/mod.rs " ) ,
start_line : Some ( 2600 ) ,
end_line : Some ( 2690 ) ,
symbol : Some (
" record_context_updates_and_set_reference_context_item " . to_string ( ) ,
) ,
note : Some ( " Per-turn baseline persistence " . to_string ( ) ) ,
} ] ,
} ] ,
edges : vec ! [ ] ,
} ,
dataflow : EpiphanyGraph {
nodes : vec ! [ EpiphanyGraphNode {
id : " turn-context " . to_string ( ) ,
title : " Turn Context " . to_string ( ) ,
purpose : " Captures the durable per-turn baseline " . to_string ( ) ,
mechanism : Some ( " Persists after each real user turn " . to_string ( ) ) ,
metaphor : Some ( " The breadcrumb the replay engine follows home " . to_string ( ) ) ,
status : Some ( " verified " . to_string ( ) ) ,
code_refs : vec ! [ ] ,
} ] ,
edges : vec ! [ EpiphanyGraphEdge {
source_id : " turn-context " . to_string ( ) ,
target_id : " session " . to_string ( ) ,
kind : " persists_to " . to_string ( ) ,
id : Some ( " edge-1 " . to_string ( ) ) ,
label : Some ( " baseline write " . to_string ( ) ) ,
mechanism : Some ( " TurnContext snapshots seed replay metadata " . to_string ( ) ) ,
code_refs : vec ! [ ] ,
} ] ,
} ,
links : vec ! [ EpiphanyGraphLink {
dataflow_node_id : " turn-context " . to_string ( ) ,
architecture_node_id : " session " . to_string ( ) ,
relationship : Some ( " implemented_by " . to_string ( ) ) ,
code_refs : vec ! [ ] ,
} ] ,
} ,
graph_frontier : Some ( EpiphanyGraphFrontier {
active_node_ids : vec ! [ " turn-context " . to_string ( ) ] ,
active_edge_ids : vec ! [ " edge-1 " . to_string ( ) ] ,
open_question_ids : vec ! [ " q1 " . to_string ( ) ] ,
open_gap_ids : vec ! [ " g1 " . to_string ( ) ] ,
dirty_paths : vec ! [ test_path_buf ( " /repo/core/src/session/mod.rs " ) ] ,
} ) ,
graph_checkpoint : Some ( EpiphanyGraphCheckpoint {
checkpoint_id : " chk-1 " . to_string ( ) ,
graph_revision : 7 ,
summary : Some ( " Latest replay seam checkpoint " . to_string ( ) ) ,
frontier_node_ids : vec ! [ " turn-context " . to_string ( ) ] ,
open_question_ids : vec ! [ " q1 " . to_string ( ) ] ,
open_gap_ids : vec ! [ " g1 " . to_string ( ) ] ,
} ) ,
2026-04-30 00:34:13 +01:00
investigation_checkpoint : Some ( EpiphanyInvestigationCheckpoint {
checkpoint_id : " ix-1 " . to_string ( ) ,
kind : " source_gathering " . to_string ( ) ,
disposition : EpiphanyInvestigationDisposition ::ResumeReady ,
focus : " Resume the prompt-facing replay seam. " . to_string ( ) ,
summary : Some ( " The checkpoint keeps the active source seam warm. " . to_string ( ) ) ,
next_action : Some ( " Re-open the rollout path before broad edits. " . to_string ( ) ) ,
captured_at_turn_id : Some ( " turn-123 " . to_string ( ) ) ,
open_questions : vec ! [ " Does compaction still preserve the seam? " . to_string ( ) ] ,
code_refs : vec ! [ EpiphanyCodeRef {
path : test_path_buf ( " /repo/core/src/session/mod.rs " ) ,
start_line : Some ( 2600 ) ,
end_line : Some ( 2690 ) ,
symbol : Some (
" record_context_updates_and_set_reference_context_item " . to_string ( ) ,
) ,
note : Some ( " Investigation checkpoint anchor " . to_string ( ) ) ,
} ] ,
evidence_ids : vec ! [ " ev-1 " . to_string ( ) ] ,
} ) ,
2026-04-24 02:45:02 +01:00
retrieval : Some ( EpiphanyRetrievalState {
workspace_root : test_path_buf ( " /repo " ) ,
index_revision : Some ( " bm25-v1 " . to_string ( ) ) ,
status : EpiphanyRetrievalStatus ::Ready ,
semantic_available : true ,
last_indexed_at_unix_seconds : Some ( 1_744_500_000 ) ,
indexed_file_count : Some ( 12 ) ,
indexed_chunk_count : Some ( 34 ) ,
shards : vec ! [ EpiphanyRetrievalShardSummary {
shard_id : " workspace " . to_string ( ) ,
path_prefix : PathBuf ::from ( " . " ) ,
indexed_file_count : Some ( 12 ) ,
indexed_chunk_count : Some ( 34 ) ,
status : EpiphanyRetrievalStatus ::Ready ,
exact_available : true ,
semantic_available : true ,
} ] ,
dirty_paths : vec ! [ test_path_buf ( " /repo/core/src/session/mod.rs " ) ] ,
} ) ,
2026-04-23 23:20:16 +01:00
scratch : Some ( EpiphanyScratchPad {
summary : Some ( " Persist first, get fancy later " . to_string ( ) ) ,
hypothesis : Some ( " One extra rollout item is enough for Phase 1 " . to_string ( ) ) ,
next_probe : Some ( " Replay after rollback and compaction " . to_string ( ) ) ,
notes : vec ! [ " Do not touch prompts yet " . to_string ( ) ] ,
} ) ,
2026-04-26 12:31:28 +01:00
job_bindings : Vec ::new ( ) ,
2026-05-13 11:04:39 +01:00
acceptance_receipts : Vec ::new ( ) ,
runtime_links : Vec ::new ( ) ,
2026-04-23 23:20:16 +01:00
observations : vec ! [ EpiphanyObservation {
id : " obs-1 " . to_string ( ) ,
summary : " TurnContext already gives us the persistence seam " . to_string ( ) ,
source_kind : " tool_output " . to_string ( ) ,
status : " promoted " . to_string ( ) ,
code_refs : vec ! [ ] ,
evidence_ids : vec ! [ " ev-1 " . to_string ( ) ] ,
} ] ,
recent_evidence : vec ! [ EpiphanyEvidenceRecord {
id : " ev-1 " . to_string ( ) ,
kind : " research " . to_string ( ) ,
status : " ok " . to_string ( ) ,
summary : " Reconstruction already respects rollback and compaction boundaries "
. to_string ( ) ,
code_refs : vec ! [ ] ,
} ] ,
churn : Some ( EpiphanyChurnState {
understanding_status : " clear " . to_string ( ) ,
diff_pressure : " low " . to_string ( ) ,
graph_freshness : Some ( " fresh " . to_string ( ) ) ,
warning : None ,
unexplained_writes : Some ( 0 ) ,
} ) ,
mode : Some ( EpiphanyModeState {
name : " epiphany " . to_string ( ) ,
2026-05-13 11:04:39 +01:00
kind : Some ( EpiphanyModeKind ::Default ) ,
2026-04-23 23:20:16 +01:00
} ) ,
2026-05-13 11:04:39 +01:00
planning : EpiphanyPlanningState ::default ( ) ,
2026-04-23 23:20:16 +01:00
last_updated_turn_id : Some ( " turn-123 " . to_string ( ) ) ,
}
}
#[ test ]
fn epiphany_thread_state_round_trips ( ) -> Result < ( ) > {
let original = sample_epiphany_thread_state ( ) ;
let value = serde_json ::to_value ( & original ) ? ;
let reparsed : EpiphanyThreadState = serde_json ::from_value ( value ) ? ;
assert_eq! ( reparsed , original ) ;
Ok ( ( ) )
}
#[ test ]
fn epiphany_state_item_round_trips ( ) -> Result < ( ) > {
let original = EpiphanyStateItem {
turn_id : Some ( " turn-123 " . to_string ( ) ) ,
state : sample_epiphany_thread_state ( ) ,
} ;
let value = serde_json ::to_value ( & original ) ? ;
let reparsed : EpiphanyStateItem = serde_json ::from_value ( value ) ? ;
assert_eq! ( reparsed , original ) ;
Ok ( ( ) )
}
/// Serialize Event to verify that its JSON representation has the expected
/// amount of nesting.
#[ test ]
fn serialize_event ( ) -> Result < ( ) > {
let conversation_id = ThreadId ::from_string ( " 67e55044-10b1-426f-9247-bb680e5fe0c8 " ) ? ;
let rollout_file = NamedTempFile ::new ( ) ? ;
let event = Event {
id : " 1234 " . to_string ( ) ,
msg : EventMsg ::SessionConfigured ( SessionConfiguredEvent {
session_id : conversation_id ,
forked_from_id : 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 ,
sandbox_policy : SandboxPolicy ::new_read_only_policy ( ) ,
cwd : test_path_buf ( " /home/user/project " ) . abs ( ) ,
reasoning_effort : Some ( ReasoningEffortConfig ::default ( ) ) ,
history_log_id : 0 ,
history_entry_count : 0 ,
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-bb680e5fe0c8 " ,
" model " : " codex-mini-latest " ,
" model_provider_id " : " openai " ,
" approval_policy " : " never " ,
" approvals_reviewer " : " user " ,
" sandbox_policy " : {
" type " : " read-only "
} ,
" cwd " : test_path_buf ( " /home/user/project " ) ,
" reasoning_effort " : " medium " ,
" history_log_id " : 0 ,
" history_entry_count " : 0 ,
" rollout_path " : format ! ( " {} " , rollout_file . path ( ) . display ( ) ) ,
}
} ) ;
assert_eq! ( expected , serde_json ::to_value ( & event ) ? ) ;
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 ) ,
2026-04-26 04:24:55 +01:00
model_auto_compact_token_limit : Some ( 240_000 ) ,
2026-04-23 23:20:16 +01:00
} ) ;
let last = Some ( TokenUsage {
input_tokens : 10 ,
cached_input_tokens : 0 ,
output_tokens : 0 ,
reasoning_output_tokens : 0 ,
total_tokens : 10 ,
} ) ;
2026-04-26 04:24:55 +01:00
let info = TokenUsageInfo ::new_or_append ( & initial , & last , Some ( 128_000 ) , Some ( 120_000 ) )
2026-04-23 23:20:16 +01:00
. expect ( " new_or_append should return info " ) ;
assert_eq! ( info . model_context_window , Some ( 128_000 ) ) ;
2026-04-26 04:24:55 +01:00
assert_eq! ( info . model_auto_compact_token_limit , Some ( 120_000 ) ) ;
2026-04-23 23:20:16 +01:00
}
#[ 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 ) ,
2026-04-26 04:24:55 +01:00
model_auto_compact_token_limit : Some ( 240_000 ) ,
2026-04-23 23:20:16 +01:00
} ) ;
let last = Some ( TokenUsage {
input_tokens : 10 ,
cached_input_tokens : 0 ,
output_tokens : 0 ,
reasoning_output_tokens : 0 ,
total_tokens : 10 ,
} ) ;
2026-04-26 04:24:55 +01:00
let info = TokenUsageInfo ::new_or_append (
& initial , & last , /* model_context_window */ None ,
/* model_auto_compact_token_limit */ None ,
)
. expect ( " new_or_append should return info " ) ;
2026-04-23 23:20:16 +01:00
assert_eq! ( info . model_context_window , Some ( 258_400 ) ) ;
2026-04-26 04:24:55 +01:00
assert_eq! ( info . model_auto_compact_token_limit , Some ( 240_000 ) ) ;
2026-04-23 23:20:16 +01:00
}
}