1use std::collections::BTreeMap;
7use std::collections::HashMap;
8use std::fmt;
9use std::ops::Mul;
10use std::path::Path;
11use std::path::PathBuf;
12use std::str::FromStr;
13use std::sync::Arc;
14use std::time::Duration;
15
16use strum_macros::EnumIter;
17
18use crate::AgentPath;
19use crate::ResponseItemId;
20use crate::SessionId;
21use crate::ThreadId;
22use crate::approvals::ElicitationRequestEvent;
23use crate::capabilities::SelectedCapabilityRoot;
24use crate::config_types::ApprovalsReviewer;
25use crate::config_types::CollaborationMode;
26use crate::config_types::ModeKind;
27use crate::config_types::MultiAgentMode;
28use crate::config_types::Personality;
29use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;
30use crate::config_types::WindowsSandboxLevel;
31use crate::dynamic_tools::DynamicToolCallOutputContentItem;
32use crate::dynamic_tools::DynamicToolCallRequest;
33use crate::dynamic_tools::DynamicToolResponse;
34use crate::dynamic_tools::DynamicToolSpec;
35use crate::items::TurnItem;
36use crate::mcp::CallToolResult;
37use crate::mcp::RequestId;
38use crate::memory_citation::MemoryCitation;
39use crate::models::ActivePermissionProfile;
40use crate::models::AgentMessageInputContent;
41use crate::models::BaseInstructions;
42use crate::models::ContentItem;
43use crate::models::ImageDetail;
44use crate::models::InternalChatMessageMetadataPassthrough;
45use crate::models::MessagePhase;
46use crate::models::PermissionProfile;
47use crate::models::ResponseInputItem;
48use crate::models::ResponseItem;
49use crate::models::SandboxEnforcement;
50use crate::models::WebSearchAction;
51use crate::num_format::format_with_separators;
52use crate::openai_models::ReasoningEffort as ReasoningEffortConfig;
53use crate::parse_command::ParsedCommand;
54use crate::plan_tool::UpdatePlanArgs;
55use crate::request_permissions::RequestPermissionsEvent;
56use crate::request_permissions::RequestPermissionsResponse;
57use crate::request_user_input::RequestUserInputResponse;
58use crate::user_input::UserInput;
59use codex_utils_absolute_path::AbsolutePathBuf;
60use codex_utils_path_uri::PathUri;
61use schemars::JsonSchema;
62use serde::Deserialize;
63use serde::Deserializer;
64use serde::Serialize;
65use serde::de::Error as _;
66use serde_json::Value;
67use serde_with::serde_as;
68use strum_macros::Display;
69use tracing::error;
70use ts_rs::TS;
71
72pub use crate::approvals::ApplyPatchApprovalRequestEvent;
73pub use crate::approvals::ElicitationAction;
74pub use crate::approvals::ExecApprovalRequestEvent;
75pub use crate::approvals::ExecPolicyAmendment;
76pub use crate::approvals::GuardianAssessmentAction;
77pub use crate::approvals::GuardianAssessmentDecisionSource;
78pub use crate::approvals::GuardianAssessmentEvent;
79pub use crate::approvals::GuardianAssessmentOutcome;
80pub use crate::approvals::GuardianAssessmentStatus;
81pub use crate::approvals::GuardianCommandSource;
82pub use crate::approvals::GuardianRiskLevel;
83pub use crate::approvals::GuardianUserAuthorization;
84pub use crate::approvals::NetworkApprovalContext;
85pub use crate::approvals::NetworkApprovalProtocol;
86pub use crate::approvals::NetworkPolicyAmendment;
87pub use crate::approvals::NetworkPolicyRuleAction;
88pub use crate::legacy_events::HasLegacyEvent;
89pub use crate::permissions::FileSystemAccessMode;
90pub use crate::permissions::FileSystemPath;
91pub use crate::permissions::FileSystemSandboxEntry;
92pub use crate::permissions::FileSystemSandboxKind;
93pub use crate::permissions::FileSystemSandboxPolicy;
94pub use crate::permissions::FileSystemSpecialPath;
95pub use crate::permissions::NetworkSandboxPolicy;
96use crate::permissions::default_read_only_subpaths_for_writable_root;
97pub use crate::request_permissions::RequestPermissionsArgs;
98pub use crate::request_user_input::RequestUserInputEvent;
99
100pub const USER_INSTRUCTIONS_OPEN_TAG: &str = "<user_instructions>";
103pub const USER_INSTRUCTIONS_CLOSE_TAG: &str = "</user_instructions>";
104pub const ENVIRONMENT_CONTEXT_OPEN_TAG: &str = "<environment_context>";
105pub const ENVIRONMENT_CONTEXT_CLOSE_TAG: &str = "</environment_context>";
106pub const ENVIRONMENTS_INSTRUCTIONS_OPEN_TAG: &str = "<environments_instructions>";
107pub const ENVIRONMENTS_INSTRUCTIONS_CLOSE_TAG: &str = "</environments_instructions>";
108pub const APPS_INSTRUCTIONS_OPEN_TAG: &str = "<apps_instructions>";
109pub const APPS_INSTRUCTIONS_CLOSE_TAG: &str = "</apps_instructions>";
110pub const SKILLS_INSTRUCTIONS_OPEN_TAG: &str = "<skills_instructions>";
111pub const SKILLS_INSTRUCTIONS_CLOSE_TAG: &str = "</skills_instructions>";
112pub const PLUGINS_INSTRUCTIONS_OPEN_TAG: &str = "<plugins_instructions>";
113pub const PLUGINS_INSTRUCTIONS_CLOSE_TAG: &str = "</plugins_instructions>";
114pub const COLLABORATION_MODE_OPEN_TAG: &str = "<collaboration_mode>";
115pub const COLLABORATION_MODE_CLOSE_TAG: &str = "</collaboration_mode>";
116pub const MULTI_AGENT_MODE_OPEN_TAG: &str = "<multi_agent_mode>";
117pub const MULTI_AGENT_MODE_CLOSE_TAG: &str = "</multi_agent_mode>";
118pub const REALTIME_CONVERSATION_OPEN_TAG: &str = "<realtime_conversation>";
119pub const REALTIME_CONVERSATION_CLOSE_TAG: &str = "</realtime_conversation>";
120pub const CONTEXT_WINDOW_OPEN_TAG: &str = "<context_window>";
121pub const CONTEXT_WINDOW_CLOSE_TAG: &str = "</context_window>";
122pub const CONTEXT_WINDOW_GUIDANCE_OPEN_TAG: &str = "<context_window_guidance>";
123pub const CONTEXT_WINDOW_GUIDANCE_CLOSE_TAG: &str = "</context_window_guidance>";
124pub const USER_MESSAGE_BEGIN: &str = "## My request for Codex:";
125
126pub fn strip_user_message_prefix(text: &str) -> &str {
128 match text.find(USER_MESSAGE_BEGIN) {
129 Some(idx) => text[idx + USER_MESSAGE_BEGIN.len()..].trim(),
130 None => text.trim(),
131 }
132}
133
134#[derive(Debug, Clone, PartialEq)]
137pub struct TurnEnvironmentSelection {
138 pub environment_id: String,
139 pub cwd: PathUri,
140 pub workspace_roots: Vec<PathUri>,
141}
142
143#[derive(Debug, Clone, PartialEq)]
144pub struct TurnEnvironmentSelections {
145 pub legacy_fallback_cwd: AbsolutePathBuf,
146 pub environments: Vec<TurnEnvironmentSelection>,
147}
148
149impl TurnEnvironmentSelections {
150 pub fn new(
151 legacy_fallback_cwd: AbsolutePathBuf,
152 environments: Vec<TurnEnvironmentSelection>,
153 ) -> Self {
154 Self {
155 legacy_fallback_cwd,
156 environments,
157 }
158 }
159}
160
161#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)]
162#[serde(transparent)]
163#[ts(type = "string")]
164pub struct GitSha(pub String);
165
166impl GitSha {
167 pub fn new(sha: &str) -> Self {
168 Self(sha.to_string())
169 }
170}
171
172#[derive(Debug, Clone)]
174pub struct Submission {
175 pub id: String,
177 pub op: Op,
179 pub client_user_message_id: Option<String>,
181 pub trace: Option<W3cTraceContext>,
183}
184
185#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
186pub struct W3cTraceContext {
187 #[serde(default, skip_serializing_if = "Option::is_none")]
188 #[ts(optional)]
189 pub traceparent: Option<String>,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
191 #[ts(optional)]
192 pub tracestate: Option<String>,
193}
194
195#[derive(Debug, Clone, PartialEq)]
197pub struct McpServerRefreshConfig {
198 pub mcp_servers: Value,
200 pub mcp_oauth_credentials_store_mode: Value,
202 pub auth_keyring_backend_kind: Value,
203}
204
205#[derive(Debug, Clone, PartialEq)]
206pub struct ConversationStartParams {
207 pub client_managed_handoffs: bool,
209 pub flush_transcript_tail_on_session_end: bool,
212 pub codex_responses_as_items: bool,
214 pub codex_response_item_prefix: Option<String>,
216 pub codex_response_handoff_mode: CodexResponseHandoffMode,
219 pub model: Option<String>,
221 pub output_modality: RealtimeOutputModality,
223 pub include_startup_context: bool,
225 pub initial_items: Vec<ConversationTextParams>,
227 pub prompt: Option<Option<String>>,
228 pub realtime_session_id: Option<String>,
229 pub transport: Option<ConversationStartTransport>,
230 pub version: Option<RealtimeConversationVersion>,
232 pub voice: Option<RealtimeVoice>,
233}
234
235#[derive(Debug, Clone, PartialEq)]
236pub enum ConversationStartTransport {
237 Websocket,
238 Webrtc { sdp: String },
239}
240
241#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
242#[serde(rename_all = "snake_case")]
243pub enum RealtimeOutputModality {
244 Text,
245 Audio,
246}
247
248#[derive(
249 Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash, JsonSchema, TS, Ord, PartialOrd,
250)]
251#[serde(rename_all = "snake_case")]
252#[ts(rename_all = "snake_case")]
253pub enum RealtimeVoice {
254 Alloy,
255 Arbor,
256 Ash,
257 Ballad,
258 Breeze,
259 Cedar,
260 Coral,
261 Cove,
262 Echo,
263 Ember,
264 Juniper,
265 Maple,
266 Marin,
267 Sage,
268 Shimmer,
269 Sol,
270 Spruce,
271 Vale,
272 Verse,
273}
274
275impl RealtimeVoice {
276 pub fn wire_name(self) -> &'static str {
277 match self {
278 Self::Alloy => "alloy",
279 Self::Arbor => "arbor",
280 Self::Ash => "ash",
281 Self::Ballad => "ballad",
282 Self::Breeze => "breeze",
283 Self::Cedar => "cedar",
284 Self::Coral => "coral",
285 Self::Cove => "cove",
286 Self::Echo => "echo",
287 Self::Ember => "ember",
288 Self::Juniper => "juniper",
289 Self::Maple => "maple",
290 Self::Marin => "marin",
291 Self::Sage => "sage",
292 Self::Shimmer => "shimmer",
293 Self::Sol => "sol",
294 Self::Spruce => "spruce",
295 Self::Vale => "vale",
296 Self::Verse => "verse",
297 }
298 }
299}
300
301#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
302#[serde(rename_all = "camelCase")]
303#[ts(rename_all = "camelCase")]
304pub struct RealtimeVoicesList {
305 pub v1: Vec<RealtimeVoice>,
306 pub v2: Vec<RealtimeVoice>,
307 pub default_v1: RealtimeVoice,
308 pub default_v2: RealtimeVoice,
309}
310
311impl RealtimeVoicesList {
312 pub fn builtin() -> Self {
313 Self {
314 v1: vec![
315 RealtimeVoice::Juniper,
316 RealtimeVoice::Maple,
317 RealtimeVoice::Spruce,
318 RealtimeVoice::Ember,
319 RealtimeVoice::Vale,
320 RealtimeVoice::Breeze,
321 RealtimeVoice::Arbor,
322 RealtimeVoice::Sol,
323 RealtimeVoice::Cove,
324 ],
325 v2: vec![
326 RealtimeVoice::Alloy,
327 RealtimeVoice::Ash,
328 RealtimeVoice::Ballad,
329 RealtimeVoice::Coral,
330 RealtimeVoice::Echo,
331 RealtimeVoice::Sage,
332 RealtimeVoice::Shimmer,
333 RealtimeVoice::Verse,
334 RealtimeVoice::Marin,
335 RealtimeVoice::Cedar,
336 ],
337 default_v1: RealtimeVoice::Cove,
338 default_v2: RealtimeVoice::Marin,
339 }
340 }
341}
342
343#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
344pub struct RealtimeAudioFrame {
345 pub data: String,
346 pub sample_rate: u32,
347 pub num_channels: u16,
348 #[serde(skip_serializing_if = "Option::is_none")]
349 pub samples_per_channel: Option<u32>,
350 #[serde(skip_serializing_if = "Option::is_none")]
351 pub item_id: Option<String>,
352}
353
354#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
355pub struct RealtimeTranscriptDelta {
356 pub delta: String,
357}
358
359#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
360pub struct RealtimeTranscriptDone {
361 pub text: String,
362}
363
364#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
365pub struct RealtimeTranscriptEntry {
366 pub role: String,
367 pub text: String,
368}
369
370#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
371pub struct RealtimeHandoffRequested {
372 pub handoff_id: String,
373 pub item_id: String,
374 pub input_transcript: String,
375 pub active_transcript: Vec<RealtimeTranscriptEntry>,
376}
377
378#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
379pub struct RealtimeNoopRequested {
380 pub call_id: String,
381 pub item_id: String,
382}
383
384#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
385pub struct RealtimeInputAudioSpeechStarted {
386 pub item_id: Option<String>,
387}
388
389#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
390pub struct RealtimeResponseCancelled {
391 pub response_id: Option<String>,
392}
393
394#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
395pub struct RealtimeResponseCreated {
396 pub response_id: Option<String>,
397}
398
399#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
400pub struct RealtimeResponseDone {
401 pub response_id: Option<String>,
402}
403
404#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
405pub enum RealtimeEvent {
406 SessionUpdated {
407 realtime_session_id: String,
408 instructions: Option<String>,
409 },
410 InputAudioSpeechStarted(RealtimeInputAudioSpeechStarted),
411 InputTranscriptDelta(RealtimeTranscriptDelta),
412 InputTranscriptDone(RealtimeTranscriptDone),
413 OutputTranscriptDelta(RealtimeTranscriptDelta),
414 OutputTranscriptDone(RealtimeTranscriptDone),
415 AudioOut(RealtimeAudioFrame),
416 ResponseCreated(RealtimeResponseCreated),
417 ResponseCancelled(RealtimeResponseCancelled),
418 ResponseDone(RealtimeResponseDone),
419 ConversationItemAdded(Value),
420 ConversationItemDone {
421 item_id: String,
422 },
423 HandoffRequested(RealtimeHandoffRequested),
424 NoopRequested(RealtimeNoopRequested),
425 Error(String),
426}
427
428#[derive(Debug, Clone, PartialEq)]
429pub struct ConversationAudioParams {
430 pub frame: RealtimeAudioFrame,
431}
432
433#[derive(Debug, Clone, PartialEq, Eq)]
434pub struct ConversationTextParams {
435 pub text: String,
436 pub role: ConversationTextRole,
437}
438
439#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize, JsonSchema, TS)]
440#[serde(rename_all = "snake_case")]
441#[ts(rename_all = "snake_case")]
442pub enum ConversationTextRole {
443 #[default]
444 User,
445 Developer,
446 Assistant,
447}
448
449#[derive(Debug, Clone, PartialEq)]
450pub struct ConversationSpeechParams {
451 pub text: String,
452}
453
454#[derive(Debug, Clone, Default, PartialEq)]
457pub struct ThreadSettingsOverrides {
458 pub environments: Option<TurnEnvironmentSelections>,
460
461 pub profile_workspace_roots: Option<Vec<AbsolutePathBuf>>,
464
465 pub approval_policy: Option<AskForApproval>,
467
468 pub approvals_reviewer: Option<ApprovalsReviewer>,
470
471 pub sandbox_policy: Option<SandboxPolicy>,
473
474 pub permission_profile: Option<PermissionProfile>,
476
477 pub active_permission_profile: Option<ActivePermissionProfile>,
480
481 pub windows_sandbox_level: Option<WindowsSandboxLevel>,
483
484 pub model: Option<String>,
486
487 pub effort: Option<Option<ReasoningEffortConfig>>,
492
493 pub summary: Option<ReasoningSummaryConfig>,
495
496 pub service_tier: Option<Option<String>>,
501
502 pub collaboration_mode: Option<CollaborationMode>,
505
506 pub personality: Option<Personality>,
508}
509
510#[derive(Debug, Clone, Copy, PartialEq, Eq)]
512pub enum AdditionalContextKind {
513 Untrusted,
514 Application,
515}
516
517#[derive(Debug, Clone, PartialEq, Eq)]
519pub struct AdditionalContextEntry {
520 pub value: String,
521 pub kind: AdditionalContextKind,
522}
523
524#[derive(Debug, Clone, PartialEq)]
526#[allow(clippy::large_enum_variant)]
527#[non_exhaustive]
528pub enum Op {
529 Interrupt,
532
533 CleanBackgroundTerminals,
536
537 RealtimeConversationStart(ConversationStartParams),
539
540 RealtimeConversationAudio(ConversationAudioParams),
542
543 RealtimeConversationText(ConversationTextParams),
545
546 RealtimeConversationSpeech(ConversationSpeechParams),
548
549 RealtimeConversationClose,
551
552 RealtimeConversationListVoices,
554
555 UserInput {
557 items: Vec<UserInput>,
559 final_output_json_schema: Option<Value>,
561 responsesapi_client_metadata: Option<HashMap<String, String>>,
563 additional_context: BTreeMap<String, AdditionalContextEntry>,
565
566 thread_settings: ThreadSettingsOverrides,
568 },
569
570 ThreadSettings {
575 thread_settings: ThreadSettingsOverrides,
577 },
578
579 InterAgentCommunication {
582 communication: InterAgentCommunication,
583 },
584
585 ExecApproval {
587 id: String,
589 turn_id: Option<String>,
591 decision: ReviewDecision,
593 },
594
595 PatchApproval {
597 id: String,
599 decision: ReviewDecision,
601 },
602
603 ResolveElicitation {
605 server_name: String,
607 request_id: RequestId,
609 decision: ElicitationAction,
611 content: Option<Value>,
613 meta: Option<Value>,
615 },
616
617 UserInputAnswer {
619 id: String,
621 response: RequestUserInputResponse,
623 },
624
625 RequestPermissionsResponse {
627 id: String,
629 response: RequestPermissionsResponse,
631 },
632
633 DynamicToolResponse {
635 id: String,
637 response: DynamicToolResponse,
639 },
640
641 RefreshMcpServers { config: McpServerRefreshConfig },
643
644 ReloadUserConfig,
649
650 Compact,
654
655 SetThreadMemoryMode { mode: ThreadMemoryMode },
660
661 ThreadRollback { num_turns: u32 },
666
667 Review { review_request: ReviewRequest },
669
670 ApproveGuardianDeniedAction { event: GuardianAssessmentEvent },
672
673 Shutdown,
675
676 RunUserShellCommand {
682 command: String,
684 },
685}
686
687#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema)]
688#[serde(rename_all = "lowercase")]
689pub enum ThreadMemoryMode {
690 Enabled,
691 Disabled,
692}
693
694#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, PartialEq, Eq, JsonSchema, TS)]
695#[serde(rename_all = "lowercase")]
696#[ts(rename_all = "lowercase")]
697pub enum ThreadHistoryMode {
698 #[default]
699 Legacy,
700 Paginated,
701}
702
703impl ThreadHistoryMode {
704 pub const fn as_str(self) -> &'static str {
705 match self {
706 Self::Legacy => "legacy",
707 Self::Paginated => "paginated",
708 }
709 }
710}
711
712impl FromStr for ThreadHistoryMode {
713 type Err = String;
714
715 fn from_str(value: &str) -> Result<Self, Self::Err> {
716 match value {
717 "legacy" => Ok(Self::Legacy),
718 "paginated" => Ok(Self::Paginated),
719 _ => Err(format!("unknown thread history mode `{value}`")),
720 }
721 }
722}
723
724impl From<Vec<UserInput>> for Op {
725 fn from(value: Vec<UserInput>) -> Self {
726 Op::UserInput {
727 items: value,
728 final_output_json_schema: None,
729 responsesapi_client_metadata: None,
730 additional_context: Default::default(),
731 thread_settings: ThreadSettingsOverrides::default(),
732 }
733 }
734}
735
736#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
737pub struct InterAgentCommunication {
738 #[serde(default, skip_serializing_if = "Option::is_none")]
739 #[ts(optional)]
740 pub id: Option<ResponseItemId>,
741 pub author: AgentPath,
742 pub recipient: AgentPath,
743 #[serde(default)]
744 pub other_recipients: Vec<AgentPath>,
745 pub content: String,
746 #[serde(default, skip_serializing_if = "Option::is_none")]
747 #[ts(optional)]
748 pub encrypted_content: Option<String>,
749 #[serde(default, skip_serializing_if = "Option::is_none")]
750 #[ts(optional)]
751 pub internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough>,
752 pub trigger_turn: bool,
753}
754
755impl InterAgentCommunication {
756 pub fn new(
757 author: AgentPath,
758 recipient: AgentPath,
759 other_recipients: Vec<AgentPath>,
760 content: String,
761 trigger_turn: bool,
762 ) -> Self {
763 Self {
764 id: None,
765 author,
766 recipient,
767 other_recipients,
768 content,
769 encrypted_content: None,
770 internal_chat_message_metadata_passthrough: None,
771 trigger_turn,
772 }
773 }
774
775 pub fn new_encrypted(
776 author: AgentPath,
777 recipient: AgentPath,
778 other_recipients: Vec<AgentPath>,
779 encrypted_content: String,
780 trigger_turn: bool,
781 ) -> Self {
782 Self {
783 id: None,
784 author,
785 recipient,
786 other_recipients,
787 content: String::new(),
788 encrypted_content: Some(encrypted_content),
789 internal_chat_message_metadata_passthrough: None,
790 trigger_turn,
791 }
792 }
793
794 pub fn set_turn_id_if_missing(&mut self, turn_id: &str) {
795 InternalChatMessageMetadataPassthrough::set_turn_id_if_missing(
796 &mut self.internal_chat_message_metadata_passthrough,
797 turn_id,
798 );
799 }
800
801 pub fn to_response_input_item(&self) -> ResponseInputItem {
802 let mut communication = self.clone();
803 communication.id = None;
804 communication.internal_chat_message_metadata_passthrough = None;
805 ResponseInputItem::Message {
806 role: "assistant".to_string(),
807 content: vec![ContentItem::OutputText {
808 text: serde_json::to_string(&communication).unwrap_or_default(),
809 }],
810 phase: Some(MessagePhase::Commentary),
811 }
812 }
813
814 pub fn to_model_input_item(&self) -> ResponseItem {
815 let content = match &self.encrypted_content {
816 Some(encrypted_content) => {
817 let message_type = if self.trigger_turn {
818 "NEW_TASK"
819 } else {
820 "MESSAGE"
821 };
822 vec![
823 AgentMessageInputContent::InputText {
824 text: format!(
825 "Message Type: {message_type}\nTask name: {}\nSender: {}\nPayload:\n",
826 self.recipient, self.author
827 ),
828 },
829 AgentMessageInputContent::EncryptedContent {
830 encrypted_content: encrypted_content.clone(),
831 },
832 ]
833 }
834 None => vec![AgentMessageInputContent::InputText {
835 text: self.content.clone(),
836 }],
837 };
838 ResponseItem::AgentMessage {
839 id: self.id.clone(),
840 author: self.author.to_string(),
841 recipient: self.recipient.to_string(),
842 content,
843 internal_chat_message_metadata_passthrough: self
844 .internal_chat_message_metadata_passthrough
845 .clone(),
846 }
847 }
848
849 pub fn is_message_content(content: &[ContentItem]) -> bool {
850 Self::from_message_content(content).is_some()
851 }
852
853 pub fn from_message_content(content: &[ContentItem]) -> Option<Self> {
854 match content {
855 [ContentItem::InputText { text }] | [ContentItem::OutputText { text }] => {
856 serde_json::from_str(text).ok()
857 }
858 _ => None,
859 }
860 }
861}
862
863impl Op {
864 pub fn kind(&self) -> &'static str {
865 match self {
866 Self::Interrupt => "interrupt",
867 Self::CleanBackgroundTerminals => "clean_background_terminals",
868 Self::RealtimeConversationStart(_) => "realtime_conversation_start",
869 Self::RealtimeConversationAudio(_) => "realtime_conversation_audio",
870 Self::RealtimeConversationText(_) => "realtime_conversation_text",
871 Self::RealtimeConversationSpeech(_) => "realtime_conversation_speech",
872 Self::RealtimeConversationClose => "realtime_conversation_close",
873 Self::RealtimeConversationListVoices => "realtime_conversation_list_voices",
874 Self::UserInput { .. } => "user_input",
875 Self::ThreadSettings { .. } => "thread_settings",
876 Self::InterAgentCommunication { .. } => "inter_agent_communication",
877 Self::ExecApproval { .. } => "exec_approval",
878 Self::PatchApproval { .. } => "patch_approval",
879 Self::ResolveElicitation { .. } => "resolve_elicitation",
880 Self::UserInputAnswer { .. } => "user_input_answer",
881 Self::RequestPermissionsResponse { .. } => "request_permissions_response",
882 Self::DynamicToolResponse { .. } => "dynamic_tool_response",
883 Self::RefreshMcpServers { .. } => "refresh_mcp_servers",
884 Self::ReloadUserConfig => "reload_user_config",
885 Self::Compact => "compact",
886 Self::SetThreadMemoryMode { .. } => "set_thread_memory_mode",
887 Self::ThreadRollback { .. } => "thread_rollback",
888 Self::Review { .. } => "review",
889 Self::ApproveGuardianDeniedAction { .. } => "approve_guardian_denied_action",
890 Self::Shutdown => "shutdown",
891 Self::RunUserShellCommand { .. } => "run_user_shell_command",
892 }
893 }
894}
895
896#[derive(
899 Debug,
900 Clone,
901 Copy,
902 Default,
903 PartialEq,
904 Eq,
905 Hash,
906 Serialize,
907 Deserialize,
908 Display,
909 JsonSchema,
910 TS,
911)]
912#[serde(rename_all = "kebab-case")]
913#[strum(serialize_all = "kebab-case")]
914pub enum AskForApproval {
915 #[serde(rename = "untrusted")]
919 #[strum(serialize = "untrusted")]
920 UnlessTrusted,
921
922 #[serde(alias = "on-failure")]
924 #[default]
925 OnRequest,
926
927 #[strum(serialize = "granular")]
933 Granular(GranularApprovalConfig),
934
935 Never,
938}
939
940#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)]
941pub struct GranularApprovalConfig {
942 pub sandbox_approval: bool,
945 pub rules: bool,
947 #[serde(default)]
949 pub skill_approval: bool,
950 #[serde(default)]
952 pub request_permissions: bool,
953 pub mcp_elicitations: bool,
955}
956
957impl GranularApprovalConfig {
958 pub const fn allows_sandbox_approval(self) -> bool {
959 self.sandbox_approval
960 }
961
962 pub const fn allows_rules_approval(self) -> bool {
963 self.rules
964 }
965
966 pub const fn allows_skill_approval(self) -> bool {
967 self.skill_approval
968 }
969
970 pub const fn allows_request_permissions(self) -> bool {
971 self.request_permissions
972 }
973
974 pub const fn allows_mcp_elicitations(self) -> bool {
975 self.mcp_elicitations
976 }
977}
978
979#[derive(
981 Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, Default, JsonSchema, TS,
982)]
983#[serde(rename_all = "kebab-case")]
984#[strum(serialize_all = "kebab-case")]
985pub enum NetworkAccess {
986 #[default]
987 Restricted,
988 Enabled,
989}
990
991impl NetworkAccess {
992 pub fn is_enabled(self) -> bool {
993 matches!(self, NetworkAccess::Enabled)
994 }
995}
996
997#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, JsonSchema, TS)]
999#[strum(serialize_all = "kebab-case")]
1000#[serde(tag = "type", rename_all = "kebab-case")]
1001pub enum SandboxPolicy {
1002 #[serde(rename = "danger-full-access")]
1004 DangerFullAccess,
1005
1006 #[serde(rename = "read-only")]
1008 ReadOnly {
1009 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1012 network_access: bool,
1013 },
1014
1015 #[serde(rename = "external-sandbox")]
1018 ExternalSandbox {
1019 #[serde(default)]
1021 network_access: NetworkAccess,
1022 },
1023
1024 #[serde(rename = "workspace-write")]
1027 WorkspaceWrite {
1028 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1031 writable_roots: Vec<AbsolutePathBuf>,
1032
1033 #[serde(default)]
1036 network_access: bool,
1037
1038 #[serde(default)]
1042 exclude_tmpdir_env_var: bool,
1043
1044 #[serde(default)]
1047 exclude_slash_tmp: bool,
1048 },
1049}
1050
1051#[derive(Debug, Clone, PartialEq, Eq, JsonSchema)]
1057pub struct WritableRoot {
1058 pub root: AbsolutePathBuf,
1059
1060 pub read_only_subpaths: Vec<AbsolutePathBuf>,
1062
1063 pub protected_metadata_names: Vec<String>,
1067}
1068
1069impl WritableRoot {
1070 pub fn is_path_writable(&self, path: &Path) -> bool {
1071 if !path.starts_with(&self.root) {
1073 return false;
1074 }
1075
1076 for subpath in &self.read_only_subpaths {
1078 if path.starts_with(subpath) {
1079 return false;
1080 }
1081 }
1082
1083 if self.path_contains_protected_metadata_name(path) {
1084 return false;
1085 }
1086
1087 true
1088 }
1089
1090 fn path_contains_protected_metadata_name(&self, path: &Path) -> bool {
1091 let Ok(relative_path) = path.strip_prefix(&self.root) else {
1092 return false;
1093 };
1094
1095 let Some(first_component) = relative_path.components().next() else {
1096 return false;
1097 };
1098
1099 self.protected_metadata_names
1100 .iter()
1101 .any(|name| first_component.as_os_str() == std::ffi::OsStr::new(name))
1102 }
1103}
1104
1105impl FromStr for SandboxPolicy {
1106 type Err = serde_json::Error;
1107
1108 fn from_str(s: &str) -> Result<Self, Self::Err> {
1109 serde_json::from_str(s)
1110 }
1111}
1112
1113impl FromStr for FileSystemSandboxPolicy {
1114 type Err = serde_json::Error;
1115
1116 fn from_str(s: &str) -> Result<Self, Self::Err> {
1117 serde_json::from_str(s)
1118 }
1119}
1120
1121impl FromStr for NetworkSandboxPolicy {
1122 type Err = serde_json::Error;
1123
1124 fn from_str(s: &str) -> Result<Self, Self::Err> {
1125 serde_json::from_str(s)
1126 }
1127}
1128
1129impl SandboxPolicy {
1130 pub fn new_read_only_policy() -> Self {
1132 SandboxPolicy::ReadOnly {
1133 network_access: false,
1134 }
1135 }
1136
1137 pub fn new_workspace_write_policy() -> Self {
1141 SandboxPolicy::WorkspaceWrite {
1142 writable_roots: vec![],
1143 network_access: false,
1144 exclude_tmpdir_env_var: false,
1145 exclude_slash_tmp: false,
1146 }
1147 }
1148
1149 pub fn has_full_disk_read_access(&self) -> bool {
1150 true
1151 }
1152
1153 pub fn has_full_disk_write_access(&self) -> bool {
1154 match self {
1155 SandboxPolicy::DangerFullAccess => true,
1156 SandboxPolicy::ExternalSandbox { .. } => true,
1157 SandboxPolicy::ReadOnly { .. } => false,
1158 SandboxPolicy::WorkspaceWrite { .. } => false,
1159 }
1160 }
1161
1162 pub fn has_full_network_access(&self) -> bool {
1163 match self {
1164 SandboxPolicy::DangerFullAccess => true,
1165 SandboxPolicy::ExternalSandbox { network_access } => network_access.is_enabled(),
1166 SandboxPolicy::ReadOnly { network_access, .. } => *network_access,
1167 SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access,
1168 }
1169 }
1170
1171 pub fn get_writable_roots_with_cwd(&self, cwd: &Path) -> Vec<WritableRoot> {
1175 match self {
1176 SandboxPolicy::DangerFullAccess => Vec::new(),
1177 SandboxPolicy::ExternalSandbox { .. } => Vec::new(),
1178 SandboxPolicy::ReadOnly { .. } => Vec::new(),
1179 SandboxPolicy::WorkspaceWrite {
1180 writable_roots,
1181 exclude_tmpdir_env_var,
1182 exclude_slash_tmp,
1183 network_access: _,
1184 } => {
1185 let mut roots: Vec<AbsolutePathBuf> = writable_roots.clone();
1187
1188 let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd);
1192 match cwd_absolute {
1193 Ok(cwd) => {
1194 roots.push(cwd);
1195 }
1196 Err(e) => {
1197 error!(
1198 "Ignoring invalid cwd {:?} for sandbox writable root: {}",
1199 cwd, e
1200 );
1201 }
1202 }
1203
1204 if cfg!(unix) && !exclude_slash_tmp {
1206 match AbsolutePathBuf::from_absolute_path("/tmp") {
1207 Ok(slash_tmp) => {
1208 if slash_tmp.as_path().is_dir() {
1209 roots.push(slash_tmp);
1210 }
1211 }
1212 Err(e) => {
1213 error!("Ignoring invalid /tmp for sandbox writable root: {e}");
1214 }
1215 }
1216 }
1217
1218 if !exclude_tmpdir_env_var
1227 && let Some(tmpdir) = std::env::var_os("TMPDIR")
1228 && !tmpdir.is_empty()
1229 {
1230 match AbsolutePathBuf::from_absolute_path(PathBuf::from(&tmpdir)) {
1231 Ok(tmpdir_path) => {
1232 roots.push(tmpdir_path);
1233 }
1234 Err(e) => {
1235 error!(
1236 "Ignoring invalid TMPDIR value {tmpdir:?} for sandbox writable root: {e}",
1237 );
1238 }
1239 }
1240 }
1241
1242 let cwd_root = AbsolutePathBuf::from_absolute_path(cwd).ok();
1244 roots
1245 .into_iter()
1246 .map(|writable_root| {
1247 let protect_missing_dot_codex = cwd_root
1248 .as_ref()
1249 .is_some_and(|cwd_root| cwd_root == &writable_root);
1250 WritableRoot {
1251 read_only_subpaths: default_read_only_subpaths_for_writable_root(
1252 &writable_root,
1253 protect_missing_dot_codex,
1254 ),
1255 protected_metadata_names: Vec::new(),
1256 root: writable_root,
1257 }
1258 })
1259 .collect()
1260 }
1261 }
1262 }
1263}
1264
1265#[derive(Debug, Clone, Deserialize, Serialize)]
1267pub struct Event {
1268 pub id: String,
1270 pub msg: EventMsg,
1272}
1273
1274#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1275pub struct EnvironmentConnectionEvent {
1276 pub environment_id: String,
1277}
1278
1279#[derive(Debug, Clone, Deserialize, Serialize, Display, JsonSchema, TS)]
1282#[serde(tag = "type", rename_all = "snake_case")]
1283#[ts(tag = "type")]
1284#[strum(serialize_all = "snake_case")]
1285pub enum EventMsg {
1286 Error(ErrorEvent),
1288
1289 Warning(WarningEvent),
1292
1293 GuardianWarning(WarningEvent),
1295
1296 RealtimeConversationStarted(RealtimeConversationStartedEvent),
1298
1299 RealtimeConversationRealtime(RealtimeConversationRealtimeEvent),
1301
1302 RealtimeConversationClosed(RealtimeConversationClosedEvent),
1304
1305 RealtimeConversationSdp(RealtimeConversationSdpEvent),
1307
1308 ModelReroute(ModelRerouteEvent),
1310
1311 ModelVerification(ModelVerificationEvent),
1313
1314 TurnModerationMetadata(TurnModerationMetadataEvent),
1316
1317 SafetyBuffering(SafetyBufferingEvent),
1319
1320 ContextCompacted(ContextCompactedEvent),
1322
1323 ThreadRolledBack(ThreadRolledBackEvent),
1325
1326 #[serde(rename = "task_started", alias = "turn_started")]
1329 TurnStarted(TurnStartedEvent),
1330
1331 ThreadSettingsApplied(ThreadSettingsAppliedEvent),
1334
1335 #[serde(rename = "task_complete", alias = "turn_complete")]
1338 TurnComplete(TurnCompleteEvent),
1339
1340 TokenCount(TokenCountEvent),
1343
1344 AgentMessage(AgentMessageEvent),
1346
1347 UserMessage(UserMessageEvent),
1349
1350 AgentReasoning(AgentReasoningEvent),
1352
1353 AgentReasoningRawContent(AgentReasoningRawContentEvent),
1355
1356 AgentReasoningSectionBreak(AgentReasoningSectionBreakEvent),
1358
1359 SessionConfigured(SessionConfiguredEvent),
1361
1362 EnvironmentConnected(EnvironmentConnectionEvent),
1364
1365 EnvironmentDisconnected(EnvironmentConnectionEvent),
1367
1368 ThreadGoalUpdated(ThreadGoalUpdatedEvent),
1370
1371 McpStartupUpdate(McpStartupUpdateEvent),
1373
1374 McpStartupComplete(McpStartupCompleteEvent),
1376
1377 McpToolCallBegin(McpToolCallBeginEvent),
1378
1379 McpToolCallEnd(McpToolCallEndEvent),
1380
1381 WebSearchBegin(WebSearchBeginEvent),
1382
1383 WebSearchEnd(WebSearchEndEvent),
1384
1385 ImageGenerationBegin(ImageGenerationBeginEvent),
1386
1387 ImageGenerationEnd(ImageGenerationEndEvent),
1388
1389 ExecCommandBegin(ExecCommandBeginEvent),
1391
1392 ExecCommandOutputDelta(ExecCommandOutputDeltaEvent),
1394
1395 TerminalInteraction(TerminalInteractionEvent),
1397
1398 ExecCommandEnd(ExecCommandEndEvent),
1399
1400 ViewImageToolCall(ViewImageToolCallEvent),
1402
1403 ExecApprovalRequest(ExecApprovalRequestEvent),
1404
1405 RequestPermissions(RequestPermissionsEvent),
1406
1407 RequestUserInput(RequestUserInputEvent),
1408
1409 DynamicToolCallRequest(DynamicToolCallRequest),
1410
1411 DynamicToolCallResponse(DynamicToolCallResponseEvent),
1412
1413 ElicitationRequest(ElicitationRequestEvent),
1414
1415 ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent),
1416
1417 GuardianAssessment(GuardianAssessmentEvent),
1419
1420 DeprecationNotice(DeprecationNoticeEvent),
1423
1424 StreamError(StreamErrorEvent),
1427
1428 PatchApplyBegin(PatchApplyBeginEvent),
1431
1432 PatchApplyUpdated(PatchApplyUpdatedEvent),
1434
1435 PatchApplyEnd(PatchApplyEndEvent),
1437
1438 TurnDiff(TurnDiffEvent),
1439
1440 RealtimeConversationListVoicesResponse(RealtimeConversationListVoicesResponseEvent),
1442
1443 PlanUpdate(UpdatePlanArgs),
1444
1445 TurnAborted(TurnAbortedEvent),
1446
1447 ShutdownComplete,
1449
1450 EnteredReviewMode(EnteredReviewModeEvent),
1452
1453 ExitedReviewMode(ExitedReviewModeEvent),
1455
1456 RawResponseItem(RawResponseItemEvent),
1457 RawResponseCompleted(RawResponseCompletedEvent),
1458
1459 ItemStarted(ItemStartedEvent),
1460 ItemCompleted(ItemCompletedEvent),
1461 HookStarted(HookStartedEvent),
1462 HookCompleted(HookCompletedEvent),
1463
1464 AgentMessageContentDelta(AgentMessageContentDeltaEvent),
1465 PlanDelta(PlanDeltaEvent),
1466 ReasoningContentDelta(ReasoningContentDeltaEvent),
1467 ReasoningRawContentDelta(ReasoningRawContentDeltaEvent),
1468
1469 CollabAgentSpawnBegin(CollabAgentSpawnBeginEvent),
1471 CollabAgentSpawnEnd(CollabAgentSpawnEndEvent),
1473 CollabAgentInteractionBegin(CollabAgentInteractionBeginEvent),
1475 CollabAgentInteractionEnd(CollabAgentInteractionEndEvent),
1477 CollabWaitingBegin(CollabWaitingBeginEvent),
1479 CollabWaitingEnd(CollabWaitingEndEvent),
1481 CollabCloseBegin(CollabCloseBeginEvent),
1483 CollabCloseEnd(CollabCloseEndEvent),
1485 CollabResumeBegin(CollabResumeBeginEvent),
1487 CollabResumeEnd(CollabResumeEndEvent),
1489
1490 SubAgentActivity(SubAgentActivityEvent),
1492}
1493
1494#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS, EnumIter)]
1495#[serde(rename_all = "snake_case")]
1496pub enum HookEventName {
1497 PreToolUse,
1498 PermissionRequest,
1499 PostToolUse,
1500 PreCompact,
1501 PostCompact,
1502 SessionStart,
1503 SessionEnd,
1504 UserPromptSubmit,
1505 SubagentStart,
1506 SubagentStop,
1507 Stop,
1508}
1509
1510#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1511#[serde(rename_all = "snake_case")]
1512pub enum HookHandlerType {
1513 Command,
1514 Prompt,
1515 Agent,
1516}
1517
1518#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1519#[serde(rename_all = "snake_case")]
1520pub enum HookExecutionMode {
1521 Sync,
1522 Async,
1523}
1524
1525#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1526#[serde(rename_all = "snake_case")]
1527pub enum HookScope {
1528 Thread,
1529 Turn,
1530}
1531
1532#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1533#[serde(rename_all = "snake_case")]
1534pub enum HookSource {
1535 System,
1536 User,
1537 Project,
1538 Mdm,
1539 SessionFlags,
1540 Plugin,
1541 CloudRequirements,
1542 CloudManagedConfig,
1543 LegacyManagedConfigFile,
1544 LegacyManagedConfigMdm,
1545 #[default]
1546 Unknown,
1547}
1548
1549#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1550#[serde(rename_all = "snake_case")]
1551pub enum HookTrustStatus {
1552 Managed,
1553 Untrusted,
1554 Trusted,
1555 Modified,
1556}
1557
1558#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1559#[serde(rename_all = "snake_case")]
1560pub enum HookRunStatus {
1561 Running,
1562 Completed,
1563 Failed,
1564 Blocked,
1565 Stopped,
1566}
1567
1568#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1569#[serde(rename_all = "snake_case")]
1570pub enum HookOutputEntryKind {
1571 Warning,
1572 Stop,
1573 Feedback,
1574 Context,
1575 Error,
1576}
1577
1578#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1579#[serde(rename_all = "snake_case")]
1580pub struct HookOutputEntry {
1581 pub kind: HookOutputEntryKind,
1582 pub text: String,
1583}
1584
1585#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1586#[serde(rename_all = "snake_case")]
1587pub struct HookRunSummary {
1588 pub id: String,
1589 pub event_name: HookEventName,
1590 pub handler_type: HookHandlerType,
1591 pub execution_mode: HookExecutionMode,
1592 pub scope: HookScope,
1593 pub source_path: AbsolutePathBuf,
1594 #[serde(default)]
1595 pub source: HookSource,
1596 pub display_order: i64,
1597 pub status: HookRunStatus,
1598 pub status_message: Option<String>,
1599 #[ts(type = "number")]
1600 pub started_at: i64,
1601 #[ts(type = "number | null")]
1602 pub completed_at: Option<i64>,
1603 #[ts(type = "number | null")]
1604 pub duration_ms: Option<i64>,
1605 pub entries: Vec<HookOutputEntry>,
1606}
1607
1608#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1609#[serde(rename_all = "snake_case")]
1610pub struct HookStartedEvent {
1611 pub turn_id: Option<String>,
1612 pub run: HookRunSummary,
1613}
1614
1615#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1616#[serde(rename_all = "snake_case")]
1617pub struct HookCompletedEvent {
1618 pub turn_id: Option<String>,
1619 pub run: HookRunSummary,
1620}
1621
1622#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1623#[serde(rename_all = "snake_case")]
1624pub enum RealtimeConversationVersion {
1625 V1,
1626 #[default]
1627 V2,
1628 V3,
1629}
1630
1631#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1632#[serde(rename_all = "camelCase")]
1633#[ts(rename_all = "camelCase")]
1634pub enum CodexResponseHandoffMode {
1635 #[default]
1636 Thinking,
1637 Commentary,
1638 BemTags,
1639}
1640
1641#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1642pub struct RealtimeConversationStartedEvent {
1643 pub realtime_session_id: Option<String>,
1644 pub version: RealtimeConversationVersion,
1645}
1646
1647#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1648pub struct RealtimeConversationRealtimeEvent {
1649 pub payload: RealtimeEvent,
1650}
1651
1652#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1653pub struct RealtimeConversationClosedEvent {
1654 #[serde(skip_serializing_if = "Option::is_none")]
1655 pub reason: Option<String>,
1656}
1657
1658#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1659pub struct RealtimeConversationSdpEvent {
1660 pub sdp: String,
1661}
1662
1663impl From<CollabAgentSpawnBeginEvent> for EventMsg {
1664 fn from(event: CollabAgentSpawnBeginEvent) -> Self {
1665 EventMsg::CollabAgentSpawnBegin(event)
1666 }
1667}
1668
1669impl From<CollabAgentSpawnEndEvent> for EventMsg {
1670 fn from(event: CollabAgentSpawnEndEvent) -> Self {
1671 EventMsg::CollabAgentSpawnEnd(event)
1672 }
1673}
1674
1675impl From<CollabAgentInteractionBeginEvent> for EventMsg {
1676 fn from(event: CollabAgentInteractionBeginEvent) -> Self {
1677 EventMsg::CollabAgentInteractionBegin(event)
1678 }
1679}
1680
1681impl From<CollabAgentInteractionEndEvent> for EventMsg {
1682 fn from(event: CollabAgentInteractionEndEvent) -> Self {
1683 EventMsg::CollabAgentInteractionEnd(event)
1684 }
1685}
1686
1687impl From<CollabWaitingBeginEvent> for EventMsg {
1688 fn from(event: CollabWaitingBeginEvent) -> Self {
1689 EventMsg::CollabWaitingBegin(event)
1690 }
1691}
1692
1693impl From<CollabWaitingEndEvent> for EventMsg {
1694 fn from(event: CollabWaitingEndEvent) -> Self {
1695 EventMsg::CollabWaitingEnd(event)
1696 }
1697}
1698
1699impl From<CollabCloseBeginEvent> for EventMsg {
1700 fn from(event: CollabCloseBeginEvent) -> Self {
1701 EventMsg::CollabCloseBegin(event)
1702 }
1703}
1704
1705impl From<CollabCloseEndEvent> for EventMsg {
1706 fn from(event: CollabCloseEndEvent) -> Self {
1707 EventMsg::CollabCloseEnd(event)
1708 }
1709}
1710
1711impl From<CollabResumeBeginEvent> for EventMsg {
1712 fn from(event: CollabResumeBeginEvent) -> Self {
1713 EventMsg::CollabResumeBegin(event)
1714 }
1715}
1716
1717impl From<CollabResumeEndEvent> for EventMsg {
1718 fn from(event: CollabResumeEndEvent) -> Self {
1719 EventMsg::CollabResumeEnd(event)
1720 }
1721}
1722
1723impl From<SubAgentActivityEvent> for EventMsg {
1724 fn from(event: SubAgentActivityEvent) -> Self {
1725 EventMsg::SubAgentActivity(event)
1726 }
1727}
1728
1729#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS, Default)]
1731#[serde(rename_all = "snake_case")]
1732#[ts(rename_all = "snake_case")]
1733pub enum AgentStatus {
1734 #[default]
1736 PendingInit,
1737 Running,
1739 Interrupted,
1741 Completed(Option<String>),
1743 Errored(String),
1745 Shutdown,
1747 NotFound,
1749}
1750
1751#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema, TS)]
1753#[serde(rename_all = "snake_case")]
1754#[ts(rename_all = "snake_case")]
1755pub enum NonSteerableTurnKind {
1756 Review,
1757 Compact,
1758}
1759
1760#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
1762#[serde(rename_all = "snake_case")]
1763#[ts(rename_all = "snake_case")]
1764pub enum CodexErrorInfo {
1765 ContextWindowExceeded,
1766 SessionBudgetExceeded,
1767 UsageLimitExceeded,
1768 ServerOverloaded,
1769 CyberPolicy,
1770 HttpConnectionFailed {
1771 http_status_code: Option<u16>,
1772 },
1773 ResponseStreamConnectionFailed {
1775 http_status_code: Option<u16>,
1776 },
1777 InternalServerError,
1778 Unauthorized,
1779 BadRequest,
1780 SandboxError,
1781 ResponseStreamDisconnected {
1783 http_status_code: Option<u16>,
1784 },
1785 ResponseTooManyFailedAttempts {
1787 http_status_code: Option<u16>,
1788 },
1789 ActiveTurnNotSteerable {
1792 turn_kind: NonSteerableTurnKind,
1793 },
1794 ThreadRollbackFailed,
1795 Other,
1796}
1797
1798impl CodexErrorInfo {
1799 pub fn affects_turn_status(&self) -> bool {
1801 match self {
1802 Self::ThreadRollbackFailed | Self::ActiveTurnNotSteerable { .. } => false,
1803 Self::ContextWindowExceeded
1804 | Self::SessionBudgetExceeded
1805 | Self::UsageLimitExceeded
1806 | Self::ServerOverloaded
1807 | Self::CyberPolicy
1808 | Self::HttpConnectionFailed { .. }
1809 | Self::ResponseStreamConnectionFailed { .. }
1810 | Self::InternalServerError
1811 | Self::Unauthorized
1812 | Self::BadRequest
1813 | Self::SandboxError
1814 | Self::ResponseStreamDisconnected { .. }
1815 | Self::ResponseTooManyFailedAttempts { .. }
1816 | Self::Other => true,
1817 }
1818 }
1819}
1820
1821#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1822pub struct RawResponseItemEvent {
1823 pub item: ResponseItem,
1824}
1825
1826#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1830pub struct RawResponseCompletedEvent {
1831 pub response_id: String,
1832 pub token_usage: Option<TokenUsage>,
1833}
1834
1835#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1836pub struct ItemStartedEvent {
1837 pub thread_id: ThreadId,
1838 pub turn_id: String,
1839 pub item: TurnItem,
1840 pub started_at_ms: i64,
1841}
1842
1843#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1844pub struct ItemCompletedEvent {
1845 pub thread_id: ThreadId,
1846 pub turn_id: String,
1847 pub item: TurnItem,
1848 #[serde(default = "default_item_completed_at_ms")]
1852 pub completed_at_ms: i64,
1853}
1854
1855const fn default_item_completed_at_ms() -> i64 {
1856 0
1857}
1858
1859#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1860pub struct AgentMessageContentDeltaEvent {
1861 pub thread_id: String,
1862 pub turn_id: String,
1863 pub item_id: String,
1864 pub delta: String,
1865}
1866
1867#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1868pub struct PlanDeltaEvent {
1869 pub thread_id: String,
1870 pub turn_id: String,
1871 pub item_id: String,
1872 pub delta: String,
1873}
1874
1875#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1876pub struct ReasoningContentDeltaEvent {
1877 pub thread_id: String,
1878 pub turn_id: String,
1879 pub item_id: String,
1880 pub delta: String,
1881 #[serde(default)]
1883 pub summary_index: i64,
1884}
1885
1886#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1887pub struct ReasoningRawContentDeltaEvent {
1888 pub thread_id: String,
1889 pub turn_id: String,
1890 pub item_id: String,
1891 pub delta: String,
1892 #[serde(default)]
1894 pub content_index: i64,
1895}
1896
1897#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1898pub struct EnteredReviewModeEvent {
1899 pub target: ReviewTarget,
1900 #[serde(skip_serializing_if = "Option::is_none")]
1901 #[ts(optional)]
1902 pub user_facing_hint: Option<String>,
1903 #[serde(default, skip_serializing_if = "Option::is_none")]
1904 #[ts(optional)]
1905 pub turn_id: Option<String>,
1906 #[serde(default, skip_serializing_if = "Option::is_none")]
1907 #[ts(optional)]
1908 pub item_id: Option<String>,
1909}
1910
1911#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1912pub struct ExitedReviewModeEvent {
1913 #[serde(default, skip_serializing_if = "Option::is_none")]
1914 #[ts(optional)]
1915 pub turn_id: Option<String>,
1916 #[serde(default, skip_serializing_if = "Option::is_none")]
1917 #[ts(optional)]
1918 pub item_id: Option<String>,
1919 pub review_output: Option<ReviewOutputEvent>,
1920}
1921
1922#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1925pub struct ErrorEvent {
1926 pub message: String,
1927 #[serde(default)]
1928 pub codex_error_info: Option<CodexErrorInfo>,
1929}
1930
1931impl ErrorEvent {
1932 pub fn affects_turn_status(&self) -> bool {
1934 self.codex_error_info
1935 .as_ref()
1936 .is_none_or(CodexErrorInfo::affects_turn_status)
1937 }
1938}
1939
1940#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1941pub struct WarningEvent {
1942 pub message: String,
1943}
1944
1945#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1946#[serde(rename_all = "snake_case")]
1947#[ts(rename_all = "snake_case")]
1948pub enum ModelRerouteReason {
1949 HighRiskCyberActivity,
1950}
1951
1952#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1953pub struct ModelRerouteEvent {
1954 pub from_model: String,
1955 pub to_model: String,
1956 pub reason: ModelRerouteReason,
1957}
1958
1959#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1960#[serde(rename_all = "snake_case")]
1961#[ts(rename_all = "snake_case")]
1962pub enum ModelVerification {
1963 TrustedAccessForCyber,
1964}
1965
1966#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1967pub struct ModelVerificationEvent {
1968 pub verifications: Vec<ModelVerification>,
1969}
1970
1971#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1972pub struct TurnModerationMetadataEvent {
1973 pub metadata: Value,
1974}
1975
1976#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1977pub struct SafetyBufferingEvent {
1978 pub model: String,
1979 pub use_cases: Vec<String>,
1980 pub reasons: Vec<String>,
1981 pub show_buffering_ui: bool,
1982 pub faster_model: Option<String>,
1983}
1984
1985#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1986pub struct ContextCompactedEvent;
1987
1988#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1989pub struct TurnCompleteEvent {
1990 pub turn_id: String,
1991 pub last_agent_message: Option<String>,
1992 #[serde(default, skip_serializing_if = "Option::is_none")]
1994 #[ts(optional)]
1995 pub error: Option<ErrorEvent>,
1996 #[serde(default, skip_serializing_if = "Option::is_none")]
1998 #[ts(type = "number | null", optional)]
1999 pub started_at: Option<i64>,
2000 #[serde(default, skip_serializing_if = "Option::is_none")]
2002 #[ts(type = "number | null", optional)]
2003 pub completed_at: Option<i64>,
2004 #[serde(default, skip_serializing_if = "Option::is_none")]
2006 #[ts(type = "number | null", optional)]
2007 pub duration_ms: Option<i64>,
2008 #[serde(default, skip_serializing_if = "Option::is_none")]
2010 #[ts(type = "number | null", optional)]
2011 pub time_to_first_token_ms: Option<i64>,
2012}
2013
2014#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2015pub struct TurnStartedEvent {
2016 pub turn_id: String,
2017 #[serde(default, skip_serializing_if = "Option::is_none")]
2019 #[ts(optional)]
2020 pub trace_id: Option<String>,
2021 #[serde(default, skip_serializing_if = "Option::is_none")]
2023 #[ts(type = "number | null", optional)]
2024 pub started_at: Option<i64>,
2025 pub model_context_window: Option<i64>,
2027 #[serde(default)]
2028 pub collaboration_mode_kind: ModeKind,
2029}
2030
2031#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2032pub struct ThreadSettingsAppliedEvent {
2033 pub thread_settings: ThreadSettingsSnapshot,
2034}
2035
2036#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2037pub struct ThreadSettingsSnapshot {
2038 pub model: String,
2039 pub model_provider_id: String,
2040 #[serde(skip_serializing_if = "Option::is_none")]
2041 pub service_tier: Option<String>,
2042 pub approval_policy: AskForApproval,
2043 pub approvals_reviewer: ApprovalsReviewer,
2044 pub permission_profile: PermissionProfile,
2045 #[serde(default, skip_serializing_if = "Option::is_none")]
2046 #[ts(optional)]
2047 pub active_permission_profile: Option<ActivePermissionProfile>,
2048 pub cwd: AbsolutePathBuf,
2049 #[serde(skip_serializing_if = "Option::is_none")]
2050 pub reasoning_effort: Option<ReasoningEffortConfig>,
2051 #[serde(skip_serializing_if = "Option::is_none")]
2052 pub reasoning_summary: Option<ReasoningSummaryConfig>,
2053 #[serde(skip_serializing_if = "Option::is_none")]
2054 pub personality: Option<Personality>,
2055 pub collaboration_mode: CollaborationMode,
2056}
2057
2058#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, TS)]
2059pub struct TokenUsage {
2060 #[ts(type = "number")]
2061 pub input_tokens: i64,
2062 #[ts(type = "number")]
2063 pub cached_input_tokens: i64,
2064 #[serde(default)]
2065 #[ts(type = "number")]
2066 pub cache_write_input_tokens: i64,
2067 #[ts(type = "number")]
2068 pub output_tokens: i64,
2069 #[ts(type = "number")]
2070 pub reasoning_output_tokens: i64,
2071 #[ts(type = "number")]
2072 pub total_tokens: i64,
2073}
2074
2075#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
2076pub struct TokenUsageInfo {
2077 pub total_token_usage: TokenUsage,
2078 pub last_token_usage: TokenUsage,
2079 #[ts(type = "number | null")]
2081 pub model_context_window: Option<i64>,
2082}
2083
2084impl TokenUsageInfo {
2085 pub fn new_or_append(
2086 info: &Option<TokenUsageInfo>,
2087 last: &Option<TokenUsage>,
2088 model_context_window: Option<i64>,
2089 ) -> Option<Self> {
2090 if info.is_none() && last.is_none() {
2091 return None;
2092 }
2093
2094 let mut info = match info {
2095 Some(info) => info.clone(),
2096 None => Self {
2097 total_token_usage: TokenUsage::default(),
2098 last_token_usage: TokenUsage::default(),
2099 model_context_window,
2100 },
2101 };
2102 if let Some(last) = last {
2103 info.append_last_usage(last);
2104 }
2105 if let Some(model_context_window) = model_context_window {
2106 info.model_context_window = Some(model_context_window);
2107 }
2108 Some(info)
2109 }
2110
2111 pub fn append_last_usage(&mut self, last: &TokenUsage) {
2112 self.total_token_usage.add_assign(last);
2113 self.last_token_usage = last.clone();
2114 }
2115
2116 pub fn fill_to_context_window(&mut self, context_window: i64) {
2117 let previous_total = self.total_token_usage.total_tokens;
2118 let delta = (context_window - previous_total).max(0);
2119
2120 self.model_context_window = Some(context_window);
2121 self.total_token_usage = TokenUsage {
2122 total_tokens: context_window,
2123 ..TokenUsage::default()
2124 };
2125 self.last_token_usage = TokenUsage {
2126 total_tokens: delta,
2127 ..TokenUsage::default()
2128 };
2129 }
2130
2131 pub fn full_context_window(context_window: i64) -> Self {
2132 let mut info = Self {
2133 total_token_usage: TokenUsage::default(),
2134 last_token_usage: TokenUsage::default(),
2135 model_context_window: Some(context_window),
2136 };
2137 info.fill_to_context_window(context_window);
2138 info
2139 }
2140}
2141
2142#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2143pub struct TokenCountEvent {
2144 pub info: Option<TokenUsageInfo>,
2145 pub rate_limits: Option<RateLimitSnapshot>,
2146}
2147
2148#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
2149pub struct RateLimitSnapshot {
2150 pub limit_id: Option<String>,
2151 pub limit_name: Option<String>,
2152 pub primary: Option<RateLimitWindow>,
2153 pub secondary: Option<RateLimitWindow>,
2154 pub credits: Option<CreditsSnapshot>,
2155 pub individual_limit: Option<SpendControlLimitSnapshot>,
2156 pub spend_control_reached: Option<bool>,
2158 pub plan_type: Option<crate::account::PlanType>,
2159 pub rate_limit_reached_type: Option<RateLimitReachedType>,
2160}
2161
2162#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema, TS)]
2163#[serde(rename_all = "snake_case")]
2164#[ts(rename_all = "snake_case")]
2165pub enum RateLimitReachedType {
2166 RateLimitReached,
2167 WorkspaceOwnerCreditsDepleted,
2168 WorkspaceMemberCreditsDepleted,
2169 WorkspaceOwnerUsageLimitReached,
2170 WorkspaceMemberUsageLimitReached,
2171}
2172
2173impl FromStr for RateLimitReachedType {
2174 type Err = String;
2175
2176 fn from_str(value: &str) -> Result<Self, Self::Err> {
2177 match value {
2178 "rate_limit_reached" => Ok(Self::RateLimitReached),
2179 "workspace_owner_credits_depleted" => Ok(Self::WorkspaceOwnerCreditsDepleted),
2180 "workspace_member_credits_depleted" => Ok(Self::WorkspaceMemberCreditsDepleted),
2181 "workspace_owner_usage_limit_reached" => Ok(Self::WorkspaceOwnerUsageLimitReached),
2182 "workspace_member_usage_limit_reached" => Ok(Self::WorkspaceMemberUsageLimitReached),
2183 other => Err(format!("unknown rate limit reached type: {other}")),
2184 }
2185 }
2186}
2187
2188#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
2189pub struct RateLimitWindow {
2190 pub used_percent: f64,
2192 #[ts(type = "number | null")]
2194 pub window_minutes: Option<i64>,
2195 #[ts(type = "number | null")]
2197 pub resets_at: Option<i64>,
2198}
2199
2200#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
2201pub struct CreditsSnapshot {
2202 pub has_credits: bool,
2203 pub unlimited: bool,
2204 pub balance: Option<String>,
2205}
2206
2207#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
2208pub struct SpendControlLimitSnapshot {
2209 pub limit: String,
2210 pub used: String,
2211 pub remaining_percent: i32,
2212 pub resets_at: i64,
2213}
2214
2215const BASELINE_TOKENS: i64 = 12000;
2217
2218impl TokenUsage {
2219 pub fn is_zero(&self) -> bool {
2220 self.total_tokens == 0
2221 }
2222
2223 pub fn cached_input(&self) -> i64 {
2224 self.cached_input_tokens.max(0)
2225 }
2226
2227 pub fn non_cached_input(&self) -> i64 {
2228 (self.input_tokens - self.cached_input()).max(0)
2229 }
2230
2231 pub fn blended_total(&self) -> i64 {
2233 (self.non_cached_input() + self.output_tokens.max(0)).max(0)
2234 }
2235
2236 pub fn tokens_in_context_window(&self) -> i64 {
2237 self.total_tokens
2238 }
2239
2240 pub fn percent_of_context_window_remaining(&self, context_window: i64) -> i64 {
2251 if context_window <= BASELINE_TOKENS {
2252 return 0;
2253 }
2254
2255 let effective_window = context_window - BASELINE_TOKENS;
2256 let used = (self.tokens_in_context_window() - BASELINE_TOKENS).max(0);
2257 let remaining = (effective_window - used).max(0);
2258 ((remaining as f64 / effective_window as f64) * 100.0)
2259 .clamp(0.0, 100.0)
2260 .round() as i64
2261 }
2262
2263 pub fn add_assign(&mut self, other: &TokenUsage) {
2265 self.input_tokens += other.input_tokens;
2266 self.cached_input_tokens += other.cached_input_tokens;
2267 self.cache_write_input_tokens += other.cache_write_input_tokens;
2268 self.output_tokens += other.output_tokens;
2269 self.reasoning_output_tokens += other.reasoning_output_tokens;
2270 self.total_tokens += other.total_tokens;
2271 }
2272}
2273
2274#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
2275pub struct FinalOutput {
2276 pub token_usage: TokenUsage,
2277}
2278
2279impl From<TokenUsage> for FinalOutput {
2280 fn from(token_usage: TokenUsage) -> Self {
2281 Self { token_usage }
2282 }
2283}
2284
2285impl fmt::Display for FinalOutput {
2286 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2287 let token_usage = &self.token_usage;
2288
2289 write!(
2290 f,
2291 "Token usage: total={} input={}{} output={}{}",
2292 format_with_separators(token_usage.blended_total()),
2293 format_with_separators(token_usage.non_cached_input()),
2294 if token_usage.cached_input() > 0 {
2295 format!(
2296 " (+ {} cached)",
2297 format_with_separators(token_usage.cached_input())
2298 )
2299 } else {
2300 String::new()
2301 },
2302 format_with_separators(token_usage.output_tokens),
2303 if token_usage.reasoning_output_tokens > 0 {
2304 format!(
2305 " (reasoning {})",
2306 format_with_separators(token_usage.reasoning_output_tokens)
2307 )
2308 } else {
2309 String::new()
2310 }
2311 )
2312 }
2313}
2314
2315#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2316pub struct AgentMessageEvent {
2317 pub message: String,
2318 #[serde(default)]
2319 pub phase: Option<MessagePhase>,
2320 #[serde(default)]
2321 pub memory_citation: Option<MemoryCitation>,
2322}
2323
2324#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema, TS)]
2325pub struct UserMessageEvent {
2326 #[serde(default, skip_serializing_if = "Option::is_none")]
2327 pub client_id: Option<String>,
2328 pub message: String,
2329 #[serde(skip_serializing_if = "Option::is_none")]
2333 pub images: Option<Vec<String>>,
2334 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2337 pub image_details: Vec<Option<ImageDetail>>,
2338 #[serde(default)]
2343 pub local_images: Vec<std::path::PathBuf>,
2344 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2347 pub local_image_details: Vec<Option<ImageDetail>>,
2348 #[serde(skip_serializing_if = "Option::is_none")]
2351 pub audio: Option<Vec<String>>,
2352 #[serde(default)]
2356 pub local_audio: Vec<std::path::PathBuf>,
2357 #[serde(default)]
2359 pub text_elements: Vec<crate::user_input::TextElement>,
2360}
2361
2362pub fn user_message_preview(user: &UserMessageEvent) -> Option<String> {
2364 let message = strip_user_message_prefix(user.message.as_str());
2365 if !message.is_empty() {
2366 return Some(message.to_string());
2367 }
2368 if user
2369 .images
2370 .as_ref()
2371 .is_some_and(|images| !images.is_empty())
2372 || !user.local_images.is_empty()
2373 {
2374 return Some("[Image]".to_string());
2375 }
2376 if user.audio.as_ref().is_some_and(|audio| !audio.is_empty()) || !user.local_audio.is_empty() {
2377 return Some("[Audio]".to_string());
2378 }
2379 None
2380}
2381
2382#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2383pub struct AgentReasoningEvent {
2384 pub text: String,
2385}
2386
2387#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2388pub struct AgentReasoningRawContentEvent {
2389 pub text: String,
2390}
2391
2392#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2393pub struct AgentReasoningSectionBreakEvent {
2394 #[serde(default)]
2396 pub item_id: String,
2397 #[serde(default)]
2398 pub summary_index: i64,
2399}
2400
2401#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
2402pub struct McpInvocation {
2403 pub server: String,
2405 pub tool: String,
2407 pub arguments: Option<serde_json::Value>,
2409}
2410
2411#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
2412pub struct McpToolCallBeginEvent {
2413 pub call_id: String,
2415 pub invocation: McpInvocation,
2416 #[serde(default, skip_serializing_if = "Option::is_none")]
2417 #[ts(optional)]
2418 pub connector_id: Option<String>,
2419 #[serde(default, skip_serializing_if = "Option::is_none")]
2420 #[ts(optional)]
2421 pub mcp_app_resource_uri: Option<String>,
2422 #[serde(default, skip_serializing_if = "Option::is_none")]
2423 #[ts(optional)]
2424 pub link_id: Option<String>,
2425 #[serde(default, skip_serializing_if = "Option::is_none")]
2426 #[ts(optional)]
2427 pub app_name: Option<String>,
2428 #[serde(default, skip_serializing_if = "Option::is_none")]
2429 #[ts(optional)]
2430 pub action_name: Option<String>,
2431 #[serde(default, skip_serializing_if = "Option::is_none")]
2432 #[ts(optional)]
2433 pub plugin_id: Option<String>,
2434}
2435
2436#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
2437pub struct McpToolCallEndEvent {
2438 pub call_id: String,
2440 pub invocation: McpInvocation,
2441 #[serde(default, skip_serializing_if = "Option::is_none")]
2442 #[ts(optional)]
2443 pub connector_id: Option<String>,
2444 #[serde(default, skip_serializing_if = "Option::is_none")]
2445 #[ts(optional)]
2446 pub mcp_app_resource_uri: Option<String>,
2447 #[serde(default, skip_serializing_if = "Option::is_none")]
2448 #[ts(optional)]
2449 pub link_id: Option<String>,
2450 #[serde(default, skip_serializing_if = "Option::is_none")]
2451 #[ts(optional)]
2452 pub app_name: Option<String>,
2453 #[serde(default, skip_serializing_if = "Option::is_none")]
2454 #[ts(optional)]
2455 pub action_name: Option<String>,
2456 #[serde(default, skip_serializing_if = "Option::is_none")]
2457 #[ts(optional)]
2458 pub plugin_id: Option<String>,
2459 #[ts(type = "string")]
2460 pub duration: Duration,
2461 pub result: Result<CallToolResult, String>,
2463}
2464
2465#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
2466pub struct DynamicToolCallResponseEvent {
2467 pub call_id: String,
2469 pub turn_id: String,
2471 #[serde(default)]
2472 pub completed_at_ms: i64,
2473 #[serde(default)]
2475 pub namespace: Option<String>,
2476 pub tool: String,
2478 pub arguments: serde_json::Value,
2480 pub content_items: Vec<DynamicToolCallOutputContentItem>,
2482 pub success: bool,
2484 pub error: Option<String>,
2486 #[ts(type = "string")]
2488 pub duration: Duration,
2489}
2490
2491impl McpToolCallEndEvent {
2492 pub fn is_success(&self) -> bool {
2493 match &self.result {
2494 Ok(result) => !result.is_error.unwrap_or(false),
2495 Err(_) => false,
2496 }
2497 }
2498}
2499
2500#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2501pub struct WebSearchBeginEvent {
2502 pub call_id: String,
2503}
2504
2505#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2506pub struct WebSearchEndEvent {
2507 pub call_id: String,
2508 pub query: String,
2509 pub action: WebSearchAction,
2510 #[serde(default, skip_serializing_if = "Option::is_none")]
2512 #[ts(optional)]
2513 pub results: Option<Vec<Value>>,
2514}
2515
2516#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2517pub struct ImageGenerationBeginEvent {
2518 pub call_id: String,
2519}
2520
2521#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2522pub struct ImageGenerationEndEvent {
2523 pub call_id: String,
2524 pub status: String,
2525 #[serde(skip_serializing_if = "Option::is_none")]
2526 #[ts(optional)]
2527 pub revised_prompt: Option<String>,
2528 pub result: String,
2529 #[serde(skip_serializing_if = "Option::is_none")]
2530 #[ts(optional)]
2531 pub saved_path: Option<AbsolutePathBuf>,
2532}
2533
2534#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2538pub struct ConversationPathResponseEvent {
2539 pub conversation_id: ThreadId,
2540 pub path: PathBuf,
2541}
2542
2543#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2544pub struct ResumedHistory {
2545 pub conversation_id: ThreadId,
2546 pub history: Arc<Vec<RolloutItem>>,
2547 pub rollout_path: Option<PathBuf>,
2548}
2549
2550#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2551pub enum InitialHistory {
2552 New,
2553 Cleared,
2554 Resumed(ResumedHistory),
2555 Forked(Vec<RolloutItem>),
2556}
2557
2558impl InitialHistory {
2559 pub fn scan_rollout_items(&self, mut predicate: impl FnMut(&RolloutItem) -> bool) -> bool {
2560 match self {
2561 InitialHistory::New | InitialHistory::Cleared => false,
2562 InitialHistory::Resumed(resumed) => resumed.history.iter().any(&mut predicate),
2563 InitialHistory::Forked(items) => items.iter().any(predicate),
2564 }
2565 }
2566
2567 pub fn forked_from_id(&self) -> Option<ThreadId> {
2568 match self {
2569 InitialHistory::New | InitialHistory::Cleared => None,
2570 InitialHistory::Resumed(resumed) => {
2571 resumed.history.iter().find_map(|item| match item {
2572 RolloutItem::SessionMeta(meta_line) => meta_line.meta.forked_from_id,
2573 _ => None,
2574 })
2575 }
2576 InitialHistory::Forked(items) => items.iter().find_map(|item| match item {
2577 RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.id),
2578 _ => None,
2579 }),
2580 }
2581 }
2582
2583 pub fn session_cwd(&self) -> Option<PathBuf> {
2584 match self {
2585 InitialHistory::New | InitialHistory::Cleared => None,
2586 InitialHistory::Resumed(resumed) => session_cwd_from_items(&resumed.history),
2587 InitialHistory::Forked(items) => session_cwd_from_items(items),
2588 }
2589 }
2590
2591 pub fn get_rollout_items(&self) -> &[RolloutItem] {
2592 match self {
2593 InitialHistory::New | InitialHistory::Cleared => &[],
2594 InitialHistory::Resumed(resumed) => &resumed.history,
2595 InitialHistory::Forked(items) => items,
2596 }
2597 }
2598
2599 pub fn get_event_msgs(&self) -> Option<Vec<EventMsg>> {
2600 match self {
2601 InitialHistory::New | InitialHistory::Cleared => None,
2602 InitialHistory::Resumed(resumed) => Some(
2603 resumed
2604 .history
2605 .iter()
2606 .filter_map(|ri| match ri {
2607 RolloutItem::EventMsg(ev) => Some(ev.clone()),
2608 _ => None,
2609 })
2610 .collect(),
2611 ),
2612 InitialHistory::Forked(items) => Some(
2613 items
2614 .iter()
2615 .filter_map(|ri| match ri {
2616 RolloutItem::EventMsg(ev) => Some(ev.clone()),
2617 _ => None,
2618 })
2619 .collect(),
2620 ),
2621 }
2622 }
2623
2624 pub fn get_base_instructions(&self) -> Option<BaseInstructions> {
2625 match self {
2627 InitialHistory::New | InitialHistory::Cleared => None,
2628 InitialHistory::Resumed(resumed) => {
2629 resumed.history.iter().find_map(|item| match item {
2630 RolloutItem::SessionMeta(meta_line) => meta_line.meta.base_instructions.clone(),
2631 _ => None,
2632 })
2633 }
2634 InitialHistory::Forked(items) => items.iter().find_map(|item| match item {
2635 RolloutItem::SessionMeta(meta_line) => meta_line.meta.base_instructions.clone(),
2636 _ => None,
2637 }),
2638 }
2639 }
2640
2641 pub fn get_dynamic_tools(&self) -> Option<Vec<DynamicToolSpec>> {
2642 match self {
2643 InitialHistory::New | InitialHistory::Cleared => None,
2644 InitialHistory::Resumed(resumed) => {
2645 resumed.history.iter().find_map(|item| match item {
2646 RolloutItem::SessionMeta(meta_line) => meta_line.meta.dynamic_tools.clone(),
2647 _ => None,
2648 })
2649 }
2650 InitialHistory::Forked(items) => items.iter().find_map(|item| match item {
2651 RolloutItem::SessionMeta(meta_line) => meta_line.meta.dynamic_tools.clone(),
2652 _ => None,
2653 }),
2654 }
2655 }
2656
2657 pub fn get_selected_capability_roots(&self) -> Vec<SelectedCapabilityRoot> {
2658 self.get_session_meta()
2659 .map(|meta| meta.selected_capability_roots.clone())
2660 .unwrap_or_default()
2661 }
2662
2663 pub fn get_multi_agent_version(&self) -> Option<MultiAgentVersion> {
2664 match self {
2665 InitialHistory::New | InitialHistory::Cleared => None,
2666 InitialHistory::Resumed(resumed) => {
2667 multi_agent_version_from_items(&resumed.history, Some(resumed.conversation_id))
2668 }
2669 InitialHistory::Forked(items) => {
2670 multi_agent_version_from_items(items, None)
2671 }
2672 }
2673 }
2674
2675 pub fn get_history_mode(&self, default_history_mode: ThreadHistoryMode) -> ThreadHistoryMode {
2676 match self {
2677 InitialHistory::New | InitialHistory::Cleared => default_history_mode,
2678 InitialHistory::Resumed(_) | InitialHistory::Forked(_) => self
2681 .get_session_meta()
2682 .map(|meta| meta.history_mode)
2683 .unwrap_or(default_history_mode),
2684 }
2685 }
2686
2687 pub fn get_latest_effective_multi_agent_mode(&self) -> Option<MultiAgentMode> {
2688 let items = match self {
2689 InitialHistory::New | InitialHistory::Cleared => return None,
2690 InitialHistory::Resumed(resumed) => &resumed.history,
2691 InitialHistory::Forked(items) => items,
2692 };
2693 items
2694 .iter()
2695 .rev()
2696 .find_map(|item| match item {
2697 RolloutItem::TurnContext(turn_context) => Some(turn_context),
2698 RolloutItem::SessionMeta(_)
2699 | RolloutItem::ResponseItem(_)
2700 | RolloutItem::InterAgentCommunication(_)
2701 | RolloutItem::InterAgentCommunicationMetadata { .. }
2702 | RolloutItem::Compacted(_)
2703 | RolloutItem::WorldState(_)
2704 | RolloutItem::EventMsg(_) => None,
2705 })
2706 .and_then(|turn_context| turn_context.multi_agent_mode.clone())
2707 }
2708
2709 pub fn get_resumed_session_sources(&self) -> Option<(SessionSource, Option<ThreadSource>)> {
2710 let meta = self.get_resumed_session_meta()?;
2711 Some((meta.source.clone(), meta.thread_source.clone()))
2712 }
2713
2714 pub fn get_resumed_thread_source(&self) -> Option<ThreadSource> {
2715 self.get_resumed_session_meta()
2716 .and_then(|meta| meta.thread_source.clone())
2717 }
2718
2719 pub fn get_session_originator(&self) -> Option<String> {
2720 self.get_session_meta()
2721 .map(|meta| meta.originator.clone())
2722 .filter(|originator| !originator.is_empty())
2723 }
2724
2725 pub fn get_resumed_parent_thread_id(&self) -> Option<ThreadId> {
2726 self.get_resumed_session_meta()
2727 .and_then(|meta| meta.parent_thread_id)
2728 }
2729
2730 fn get_session_meta(&self) -> Option<&SessionMeta> {
2731 match self {
2732 InitialHistory::New | InitialHistory::Cleared => None,
2733 InitialHistory::Resumed(resumed) => {
2734 resumed.history.iter().find_map(|item| match item {
2735 RolloutItem::SessionMeta(meta_line) => Some(&meta_line.meta),
2736 _ => None,
2737 })
2738 }
2739 InitialHistory::Forked(items) => items.iter().find_map(|item| match item {
2740 RolloutItem::SessionMeta(meta_line) => Some(&meta_line.meta),
2741 _ => None,
2742 }),
2743 }
2744 }
2745
2746 fn get_resumed_session_meta(&self) -> Option<&SessionMeta> {
2747 match self {
2748 InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => None,
2749 InitialHistory::Resumed(resumed) => {
2750 resumed.history.iter().find_map(|item| match item {
2751 RolloutItem::SessionMeta(meta_line) => Some(&meta_line.meta),
2752 _ => None,
2753 })
2754 }
2755 }
2756 }
2757}
2758
2759fn session_cwd_from_items(items: &[RolloutItem]) -> Option<PathBuf> {
2760 items.iter().find_map(|item| match item {
2761 RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.cwd.clone()),
2762 _ => None,
2763 })
2764}
2765
2766#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
2767#[serde(rename_all = "lowercase")]
2768#[ts(rename_all = "lowercase")]
2769pub enum SessionSource {
2770 Cli,
2771 #[default]
2772 VSCode,
2773 Exec,
2774 Mcp,
2775 Custom(String),
2776 Internal(InternalSessionSource),
2777 SubAgent(SubAgentSource),
2778 #[serde(other)]
2779 Unknown,
2780}
2781
2782#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
2783#[serde(try_from = "String", into = "String")]
2784#[schemars(with = "String")]
2785#[ts(type = "string")]
2786pub enum ThreadSource {
2787 User,
2788 Subagent,
2789 Feature(String),
2790 MemoryConsolidation,
2791}
2792
2793impl ThreadSource {
2794 pub fn as_str(&self) -> &str {
2795 match self {
2796 ThreadSource::User => "user",
2797 ThreadSource::Subagent => "subagent",
2798 ThreadSource::Feature(feature) => feature,
2799 ThreadSource::MemoryConsolidation => "memory_consolidation",
2800 }
2801 }
2802}
2803
2804impl fmt::Display for ThreadSource {
2805 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2806 f.write_str(self.as_str())
2807 }
2808}
2809
2810impl TryFrom<String> for ThreadSource {
2811 type Error = String;
2812
2813 fn try_from(value: String) -> Result<Self, Self::Error> {
2814 value.parse()
2815 }
2816}
2817
2818impl From<ThreadSource> for String {
2819 fn from(value: ThreadSource) -> Self {
2820 value.to_string()
2821 }
2822}
2823
2824impl FromStr for ThreadSource {
2825 type Err = String;
2826
2827 fn from_str(value: &str) -> Result<Self, Self::Err> {
2828 match value {
2829 "user" => Ok(ThreadSource::User),
2830 "subagent" => Ok(ThreadSource::Subagent),
2831 "memory_consolidation" => Ok(ThreadSource::MemoryConsolidation),
2832 other => Ok(ThreadSource::Feature(other.to_string())),
2833 }
2834 }
2835}
2836
2837#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
2838#[serde(rename_all = "snake_case")]
2839#[ts(rename_all = "snake_case")]
2840pub enum InternalSessionSource {
2841 MemoryConsolidation,
2842}
2843
2844#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
2845#[serde(rename_all = "snake_case")]
2846#[ts(rename_all = "snake_case")]
2847pub enum SubAgentSource {
2848 Review,
2849 Compact,
2850 ThreadSpawn {
2851 parent_thread_id: ThreadId,
2852 depth: i32,
2853 #[serde(default)]
2854 agent_path: Option<AgentPath>,
2855 #[serde(default)]
2856 agent_nickname: Option<String>,
2857 #[serde(default, alias = "agent_type")]
2858 agent_role: Option<String>,
2859 },
2860 MemoryConsolidation,
2861 Other(String),
2862}
2863
2864impl fmt::Display for SessionSource {
2865 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2866 match self {
2867 SessionSource::Cli => f.write_str("cli"),
2868 SessionSource::VSCode => f.write_str("vscode"),
2869 SessionSource::Exec => f.write_str("exec"),
2870 SessionSource::Mcp => f.write_str("mcp"),
2871 SessionSource::Custom(source) => f.write_str(source),
2872 SessionSource::Internal(source) => write!(f, "internal_{source}"),
2873 SessionSource::SubAgent(sub_source) => write!(f, "subagent_{sub_source}"),
2874 SessionSource::Unknown => f.write_str("unknown"),
2875 }
2876 }
2877}
2878
2879impl SessionSource {
2880 pub fn from_startup_arg(value: &str) -> Result<Self, &'static str> {
2881 let trimmed = value.trim();
2882 if trimmed.is_empty() {
2883 return Err("session source must not be empty");
2884 }
2885
2886 let normalized = trimmed.to_ascii_lowercase();
2887 Ok(match normalized.as_str() {
2888 "cli" => SessionSource::Cli,
2889 "vscode" => SessionSource::VSCode,
2890 "exec" => SessionSource::Exec,
2891 "mcp" | "appserver" | "app-server" | "app_server" => SessionSource::Mcp,
2892 "unknown" => SessionSource::Unknown,
2893 _ => SessionSource::Custom(normalized),
2894 })
2895 }
2896
2897 pub fn is_internal(&self) -> bool {
2898 matches!(self, SessionSource::Internal(_))
2899 }
2900
2901 pub fn is_non_root_agent(&self) -> bool {
2902 matches!(
2903 self,
2904 SessionSource::Internal(_) | SessionSource::SubAgent(_)
2905 )
2906 }
2907
2908 pub fn get_nickname(&self) -> Option<String> {
2909 match self {
2910 SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_nickname, .. }) => {
2911 agent_nickname.clone()
2912 }
2913 _ => None,
2914 }
2915 }
2916
2917 pub fn get_agent_role(&self) -> Option<String> {
2918 match self {
2919 SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_role, .. }) => {
2920 agent_role.clone()
2921 }
2922 _ => None,
2923 }
2924 }
2925
2926 pub fn get_agent_path(&self) -> Option<AgentPath> {
2927 match self {
2928 SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_path, .. }) => {
2929 agent_path.clone()
2930 }
2931 _ => None,
2932 }
2933 }
2934
2935 pub fn restriction_product(&self) -> Option<Product> {
2936 match self {
2937 SessionSource::Custom(source) => Product::from_session_source_name(source),
2938 SessionSource::Cli
2939 | SessionSource::VSCode
2940 | SessionSource::Exec
2941 | SessionSource::Mcp
2942 | SessionSource::Unknown => Some(Product::Codex),
2943 SessionSource::Internal(_) | SessionSource::SubAgent(_) => None,
2944 }
2945 }
2946
2947 pub fn matches_product_restriction(&self, products: &[Product]) -> bool {
2948 products.is_empty()
2949 || self
2950 .restriction_product()
2951 .is_some_and(|product| product.matches_product_restriction(products))
2952 }
2953
2954 pub fn parent_thread_id(&self) -> Option<ThreadId> {
2955 match self {
2956 SessionSource::SubAgent(subagent_source) => subagent_source.parent_thread_id(),
2957 SessionSource::Cli
2958 | SessionSource::VSCode
2959 | SessionSource::Exec
2960 | SessionSource::Mcp
2961 | SessionSource::Custom(_)
2962 | SessionSource::Internal(_)
2963 | SessionSource::Unknown => None,
2964 }
2965 }
2966}
2967
2968impl fmt::Display for SubAgentSource {
2969 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2970 match self {
2971 SubAgentSource::Review => f.write_str("review"),
2972 SubAgentSource::Compact => f.write_str("compact"),
2973 SubAgentSource::MemoryConsolidation => f.write_str("memory_consolidation"),
2974 SubAgentSource::ThreadSpawn {
2975 parent_thread_id,
2976 depth,
2977 ..
2978 } => {
2979 write!(f, "thread_spawn_{parent_thread_id}_d{depth}")
2980 }
2981 SubAgentSource::Other(other) => f.write_str(other),
2982 }
2983 }
2984}
2985
2986impl SubAgentSource {
2987 pub fn kind(&self) -> &str {
2988 match self {
2989 SubAgentSource::Review => "review",
2990 SubAgentSource::Compact => "compact",
2991 SubAgentSource::ThreadSpawn { .. } => "thread_spawn",
2992 SubAgentSource::MemoryConsolidation => "memory_consolidation",
2993 SubAgentSource::Other(other) => other,
2994 }
2995 }
2996
2997 pub fn parent_thread_id(&self) -> Option<ThreadId> {
2998 match self {
2999 SubAgentSource::ThreadSpawn {
3000 parent_thread_id, ..
3001 } => Some(*parent_thread_id),
3002 SubAgentSource::Review
3003 | SubAgentSource::Compact
3004 | SubAgentSource::MemoryConsolidation
3005 | SubAgentSource::Other(_) => None,
3006 }
3007 }
3008}
3009
3010impl fmt::Display for InternalSessionSource {
3011 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3012 match self {
3013 InternalSessionSource::MemoryConsolidation => f.write_str("memory_consolidation"),
3014 }
3015 }
3016}
3017
3018fn multi_agent_version_from_items(
3019 items: &[RolloutItem],
3020 thread_id: Option<ThreadId>,
3021) -> Option<MultiAgentVersion> {
3022 let session_meta_version = items.iter().rev().find_map(|item| match item {
3023 RolloutItem::SessionMeta(meta_line)
3024 if thread_id.is_none_or(|thread_id| meta_line.meta.id == thread_id) =>
3025 {
3026 meta_line.meta.multi_agent_version
3027 }
3028 _ => None,
3029 });
3030
3031 session_meta_version.or_else(|| {
3032 items.iter().rev().find_map(|item| match item {
3033 RolloutItem::TurnContext(turn_context) => turn_context.multi_agent_version,
3034 RolloutItem::SessionMeta(_)
3035 | RolloutItem::ResponseItem(_)
3036 | RolloutItem::InterAgentCommunication(_)
3037 | RolloutItem::InterAgentCommunicationMetadata { .. }
3038 | RolloutItem::Compacted(_)
3039 | RolloutItem::WorldState(_)
3040 | RolloutItem::EventMsg(_) => None,
3041 })
3042 })
3043}
3044
3045#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema, TS)]
3046#[serde(rename_all = "snake_case")]
3047#[ts(rename_all = "snake_case")]
3048pub enum MultiAgentVersion {
3049 Disabled,
3050 V1,
3051 V2,
3052}
3053
3054#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
3055pub struct SessionContextWindow {
3056 pub window_id: String,
3058}
3059
3060impl SessionContextWindow {
3061 pub fn new(window_id: String) -> Self {
3062 Self { window_id }
3063 }
3064}
3065
3066#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema, TS)]
3068pub struct HistoryPosition {
3069 pub thread_id: ThreadId,
3070 pub end_ordinal_exclusive: u64,
3072 pub end_byte_offset: u64,
3074}
3075
3076#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
3082pub struct SessionMeta {
3083 pub session_id: SessionId,
3084 pub id: ThreadId,
3085 #[serde(skip_serializing_if = "Option::is_none")]
3086 pub forked_from_id: Option<ThreadId>,
3087 #[serde(skip_serializing_if = "Option::is_none")]
3088 pub parent_thread_id: Option<ThreadId>,
3089 pub timestamp: String,
3090 pub cwd: PathBuf,
3091 pub originator: String,
3092 pub cli_version: String,
3093 #[serde(default)]
3094 pub source: SessionSource,
3095 #[serde(default, skip_serializing_if = "Option::is_none")]
3097 pub thread_source: Option<ThreadSource>,
3098 #[serde(skip_serializing_if = "Option::is_none")]
3100 pub agent_nickname: Option<String>,
3101 #[serde(default, alias = "agent_type", skip_serializing_if = "Option::is_none")]
3103 pub agent_role: Option<String>,
3104 #[serde(skip_serializing_if = "Option::is_none")]
3106 pub agent_path: Option<String>,
3107 pub model_provider: Option<String>,
3108 pub base_instructions: Option<BaseInstructions>,
3112 #[serde(
3113 default,
3114 deserialize_with = "crate::dynamic_tools::deserialize_dynamic_tool_specs",
3115 skip_serializing_if = "Option::is_none"
3116 )]
3117 pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
3118 #[serde(default, skip_serializing_if = "Vec::is_empty")]
3120 pub selected_capability_roots: Vec<SelectedCapabilityRoot>,
3121 #[serde(skip_serializing_if = "Option::is_none")]
3122 pub memory_mode: Option<String>,
3123 #[serde(default)]
3124 pub history_mode: ThreadHistoryMode,
3125 #[serde(default, skip_serializing_if = "Option::is_none")]
3127 pub history_base: Option<HistoryPosition>,
3128 #[serde(default, skip_serializing_if = "Option::is_none")]
3133 pub subagent_history_start_ordinal: Option<u64>,
3134 #[serde(skip_serializing_if = "Option::is_none")]
3135 pub multi_agent_version: Option<MultiAgentVersion>,
3136 #[serde(default, skip_serializing_if = "Option::is_none")]
3138 pub context_window: Option<SessionContextWindow>,
3139}
3140
3141impl Default for SessionMeta {
3142 fn default() -> Self {
3143 let id = ThreadId::default();
3144 SessionMeta {
3145 session_id: id.into(),
3146 id,
3147 forked_from_id: None,
3148 parent_thread_id: None,
3149 timestamp: String::new(),
3150 cwd: PathBuf::new(),
3151 originator: String::new(),
3152 cli_version: String::new(),
3153 source: SessionSource::default(),
3154 thread_source: None,
3155 agent_nickname: None,
3156 agent_role: None,
3157 agent_path: None,
3158 model_provider: None,
3159 base_instructions: None,
3160 dynamic_tools: None,
3161 selected_capability_roots: Vec::new(),
3162 memory_mode: None,
3163 history_mode: ThreadHistoryMode::default(),
3164 history_base: None,
3165 subagent_history_start_ordinal: None,
3166 multi_agent_version: None,
3167 context_window: None,
3168 }
3169 }
3170}
3171
3172#[derive(Serialize, Debug, Clone, JsonSchema, TS)]
3173pub struct SessionMetaLine {
3174 #[serde(flatten)]
3175 pub meta: SessionMeta,
3176 #[serde(skip_serializing_if = "Option::is_none")]
3177 pub git: Option<GitInfo>,
3178}
3179
3180impl<'de> Deserialize<'de> for SessionMetaLine {
3181 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3182 where
3183 D: Deserializer<'de>,
3184 {
3185 #[derive(Deserialize)]
3186 struct SessionMetaLineFields {
3187 #[serde(flatten)]
3188 meta: SessionMeta,
3189 git: Option<GitInfo>,
3190 }
3191
3192 let mut value = Value::deserialize(deserializer)?;
3193 let fields = value
3194 .as_object_mut()
3195 .ok_or_else(|| D::Error::custom("session metadata must be an object"))?;
3196 if !fields.contains_key("session_id") {
3197 let thread_id = fields
3198 .get("id")
3199 .cloned()
3200 .ok_or_else(|| D::Error::missing_field("id"))?;
3201 fields.insert("session_id".to_string(), thread_id);
3202 }
3203 let SessionMetaLineFields { meta, git } =
3204 serde_json::from_value(value).map_err(D::Error::custom)?;
3205 Ok(Self { meta, git })
3206 }
3207}
3208
3209#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
3210#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
3211pub enum RolloutItem {
3212 SessionMeta(SessionMetaLine),
3213 ResponseItem(ResponseItem),
3214 InterAgentCommunication(InterAgentCommunication),
3216 InterAgentCommunicationMetadata {
3218 trigger_turn: bool,
3219 },
3220 Compacted(CompactedItem),
3221 TurnContext(TurnContextItem),
3222 WorldState(WorldStateItem),
3223 EventMsg(EventMsg),
3224}
3225
3226#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)]
3228pub struct WorldStateItem {
3229 pub full: bool,
3231 pub state: Value,
3232}
3233
3234impl WorldStateItem {
3235 pub fn full(state: Value) -> Self {
3236 Self { full: true, state }
3237 }
3238
3239 pub fn patch(state: Value) -> Self {
3240 Self { full: false, state }
3241 }
3242}
3243
3244#[derive(Serialize, Clone, Debug, PartialEq, JsonSchema, TS)]
3245pub struct CompactedItem {
3246 pub message: String,
3247 #[serde(default, skip_serializing_if = "Option::is_none")]
3248 pub replacement_history: Option<Vec<ResponseItem>>,
3249 #[serde(default, skip_serializing_if = "Option::is_none")]
3251 pub window_number: Option<u64>,
3252 #[serde(default, skip_serializing_if = "Option::is_none")]
3254 pub first_window_id: Option<String>,
3255 #[serde(default, skip_serializing_if = "Option::is_none")]
3257 pub previous_window_id: Option<String>,
3258 #[serde(default, skip_serializing_if = "Option::is_none")]
3260 pub window_id: Option<String>,
3261}
3262
3263impl From<CompactedItem> for ResponseItem {
3264 fn from(value: CompactedItem) -> Self {
3265 ResponseItem::Message {
3266 id: None,
3267 role: "assistant".to_string(),
3268 content: vec![ContentItem::OutputText {
3269 text: value.message,
3270 }],
3271 phase: None,
3272 internal_chat_message_metadata_passthrough: None,
3273 }
3274 }
3275}
3276
3277#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
3278pub struct TurnContextNetworkItem {
3279 pub allowed_domains: Vec<String>,
3280 pub denied_domains: Vec<String>,
3281}
3282
3283#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)]
3288pub struct TurnContextItem {
3289 #[serde(default, skip_serializing_if = "Option::is_none")]
3290 pub turn_id: Option<String>,
3291 pub cwd: AbsolutePathBuf,
3292 #[serde(default, skip_serializing_if = "Option::is_none")]
3295 pub workspace_roots: Option<Vec<AbsolutePathBuf>>,
3296 #[serde(default, skip_serializing_if = "Option::is_none")]
3297 pub current_date: Option<String>,
3298 #[serde(default, skip_serializing_if = "Option::is_none")]
3299 pub timezone: Option<String>,
3300 pub approval_policy: AskForApproval,
3301 #[serde(default, skip_serializing_if = "Option::is_none")]
3302 pub approvals_reviewer: Option<ApprovalsReviewer>,
3303 pub sandbox_policy: SandboxPolicy,
3304 #[serde(default, skip_serializing_if = "Option::is_none")]
3305 pub permission_profile: Option<PermissionProfile>,
3306 #[serde(skip_serializing_if = "Option::is_none")]
3307 pub network: Option<TurnContextNetworkItem>,
3308 #[serde(default, skip_serializing_if = "Option::is_none")]
3309 pub file_system_sandbox_policy: Option<FileSystemSandboxPolicy>,
3310 pub model: String,
3311 #[serde(default, skip_serializing_if = "Option::is_none")]
3312 pub comp_hash: Option<String>,
3313 #[serde(skip_serializing_if = "Option::is_none")]
3314 pub personality: Option<Personality>,
3315 #[serde(default, skip_serializing_if = "Option::is_none")]
3316 pub collaboration_mode: Option<CollaborationMode>,
3317 #[serde(default, skip_serializing_if = "Option::is_none")]
3318 pub multi_agent_version: Option<MultiAgentVersion>,
3319 #[serde(default, skip_serializing_if = "Option::is_none")]
3321 pub multi_agent_mode: Option<MultiAgentMode>,
3322 #[serde(default, skip_serializing_if = "Option::is_none")]
3323 pub realtime_active: Option<bool>,
3324 #[serde(skip_serializing_if = "Option::is_none")]
3325 pub effort: Option<ReasoningEffortConfig>,
3326 pub summary: ReasoningSummaryConfig,
3331}
3332
3333impl TurnContextItem {
3334 pub fn permission_profile(&self) -> PermissionProfile {
3335 self.permission_profile.clone().unwrap_or_else(|| {
3336 let file_system_sandbox_policy =
3337 self.file_system_sandbox_policy.clone().unwrap_or_else(|| {
3338 FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
3339 &self.sandbox_policy,
3340 self.cwd.as_path(),
3341 )
3342 });
3343 PermissionProfile::from_runtime_permissions_with_enforcement(
3344 SandboxEnforcement::from_legacy_sandbox_policy(&self.sandbox_policy),
3345 &file_system_sandbox_policy,
3346 NetworkSandboxPolicy::from(&self.sandbox_policy),
3347 )
3348 })
3349 }
3350}
3351
3352#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
3353#[serde(tag = "mode", content = "limit", rename_all = "snake_case")]
3354pub enum TruncationPolicy {
3355 Bytes(usize),
3356 Tokens(usize),
3357}
3358
3359impl From<crate::openai_models::TruncationPolicyConfig> for TruncationPolicy {
3360 fn from(config: crate::openai_models::TruncationPolicyConfig) -> Self {
3361 match config.mode {
3362 crate::openai_models::TruncationMode::Bytes => Self::Bytes(config.limit as usize),
3363 crate::openai_models::TruncationMode::Tokens => Self::Tokens(config.limit as usize),
3364 }
3365 }
3366}
3367
3368impl TruncationPolicy {
3369 pub fn token_budget(&self) -> usize {
3370 match self {
3371 TruncationPolicy::Bytes(bytes) => {
3372 usize::try_from(codex_utils_string::approx_tokens_from_byte_count(*bytes))
3373 .unwrap_or(usize::MAX)
3374 }
3375 TruncationPolicy::Tokens(tokens) => *tokens,
3376 }
3377 }
3378
3379 pub fn byte_budget(&self) -> usize {
3380 match self {
3381 TruncationPolicy::Bytes(bytes) => *bytes,
3382 TruncationPolicy::Tokens(tokens) => {
3383 codex_utils_string::approx_bytes_for_tokens(*tokens)
3384 }
3385 }
3386 }
3387}
3388
3389impl Mul<f64> for TruncationPolicy {
3390 type Output = Self;
3391
3392 fn mul(self, multiplier: f64) -> Self::Output {
3393 match self {
3394 TruncationPolicy::Bytes(bytes) => {
3395 TruncationPolicy::Bytes((bytes as f64 * multiplier).ceil() as usize)
3396 }
3397 TruncationPolicy::Tokens(tokens) => {
3398 TruncationPolicy::Tokens((tokens as f64 * multiplier).ceil() as usize)
3399 }
3400 }
3401 }
3402}
3403
3404#[derive(Serialize, Deserialize, Clone, JsonSchema)]
3405pub struct RolloutLine {
3406 pub timestamp: String,
3407 #[serde(default, skip_serializing_if = "Option::is_none")]
3408 pub ordinal: Option<u64>,
3409 #[serde(flatten)]
3410 pub item: RolloutItem,
3411}
3412
3413#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
3414pub struct GitInfo {
3415 #[serde(skip_serializing_if = "Option::is_none")]
3417 pub commit_hash: Option<GitSha>,
3418 #[serde(skip_serializing_if = "Option::is_none")]
3420 pub branch: Option<String>,
3421 #[serde(skip_serializing_if = "Option::is_none")]
3423 pub repository_url: Option<String>,
3424}
3425
3426#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
3427#[serde(rename_all = "snake_case")]
3428pub enum ReviewDelivery {
3429 Inline,
3430 Detached,
3431}
3432
3433#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)]
3434#[serde(tag = "type", rename_all = "camelCase")]
3435#[ts(tag = "type")]
3436pub enum ReviewTarget {
3437 UncommittedChanges,
3439
3440 #[serde(rename_all = "camelCase")]
3442 #[ts(rename_all = "camelCase")]
3443 BaseBranch { branch: String },
3444
3445 #[serde(rename_all = "camelCase")]
3447 #[ts(rename_all = "camelCase")]
3448 Commit {
3449 sha: String,
3450 title: Option<String>,
3452 },
3453
3454 #[serde(rename_all = "camelCase")]
3456 #[ts(rename_all = "camelCase")]
3457 Custom { instructions: String },
3458}
3459
3460#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
3461pub struct ReviewRequest {
3463 pub target: ReviewTarget,
3464 #[serde(skip_serializing_if = "Option::is_none")]
3465 #[ts(optional)]
3466 pub user_facing_hint: Option<String>,
3467}
3468
3469#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
3471pub struct ReviewOutputEvent {
3472 pub findings: Vec<ReviewFinding>,
3473 pub overall_correctness: String,
3474 pub overall_explanation: String,
3475 pub overall_confidence_score: f32,
3476}
3477
3478impl Default for ReviewOutputEvent {
3479 fn default() -> Self {
3480 Self {
3481 findings: Vec::new(),
3482 overall_correctness: String::default(),
3483 overall_explanation: String::default(),
3484 overall_confidence_score: 0.0,
3485 }
3486 }
3487}
3488
3489#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
3491pub struct ReviewFinding {
3492 pub title: String,
3493 pub body: String,
3494 pub confidence_score: f32,
3495 pub priority: i32,
3496 pub code_location: ReviewCodeLocation,
3497}
3498
3499#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
3501pub struct ReviewCodeLocation {
3502 pub absolute_file_path: PathBuf,
3503 pub line_range: ReviewLineRange,
3504}
3505
3506#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
3508pub struct ReviewLineRange {
3509 pub start: u32,
3510 pub end: u32,
3511}
3512
3513#[derive(
3514 Debug, Clone, Copy, Display, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS, Default,
3515)]
3516#[serde(rename_all = "snake_case")]
3517pub enum ExecCommandSource {
3518 #[default]
3519 Agent,
3520 UserShell,
3521 UnifiedExecStartup,
3522 UnifiedExecInteraction,
3523}
3524
3525#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
3526#[serde(rename_all = "snake_case")]
3527pub enum ExecCommandStatus {
3528 Completed,
3529 Failed,
3530 Declined,
3531}
3532
3533#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
3534pub struct ExecCommandBeginEvent {
3535 pub call_id: String,
3537 #[serde(default, skip_serializing_if = "Option::is_none")]
3539 #[ts(optional)]
3540 pub process_id: Option<String>,
3541 pub turn_id: String,
3543 #[serde(default)]
3544 pub started_at_ms: i64,
3545 pub command: Vec<String>,
3547 pub cwd: PathUri,
3549 pub parsed_cmd: Vec<ParsedCommand>,
3550 #[serde(default)]
3552 pub source: ExecCommandSource,
3553 #[serde(default, skip_serializing_if = "Option::is_none")]
3555 #[ts(optional)]
3556 pub interaction_input: Option<String>,
3557}
3558
3559#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
3560pub struct ExecCommandEndEvent {
3561 pub call_id: String,
3563 #[serde(default, skip_serializing_if = "Option::is_none")]
3565 #[ts(optional)]
3566 pub process_id: Option<String>,
3567 pub turn_id: String,
3569 #[serde(default)]
3570 pub completed_at_ms: i64,
3571 pub command: Vec<String>,
3573 pub cwd: PathUri,
3575 pub parsed_cmd: Vec<ParsedCommand>,
3576 #[serde(default)]
3578 pub source: ExecCommandSource,
3579 #[serde(default, skip_serializing_if = "Option::is_none")]
3581 #[ts(optional)]
3582 pub interaction_input: Option<String>,
3583
3584 pub stdout: String,
3586 pub stderr: String,
3588 #[serde(default)]
3590 pub aggregated_output: String,
3591 pub exit_code: i32,
3593 #[ts(type = "string")]
3595 pub duration: Duration,
3596 pub formatted_output: String,
3598 pub status: ExecCommandStatus,
3600}
3601
3602#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
3603pub struct ViewImageToolCallEvent {
3604 pub call_id: String,
3606 pub path: PathUri,
3611}
3612
3613#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
3614#[serde(rename_all = "snake_case")]
3615pub enum ExecOutputStream {
3616 Stdout,
3617 Stderr,
3618}
3619
3620#[serde_as]
3621#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
3622pub struct ExecCommandOutputDeltaEvent {
3623 pub call_id: String,
3625 pub stream: ExecOutputStream,
3627 #[serde_as(as = "serde_with::base64::Base64")]
3629 #[schemars(with = "String")]
3630 #[ts(type = "string")]
3631 pub chunk: Vec<u8>,
3632}
3633
3634#[serde_as]
3635#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
3636pub struct TerminalInteractionEvent {
3637 pub call_id: String,
3639 pub process_id: String,
3641 pub stdin: String,
3643}
3644
3645#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
3646pub struct DeprecationNoticeEvent {
3647 pub summary: String,
3649 #[serde(skip_serializing_if = "Option::is_none")]
3651 pub details: Option<String>,
3652}
3653
3654#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
3655pub struct ThreadRolledBackEvent {
3656 pub num_turns: u32,
3658}
3659
3660#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
3661pub struct StreamErrorEvent {
3662 pub message: String,
3663 #[serde(default)]
3664 pub codex_error_info: Option<CodexErrorInfo>,
3665 #[serde(default)]
3669 pub additional_details: Option<String>,
3670}
3671
3672#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
3673pub struct StreamInfoEvent {
3674 pub message: String,
3675}
3676
3677#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
3678pub struct PatchApplyBeginEvent {
3679 pub call_id: String,
3681 #[serde(default)]
3684 pub turn_id: String,
3685 pub auto_approved: bool,
3687 pub changes: HashMap<PathBuf, FileChange>,
3689}
3690
3691#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
3692pub struct PatchApplyUpdatedEvent {
3693 pub call_id: String,
3695 pub changes: HashMap<PathBuf, FileChange>,
3697}
3698
3699#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
3700pub struct PatchApplyEndEvent {
3701 pub call_id: String,
3703 #[serde(default)]
3706 pub turn_id: String,
3707 pub stdout: String,
3709 pub stderr: String,
3711 pub success: bool,
3713 #[serde(default)]
3715 pub changes: HashMap<PathBuf, FileChange>,
3716 pub status: PatchApplyStatus,
3718}
3719
3720#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
3721#[serde(rename_all = "snake_case")]
3722pub enum PatchApplyStatus {
3723 Completed,
3724 Failed,
3725 Declined,
3726}
3727
3728#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
3729pub struct TurnDiffEvent {
3730 pub unified_diff: String,
3731}
3732
3733#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
3734pub struct McpStartupUpdateEvent {
3735 pub server: String,
3737 pub status: McpStartupStatus,
3739}
3740
3741#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
3742#[serde(rename_all = "snake_case", tag = "state")]
3743#[ts(rename_all = "snake_case", tag = "state")]
3744pub enum McpStartupStatus {
3745 Starting,
3746 Ready,
3747 Failed {
3748 error: String,
3749 #[serde(default, skip_serializing_if = "Option::is_none")]
3750 #[ts(optional = nullable)]
3751 reason: Option<McpStartupFailureReason>,
3752 },
3753 Cancelled,
3754}
3755
3756#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema, TS)]
3757#[serde(rename_all = "snake_case")]
3758#[ts(rename_all = "snake_case")]
3759pub enum McpStartupFailureReason {
3760 ReauthenticationRequired,
3761}
3762
3763#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, Default)]
3764pub struct McpStartupCompleteEvent {
3765 pub ready: Vec<String>,
3766 pub failed: Vec<McpStartupFailure>,
3767 pub cancelled: Vec<String>,
3768}
3769
3770#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
3771pub struct McpStartupFailure {
3772 pub server: String,
3773 pub error: String,
3774}
3775
3776#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
3777#[serde(rename_all = "snake_case")]
3778#[ts(rename_all = "snake_case")]
3779pub enum McpAuthStatus {
3780 Unsupported,
3781 NotLoggedIn,
3782 BearerToken,
3783 OAuth,
3784}
3785
3786impl fmt::Display for McpAuthStatus {
3787 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3788 let text = match self {
3789 McpAuthStatus::Unsupported => "Unsupported",
3790 McpAuthStatus::NotLoggedIn => "Not logged in",
3791 McpAuthStatus::BearerToken => "Bearer token",
3792 McpAuthStatus::OAuth => "OAuth",
3793 };
3794 f.write_str(text)
3795 }
3796}
3797
3798#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
3799pub struct RealtimeConversationListVoicesResponseEvent {
3800 pub voices: RealtimeVoicesList,
3801}
3802
3803#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)]
3804#[serde(rename_all = "lowercase")]
3805#[ts(rename_all = "lowercase")]
3806pub enum Product {
3807 #[serde(alias = "CHATGPT")]
3808 Chatgpt,
3809 #[serde(alias = "CODEX")]
3810 Codex,
3811 #[serde(alias = "ATLAS")]
3812 Atlas,
3813}
3814impl Product {
3815 pub fn to_app_platform(self) -> &'static str {
3816 match self {
3817 Self::Chatgpt => "chat",
3818 Self::Codex => "codex",
3819 Self::Atlas => "atlas",
3820 }
3821 }
3822
3823 pub fn from_session_source_name(value: &str) -> Option<Self> {
3824 let normalized = value.trim().to_ascii_lowercase();
3825 match normalized.as_str() {
3826 "chatgpt" => Some(Self::Chatgpt),
3827 "codex" => Some(Self::Codex),
3828 "atlas" => Some(Self::Atlas),
3829 _ => None,
3830 }
3831 }
3832
3833 pub fn matches_product_restriction(&self, products: &[Product]) -> bool {
3834 products.is_empty() || products.contains(self)
3835 }
3836}
3837#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
3838#[serde(rename_all = "snake_case")]
3839#[ts(rename_all = "snake_case")]
3840pub enum SkillScope {
3841 User,
3842 Repo,
3843 System,
3844 Admin,
3845}
3846
3847#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
3848pub struct SkillMetadata {
3849 pub name: String,
3850 pub description: String,
3851 #[serde(default, skip_serializing_if = "Option::is_none")]
3852 #[ts(optional)]
3853 pub short_description: Option<String>,
3855 #[serde(default, skip_serializing_if = "Option::is_none")]
3856 #[ts(optional)]
3857 pub interface: Option<SkillInterface>,
3858 #[serde(default, skip_serializing_if = "Option::is_none")]
3859 #[ts(optional)]
3860 pub dependencies: Option<SkillDependencies>,
3861 pub path: AbsolutePathBuf,
3862 pub scope: SkillScope,
3863 pub enabled: bool,
3864}
3865
3866#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)]
3867pub struct SkillInterface {
3868 #[ts(optional)]
3869 pub display_name: Option<String>,
3870 #[ts(optional)]
3871 pub short_description: Option<String>,
3872 #[ts(optional)]
3873 pub icon_small: Option<AbsolutePathBuf>,
3874 #[ts(optional)]
3875 pub icon_large: Option<AbsolutePathBuf>,
3876 #[ts(optional)]
3877 pub brand_color: Option<String>,
3878 #[ts(optional)]
3879 pub default_prompt: Option<String>,
3880}
3881
3882#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)]
3883pub struct SkillDependencies {
3884 pub tools: Vec<SkillToolDependency>,
3885}
3886
3887#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)]
3888pub struct SkillToolDependency {
3889 #[serde(rename = "type")]
3890 #[ts(rename = "type")]
3891 pub r#type: String,
3892 pub value: String,
3893 #[serde(default, skip_serializing_if = "Option::is_none")]
3894 #[ts(optional)]
3895 pub description: Option<String>,
3896 #[serde(default, skip_serializing_if = "Option::is_none")]
3897 #[ts(optional)]
3898 pub transport: Option<String>,
3899 #[serde(default, skip_serializing_if = "Option::is_none")]
3900 #[ts(optional)]
3901 pub command: Option<String>,
3902 #[serde(default, skip_serializing_if = "Option::is_none")]
3903 #[ts(optional)]
3904 pub url: Option<String>,
3905}
3906
3907#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)]
3908pub struct SessionNetworkProxyRuntime {
3909 pub http_addr: String,
3910 pub socks_addr: String,
3911}
3912
3913#[derive(Debug, Clone, Serialize, JsonSchema, TS)]
3914pub struct SessionConfiguredEvent {
3915 pub session_id: SessionId,
3916 pub thread_id: ThreadId,
3917 #[serde(skip_serializing_if = "Option::is_none")]
3918 pub forked_from_id: Option<ThreadId>,
3919 #[serde(skip_serializing_if = "Option::is_none")]
3920 pub parent_thread_id: Option<ThreadId>,
3921 #[serde(default, skip_serializing_if = "Option::is_none")]
3923 pub thread_source: Option<ThreadSource>,
3924
3925 #[serde(default, skip_serializing_if = "Option::is_none")]
3927 #[ts(optional)]
3928 pub thread_name: Option<String>,
3929
3930 pub model: String,
3932
3933 pub model_provider_id: String,
3934
3935 #[serde(skip_serializing_if = "Option::is_none")]
3936 pub service_tier: Option<String>,
3937
3938 pub approval_policy: AskForApproval,
3940
3941 #[serde(default)]
3945 pub approvals_reviewer: ApprovalsReviewer,
3946
3947 pub permission_profile: PermissionProfile,
3949
3950 #[serde(default, skip_serializing_if = "Option::is_none")]
3953 #[ts(optional)]
3954 pub active_permission_profile: Option<ActivePermissionProfile>,
3955
3956 pub cwd: AbsolutePathBuf,
3959
3960 #[serde(skip_serializing_if = "Option::is_none")]
3962 pub reasoning_effort: Option<ReasoningEffortConfig>,
3963
3964 #[serde(skip_serializing_if = "Option::is_none")]
3967 pub initial_messages: Option<Vec<EventMsg>>,
3968
3969 #[serde(default, skip_serializing_if = "Option::is_none")]
3971 #[ts(optional)]
3972 pub network_proxy: Option<SessionNetworkProxyRuntime>,
3973
3974 #[serde(skip_serializing_if = "Option::is_none")]
3976 pub rollout_path: Option<PathBuf>,
3977}
3978
3979impl<'de> Deserialize<'de> for SessionConfiguredEvent {
3980 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3981 where
3982 D: serde::Deserializer<'de>,
3983 {
3984 #[derive(Deserialize)]
3985 struct Wire {
3986 session_id: SessionId,
3987 #[serde(default)]
3988 thread_id: Option<ThreadId>,
3989 forked_from_id: Option<ThreadId>,
3990 parent_thread_id: Option<ThreadId>,
3991 #[serde(default)]
3992 thread_source: Option<ThreadSource>,
3993 #[serde(default)]
3994 thread_name: Option<String>,
3995 model: String,
3996 model_provider_id: String,
3997 service_tier: Option<String>,
3998 approval_policy: AskForApproval,
3999 #[serde(default)]
4000 approvals_reviewer: ApprovalsReviewer,
4001 sandbox_policy: Option<SandboxPolicy>,
4005 permission_profile: Option<PermissionProfile>,
4006 #[serde(default)]
4007 active_permission_profile: Option<ActivePermissionProfile>,
4008 cwd: AbsolutePathBuf,
4009 reasoning_effort: Option<ReasoningEffortConfig>,
4010 initial_messages: Option<Vec<EventMsg>>,
4011 network_proxy: Option<SessionNetworkProxyRuntime>,
4012 rollout_path: Option<PathBuf>,
4013 }
4014
4015 let wire = Wire::deserialize(deserializer)?;
4016 let permission_profile = match (wire.permission_profile, wire.sandbox_policy) {
4017 (Some(permission_profile), _) => permission_profile,
4018 (None, Some(sandbox_policy)) => PermissionProfile::from_legacy_sandbox_policy_for_cwd(
4019 &sandbox_policy,
4020 wire.cwd.as_path(),
4021 ),
4022 (None, None) => {
4023 return Err(serde::de::Error::missing_field("permission_profile"));
4024 }
4025 };
4026
4027 Ok(Self {
4028 session_id: wire.session_id,
4029 thread_id: wire.thread_id.unwrap_or_else(|| wire.session_id.into()),
4030 forked_from_id: wire.forked_from_id,
4031 parent_thread_id: wire.parent_thread_id,
4032 thread_source: wire.thread_source,
4033 thread_name: wire.thread_name,
4034 model: wire.model,
4035 model_provider_id: wire.model_provider_id,
4036 service_tier: wire.service_tier,
4037 approval_policy: wire.approval_policy,
4038 approvals_reviewer: wire.approvals_reviewer,
4039 permission_profile,
4040 active_permission_profile: wire.active_permission_profile,
4041 cwd: wire.cwd,
4042 reasoning_effort: wire.reasoning_effort,
4043 initial_messages: wire.initial_messages,
4044 network_proxy: wire.network_proxy,
4045 rollout_path: wire.rollout_path,
4046 })
4047 }
4048}
4049
4050#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
4051#[serde(rename_all = "camelCase")]
4052#[ts(export_to = "protocol/")]
4053pub enum ThreadGoalStatus {
4054 Active,
4055 Paused,
4056 Blocked,
4057 UsageLimited,
4058 BudgetLimited,
4059 Complete,
4060}
4061
4062pub const MAX_THREAD_GOAL_OBJECTIVE_CHARS: usize = 4_000;
4063
4064pub fn validate_thread_goal_objective(value: &str) -> Result<(), String> {
4065 if value.is_empty() {
4066 return Err("goal objective must not be empty".to_string());
4067 }
4068 if value.chars().count() > MAX_THREAD_GOAL_OBJECTIVE_CHARS {
4069 return Err(format!(
4070 "goal objective must be at most {MAX_THREAD_GOAL_OBJECTIVE_CHARS} characters"
4071 ));
4072 }
4073 Ok(())
4074}
4075
4076#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
4077#[serde(rename_all = "camelCase")]
4078#[ts(export_to = "protocol/")]
4079pub struct ThreadGoal {
4080 pub thread_id: ThreadId,
4081 pub objective: String,
4082 pub status: ThreadGoalStatus,
4083 #[serde(default, skip_serializing_if = "Option::is_none")]
4084 #[ts(optional)]
4085 pub token_budget: Option<i64>,
4086 pub tokens_used: i64,
4087 pub time_used_seconds: i64,
4088 pub created_at: i64,
4089 pub updated_at: i64,
4090}
4091
4092#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
4093#[serde(rename_all = "camelCase")]
4094#[ts(export_to = "protocol/")]
4095pub struct ThreadGoalUpdatedEvent {
4096 pub thread_id: ThreadId,
4097 #[serde(default, skip_serializing_if = "Option::is_none")]
4098 #[ts(optional)]
4099 pub turn_id: Option<String>,
4100 pub goal: ThreadGoal,
4101}
4102
4103#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Display, JsonSchema, TS)]
4105#[serde(rename_all = "snake_case")]
4106pub enum ReviewDecision {
4107 Approved,
4109
4110 ApprovedExecpolicyAmendment {
4113 proposed_execpolicy_amendment: ExecPolicyAmendment,
4114 },
4115
4116 ApprovedForSession,
4120
4121 NetworkPolicyAmendment {
4124 network_policy_amendment: NetworkPolicyAmendment,
4125 },
4126
4127 Denied { rejection: String },
4130
4131 TimedOut,
4133
4134 Abort,
4137}
4138
4139impl Default for ReviewDecision {
4140 fn default() -> Self {
4141 Self::Denied {
4142 rejection: "denied".to_string(),
4143 }
4144 }
4145}
4146
4147impl ReviewDecision {
4148 pub fn denied(rejection: impl Into<String>) -> Self {
4149 Self::Denied {
4150 rejection: rejection.into(),
4151 }
4152 }
4153
4154 pub fn to_opaque_string(&self) -> &'static str {
4157 match self {
4158 ReviewDecision::Approved => "approved",
4159 ReviewDecision::ApprovedExecpolicyAmendment { .. } => "approved_with_amendment",
4160 ReviewDecision::ApprovedForSession => "approved_for_session",
4161 ReviewDecision::NetworkPolicyAmendment {
4162 network_policy_amendment,
4163 } => match network_policy_amendment.action {
4164 NetworkPolicyRuleAction::Allow => "approved_with_network_policy_allow",
4165 NetworkPolicyRuleAction::Deny => "denied_with_network_policy_deny",
4166 },
4167 ReviewDecision::Denied { .. } => "denied",
4168 ReviewDecision::TimedOut => "timed_out",
4169 ReviewDecision::Abort => "abort",
4170 }
4171 }
4172}
4173
4174#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
4175#[serde(tag = "type", rename_all = "snake_case")]
4176#[ts(tag = "type")]
4177pub enum FileChange {
4178 Add {
4179 content: String,
4180 },
4181 Delete {
4182 content: String,
4183 },
4184 Update {
4185 unified_diff: String,
4186 move_path: Option<PathBuf>,
4187 },
4188}
4189
4190#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
4191pub struct Chunk {
4192 pub orig_index: u32,
4194 pub deleted_lines: Vec<String>,
4195 pub inserted_lines: Vec<String>,
4196}
4197
4198#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
4199pub struct TurnAbortedEvent {
4200 pub turn_id: Option<String>,
4201 pub reason: TurnAbortReason,
4202 #[serde(default, skip_serializing_if = "Option::is_none")]
4204 #[ts(type = "number | null", optional)]
4205 pub started_at: Option<i64>,
4206 #[serde(default, skip_serializing_if = "Option::is_none")]
4208 #[ts(type = "number | null", optional)]
4209 pub completed_at: Option<i64>,
4210 #[serde(default, skip_serializing_if = "Option::is_none")]
4212 #[ts(type = "number | null", optional)]
4213 pub duration_ms: Option<i64>,
4214}
4215
4216#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
4217#[serde(rename_all = "snake_case")]
4218pub enum TurnAbortReason {
4219 Interrupted,
4220 Replaced,
4221 ReviewEnded,
4222 BudgetLimited,
4223}
4224
4225#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
4226pub struct CollabAgentSpawnBeginEvent {
4227 pub call_id: String,
4229 #[serde(default)]
4230 pub started_at_ms: i64,
4231 pub sender_thread_id: ThreadId,
4233 pub prompt: String,
4236 pub model: String,
4237 pub reasoning_effort: ReasoningEffortConfig,
4238}
4239
4240#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
4241pub struct CollabAgentRef {
4242 pub thread_id: ThreadId,
4244 #[serde(default, skip_serializing_if = "Option::is_none")]
4246 pub agent_nickname: Option<String>,
4247 #[serde(default, alias = "agent_type", skip_serializing_if = "Option::is_none")]
4249 pub agent_role: Option<String>,
4250}
4251
4252#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
4253pub struct CollabAgentStatusEntry {
4254 pub thread_id: ThreadId,
4256 #[serde(default, skip_serializing_if = "Option::is_none")]
4258 pub agent_nickname: Option<String>,
4259 #[serde(default, alias = "agent_type", skip_serializing_if = "Option::is_none")]
4261 pub agent_role: Option<String>,
4262 pub status: AgentStatus,
4264}
4265
4266#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
4267pub struct CollabAgentSpawnEndEvent {
4268 pub call_id: String,
4270 #[serde(default)]
4271 pub completed_at_ms: i64,
4272 pub sender_thread_id: ThreadId,
4274 pub new_thread_id: Option<ThreadId>,
4276 #[serde(default, skip_serializing_if = "Option::is_none")]
4278 pub new_agent_nickname: Option<String>,
4279 #[serde(default, skip_serializing_if = "Option::is_none")]
4281 pub new_agent_role: Option<String>,
4282 pub prompt: String,
4285 pub model: String,
4287 pub reasoning_effort: ReasoningEffortConfig,
4289 pub status: AgentStatus,
4291}
4292
4293#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
4294pub struct CollabAgentInteractionBeginEvent {
4295 pub call_id: String,
4297 #[serde(default)]
4298 pub started_at_ms: i64,
4299 pub sender_thread_id: ThreadId,
4301 pub receiver_thread_id: ThreadId,
4303 pub prompt: String,
4306}
4307
4308#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
4309pub struct CollabAgentInteractionEndEvent {
4310 pub call_id: String,
4312 #[serde(default)]
4313 pub completed_at_ms: i64,
4314 pub sender_thread_id: ThreadId,
4316 pub receiver_thread_id: ThreadId,
4318 #[serde(default, skip_serializing_if = "Option::is_none")]
4320 pub receiver_agent_nickname: Option<String>,
4321 #[serde(default, skip_serializing_if = "Option::is_none")]
4323 pub receiver_agent_role: Option<String>,
4324 pub prompt: String,
4327 pub status: AgentStatus,
4329}
4330
4331#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
4332#[serde(rename_all = "snake_case")]
4333#[ts(rename_all = "snake_case")]
4334pub enum SubAgentActivityKind {
4335 Started,
4336 Interacted,
4337 Interrupted,
4338}
4339
4340#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
4341pub struct SubAgentActivityEvent {
4342 pub event_id: String,
4343 #[serde(default)]
4344 pub occurred_at_ms: i64,
4345 pub agent_thread_id: ThreadId,
4347 pub agent_path: AgentPath,
4349 pub kind: SubAgentActivityKind,
4350}
4351
4352#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
4353pub struct CollabWaitingBeginEvent {
4354 #[serde(default)]
4355 pub started_at_ms: i64,
4356 pub sender_thread_id: ThreadId,
4358 pub receiver_thread_ids: Vec<ThreadId>,
4360 #[serde(default, skip_serializing_if = "Vec::is_empty")]
4362 pub receiver_agents: Vec<CollabAgentRef>,
4363 pub call_id: String,
4365}
4366
4367#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
4368pub struct CollabWaitingEndEvent {
4369 pub sender_thread_id: ThreadId,
4371 pub call_id: String,
4373 #[serde(default)]
4374 pub completed_at_ms: i64,
4375 #[serde(default, skip_serializing_if = "Vec::is_empty")]
4377 pub agent_statuses: Vec<CollabAgentStatusEntry>,
4378 pub statuses: HashMap<ThreadId, AgentStatus>,
4380}
4381
4382#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
4383pub struct CollabCloseBeginEvent {
4384 pub call_id: String,
4386 #[serde(default)]
4387 pub started_at_ms: i64,
4388 pub sender_thread_id: ThreadId,
4390 pub receiver_thread_id: ThreadId,
4392}
4393
4394#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
4395pub struct CollabCloseEndEvent {
4396 pub call_id: String,
4398 #[serde(default)]
4399 pub completed_at_ms: i64,
4400 pub sender_thread_id: ThreadId,
4402 pub receiver_thread_id: ThreadId,
4404 #[serde(default, skip_serializing_if = "Option::is_none")]
4406 pub receiver_agent_nickname: Option<String>,
4407 #[serde(default, skip_serializing_if = "Option::is_none")]
4409 pub receiver_agent_role: Option<String>,
4410 pub status: AgentStatus,
4413}
4414
4415#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
4416pub struct CollabResumeBeginEvent {
4417 pub call_id: String,
4419 #[serde(default)]
4420 pub started_at_ms: i64,
4421 pub sender_thread_id: ThreadId,
4423 pub receiver_thread_id: ThreadId,
4425 #[serde(default, skip_serializing_if = "Option::is_none")]
4427 pub receiver_agent_nickname: Option<String>,
4428 #[serde(default, skip_serializing_if = "Option::is_none")]
4430 pub receiver_agent_role: Option<String>,
4431}
4432
4433#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
4434pub struct CollabResumeEndEvent {
4435 pub call_id: String,
4437 #[serde(default)]
4438 pub completed_at_ms: i64,
4439 pub sender_thread_id: ThreadId,
4441 pub receiver_thread_id: ThreadId,
4443 #[serde(default, skip_serializing_if = "Option::is_none")]
4445 pub receiver_agent_nickname: Option<String>,
4446 #[serde(default, skip_serializing_if = "Option::is_none")]
4448 pub receiver_agent_role: Option<String>,
4449 pub status: AgentStatus,
4452}
4453
4454#[cfg(test)]
4455mod tests {
4456 use super::*;
4457 use crate::items::CommandExecutionItem;
4458 use crate::items::CommandExecutionStatus;
4459 use crate::items::DynamicToolCallItem;
4460 use crate::items::DynamicToolCallStatus;
4461 use crate::items::EnteredReviewModeItem;
4462 use crate::items::ExitedReviewModeItem;
4463 use crate::items::FileChangeItem;
4464 use crate::items::ImageGenerationItem;
4465 use crate::items::McpToolCallItem;
4466 use crate::items::McpToolCallStatus;
4467 use crate::items::UserMessageItem;
4468 use crate::items::WebSearchItem;
4469 use crate::mcp::CallToolResult;
4470 use crate::permissions::FileSystemAccessMode;
4471 use crate::permissions::FileSystemPath;
4472 use crate::permissions::FileSystemSandboxEntry;
4473 use crate::permissions::FileSystemSandboxPolicy;
4474 use crate::permissions::FileSystemSpecialPath;
4475 use crate::permissions::NetworkSandboxPolicy;
4476 use anyhow::Result;
4477 use codex_utils_absolute_path::AbsolutePathBuf;
4478 use codex_utils_absolute_path::test_support::PathBufExt;
4479 use codex_utils_absolute_path::test_support::test_path_buf;
4480 use pretty_assertions::assert_eq;
4481 use serde_json::json;
4482 use std::path::PathBuf;
4483 use tempfile::NamedTempFile;
4484 use tempfile::TempDir;
4485
4486 #[test]
4487 fn review_decision_denied_round_trip() -> Result<()> {
4488 let decision = ReviewDecision::Denied {
4489 rejection: "denied reason".to_string(),
4490 };
4491 let value = json!({"denied": {"rejection": "denied reason"}});
4492
4493 assert_eq!(serde_json::to_value(&decision)?, value);
4494 assert_eq!(serde_json::from_value::<ReviewDecision>(value)?, decision);
4495 Ok(())
4496 }
4497
4498 #[test]
4499 fn feature_thread_source_serializes_as_its_app_owned_label() -> Result<()> {
4500 let source = ThreadSource::Feature("automation".to_string());
4501
4502 assert_eq!(serde_json::to_value(&source)?, json!("automation"));
4503 assert_eq!(
4504 serde_json::from_value::<ThreadSource>(json!("automation"))?,
4505 source
4506 );
4507 Ok(())
4508 }
4509
4510 #[test]
4511 fn session_meta_normalizes_legacy_dynamic_tools() -> Result<()> {
4512 let mut value = serde_json::to_value(SessionMeta::default())?;
4513 value["dynamic_tools"] = json!([
4514 {
4515 "namespace": "legacy_app",
4516 "name": "lookup_ticket",
4517 "description": "Look up a ticket",
4518 "inputSchema": {"type": "object", "properties": {}},
4519 "exposeToContext": false
4520 },
4521 {
4522 "namespace": "legacy_app",
4523 "name": "update_ticket",
4524 "description": "Update a ticket",
4525 "inputSchema": {"type": "object", "properties": {}},
4526 "deferLoading": false,
4527 "exposeToContext": false
4528 }
4529 ]);
4530
4531 let meta: SessionMeta = serde_json::from_value(value)?;
4532
4533 assert_eq!(
4534 meta.dynamic_tools,
4535 Some(vec![DynamicToolSpec::Namespace(
4536 crate::dynamic_tools::DynamicToolNamespaceSpec {
4537 name: "legacy_app".to_string(),
4538 description: String::new(),
4539 tools: vec![
4540 crate::dynamic_tools::DynamicToolNamespaceTool::Function(
4541 crate::dynamic_tools::DynamicToolFunctionSpec {
4542 name: "lookup_ticket".to_string(),
4543 description: "Look up a ticket".to_string(),
4544 input_schema: json!({"type": "object", "properties": {}}),
4545 defer_loading: true,
4546 },
4547 ),
4548 crate::dynamic_tools::DynamicToolNamespaceTool::Function(
4549 crate::dynamic_tools::DynamicToolFunctionSpec {
4550 name: "update_ticket".to_string(),
4551 description: "Update a ticket".to_string(),
4552 input_schema: json!({"type": "object", "properties": {}}),
4553 defer_loading: false,
4554 },
4555 ),
4556 ],
4557 },
4558 )])
4559 );
4560 Ok(())
4561 }
4562
4563 fn sorted_writable_roots(roots: Vec<WritableRoot>) -> Vec<(PathBuf, Vec<PathBuf>)> {
4564 let mut sorted_roots: Vec<(PathBuf, Vec<PathBuf>)> = roots
4565 .into_iter()
4566 .map(|root| {
4567 let mut read_only_subpaths: Vec<PathBuf> = root
4568 .read_only_subpaths
4569 .into_iter()
4570 .map(|path| path.to_path_buf())
4571 .collect();
4572 read_only_subpaths.sort();
4573 (root.root.to_path_buf(), read_only_subpaths)
4574 })
4575 .collect();
4576 sorted_roots.sort_by(|left, right| left.0.cmp(&right.0));
4577 sorted_roots
4578 }
4579
4580 fn sandbox_policy_allows_read(policy: &SandboxPolicy, _path: &Path, _cwd: &Path) -> bool {
4581 policy.has_full_disk_read_access()
4582 }
4583
4584 fn sandbox_policy_allows_write(policy: &SandboxPolicy, path: &Path, cwd: &Path) -> bool {
4585 if policy.has_full_disk_write_access() {
4586 return true;
4587 }
4588
4589 policy
4590 .get_writable_roots_with_cwd(cwd)
4591 .iter()
4592 .any(|root| root.is_path_writable(path))
4593 }
4594
4595 #[test]
4596 fn session_source_from_startup_arg_maps_known_values() {
4597 assert_eq!(
4598 SessionSource::from_startup_arg("vscode").unwrap(),
4599 SessionSource::VSCode
4600 );
4601 assert_eq!(
4602 SessionSource::from_startup_arg("app-server").unwrap(),
4603 SessionSource::Mcp
4604 );
4605 }
4606
4607 #[test]
4608 fn inter_agent_communication_response_input_item_preserves_commentary_phase() {
4609 let mut communication = InterAgentCommunication {
4610 id: Some(ResponseItemId::with_suffix("amsg", "1")),
4611 author: AgentPath::root(),
4612 recipient: AgentPath::root().join("reviewer").expect("recipient path"),
4613 other_recipients: vec![AgentPath::root().join("worker").expect("recipient path")],
4614 content: "review the diff".to_string(),
4615 encrypted_content: None,
4616 internal_chat_message_metadata_passthrough: None,
4617 trigger_turn: true,
4618 };
4619 communication.set_turn_id_if_missing("turn-1");
4620 let mut serialized_communication = communication.clone();
4621 serialized_communication.id = None;
4622 serialized_communication.internal_chat_message_metadata_passthrough = None;
4623
4624 assert_eq!(
4625 communication.to_response_input_item(),
4626 ResponseInputItem::Message {
4627 role: "assistant".to_string(),
4628 content: vec![ContentItem::OutputText {
4629 text: serde_json::to_string(&serialized_communication)
4630 .expect("serialize communication"),
4631 }],
4632 phase: Some(MessagePhase::Commentary),
4633 }
4634 );
4635 }
4636
4637 #[test]
4638 fn queued_encrypted_inter_agent_communication_renders_message_envelope() {
4639 let communication = InterAgentCommunication::new_encrypted(
4640 AgentPath::root().join("worker").expect("author path"),
4641 AgentPath::root(),
4642 Vec::new(),
4643 "encrypted payload".to_string(),
4644 false,
4645 );
4646
4647 assert_eq!(
4648 communication.to_model_input_item(),
4649 ResponseItem::AgentMessage {
4650 id: None,
4651 author: "/root/worker".to_string(),
4652 recipient: "/root".to_string(),
4653 content: vec![
4654 AgentMessageInputContent::InputText {
4655 text: "Message Type: MESSAGE\nTask name: /root\nSender: /root/worker\nPayload:\n"
4656 .to_string(),
4657 },
4658 AgentMessageInputContent::EncryptedContent {
4659 encrypted_content: "encrypted payload".to_string(),
4660 },
4661 ],
4662 internal_chat_message_metadata_passthrough: None,
4663 }
4664 );
4665 }
4666
4667 #[test]
4668 fn session_source_from_startup_arg_normalizes_custom_values() {
4669 assert_eq!(
4670 SessionSource::from_startup_arg("atlas").unwrap(),
4671 SessionSource::Custom("atlas".to_string())
4672 );
4673 assert_eq!(
4674 SessionSource::from_startup_arg(" Atlas ").unwrap(),
4675 SessionSource::Custom("atlas".to_string())
4676 );
4677 }
4678
4679 #[test]
4680 fn session_source_restriction_product_defaults_non_subagent_sources_to_codex() {
4681 assert_eq!(
4682 SessionSource::Cli.restriction_product(),
4683 Some(Product::Codex)
4684 );
4685 assert_eq!(
4686 SessionSource::VSCode.restriction_product(),
4687 Some(Product::Codex)
4688 );
4689 assert_eq!(
4690 SessionSource::Exec.restriction_product(),
4691 Some(Product::Codex)
4692 );
4693 assert_eq!(
4694 SessionSource::Mcp.restriction_product(),
4695 Some(Product::Codex)
4696 );
4697 assert_eq!(
4698 SessionSource::Unknown.restriction_product(),
4699 Some(Product::Codex)
4700 );
4701 }
4702
4703 #[test]
4704 fn session_source_restriction_product_does_not_guess_subagent_products() {
4705 assert_eq!(
4706 SessionSource::SubAgent(SubAgentSource::Review).restriction_product(),
4707 None
4708 );
4709 assert_eq!(
4710 SessionSource::Internal(InternalSessionSource::MemoryConsolidation)
4711 .restriction_product(),
4712 None
4713 );
4714 }
4715
4716 #[test]
4717 fn session_source_restriction_product_maps_custom_sources_to_products() {
4718 assert_eq!(
4719 SessionSource::Custom("chatgpt".to_string()).restriction_product(),
4720 Some(Product::Chatgpt)
4721 );
4722 assert_eq!(
4723 SessionSource::Custom("ATLAS".to_string()).restriction_product(),
4724 Some(Product::Atlas)
4725 );
4726 assert_eq!(
4727 SessionSource::Custom("codex".to_string()).restriction_product(),
4728 Some(Product::Codex)
4729 );
4730 assert_eq!(
4731 SessionSource::Custom("atlas-dev".to_string()).restriction_product(),
4732 None
4733 );
4734 }
4735
4736 #[test]
4737 fn session_source_matches_product_restriction() {
4738 assert!(
4739 SessionSource::Custom("chatgpt".to_string())
4740 .matches_product_restriction(&[Product::Chatgpt])
4741 );
4742 assert!(
4743 !SessionSource::Custom("chatgpt".to_string())
4744 .matches_product_restriction(&[Product::Codex])
4745 );
4746 assert!(SessionSource::VSCode.matches_product_restriction(&[Product::Codex]));
4747 assert!(
4748 !SessionSource::Custom("atlas-dev".to_string())
4749 .matches_product_restriction(&[Product::Atlas])
4750 );
4751 assert!(SessionSource::Custom("atlas-dev".to_string()).matches_product_restriction(&[]));
4752 }
4753
4754 fn sandbox_policy_probe_paths(policy: &SandboxPolicy, cwd: &Path) -> Vec<PathBuf> {
4755 let mut paths = vec![cwd.to_path_buf()];
4756 for root in policy.get_writable_roots_with_cwd(cwd) {
4757 paths.push(root.root.to_path_buf());
4758 paths.extend(
4759 root.read_only_subpaths
4760 .into_iter()
4761 .map(|path| path.to_path_buf()),
4762 );
4763 }
4764 paths.sort();
4765 paths.dedup();
4766 paths
4767 }
4768
4769 fn assert_same_sandbox_policy_semantics(
4770 expected: &SandboxPolicy,
4771 actual: &SandboxPolicy,
4772 cwd: &Path,
4773 ) {
4774 assert_eq!(
4775 actual.has_full_disk_read_access(),
4776 expected.has_full_disk_read_access()
4777 );
4778 assert_eq!(
4779 actual.has_full_disk_write_access(),
4780 expected.has_full_disk_write_access()
4781 );
4782 assert_eq!(
4783 actual.has_full_network_access(),
4784 expected.has_full_network_access()
4785 );
4786 let mut probe_paths = sandbox_policy_probe_paths(expected, cwd);
4787 probe_paths.extend(sandbox_policy_probe_paths(actual, cwd));
4788 probe_paths.sort();
4789 probe_paths.dedup();
4790
4791 for path in probe_paths {
4792 assert_eq!(
4793 sandbox_policy_allows_read(actual, &path, cwd),
4794 sandbox_policy_allows_read(expected, &path, cwd),
4795 "read access mismatch for {}",
4796 path.display()
4797 );
4798 assert_eq!(
4799 sandbox_policy_allows_write(actual, &path, cwd),
4800 sandbox_policy_allows_write(expected, &path, cwd),
4801 "write access mismatch for {}",
4802 path.display()
4803 );
4804 }
4805 }
4806
4807 #[test]
4808 fn external_sandbox_reports_full_access_flags() {
4809 let restricted = SandboxPolicy::ExternalSandbox {
4810 network_access: NetworkAccess::Restricted,
4811 };
4812 assert!(restricted.has_full_disk_write_access());
4813 assert!(!restricted.has_full_network_access());
4814
4815 let enabled = SandboxPolicy::ExternalSandbox {
4816 network_access: NetworkAccess::Enabled,
4817 };
4818 assert!(enabled.has_full_disk_write_access());
4819 assert!(enabled.has_full_network_access());
4820 }
4821
4822 #[test]
4823 fn read_only_reports_network_access_flags() {
4824 let restricted = SandboxPolicy::new_read_only_policy();
4825 assert!(!restricted.has_full_network_access());
4826
4827 let enabled = SandboxPolicy::ReadOnly {
4828 network_access: true,
4829 };
4830 assert!(enabled.has_full_network_access());
4831 }
4832
4833 #[test]
4834 fn granular_approval_config_mcp_elicitation_flag_is_field_driven() {
4835 assert!(
4836 GranularApprovalConfig {
4837 sandbox_approval: false,
4838 rules: false,
4839 skill_approval: false,
4840 request_permissions: false,
4841 mcp_elicitations: true,
4842 }
4843 .allows_mcp_elicitations()
4844 );
4845 assert!(
4846 !GranularApprovalConfig {
4847 sandbox_approval: false,
4848 rules: false,
4849 skill_approval: false,
4850 request_permissions: false,
4851 mcp_elicitations: false,
4852 }
4853 .allows_mcp_elicitations()
4854 );
4855 }
4856
4857 #[test]
4858 fn granular_approval_config_skill_approval_flag_is_field_driven() {
4859 assert!(
4860 GranularApprovalConfig {
4861 sandbox_approval: false,
4862 rules: false,
4863 skill_approval: true,
4864 request_permissions: false,
4865 mcp_elicitations: false,
4866 }
4867 .allows_skill_approval()
4868 );
4869 assert!(
4870 !GranularApprovalConfig {
4871 sandbox_approval: false,
4872 rules: false,
4873 skill_approval: false,
4874 request_permissions: false,
4875 mcp_elicitations: false,
4876 }
4877 .allows_skill_approval()
4878 );
4879 }
4880
4881 #[test]
4882 fn granular_approval_config_request_permissions_flag_is_field_driven() {
4883 assert!(
4884 GranularApprovalConfig {
4885 sandbox_approval: false,
4886 rules: false,
4887 skill_approval: false,
4888 request_permissions: true,
4889 mcp_elicitations: false,
4890 }
4891 .allows_request_permissions()
4892 );
4893 assert!(
4894 !GranularApprovalConfig {
4895 sandbox_approval: false,
4896 rules: false,
4897 skill_approval: false,
4898 request_permissions: false,
4899 mcp_elicitations: false,
4900 }
4901 .allows_request_permissions()
4902 );
4903 }
4904
4905 #[test]
4906 fn granular_approval_config_defaults_missing_optional_flags_to_false() {
4907 let decoded = serde_json::from_value::<GranularApprovalConfig>(serde_json::json!({
4908 "sandbox_approval": true,
4909 "rules": false,
4910 "mcp_elicitations": true,
4911 }))
4912 .expect("granular approval config should deserialize");
4913
4914 assert_eq!(
4915 decoded,
4916 GranularApprovalConfig {
4917 sandbox_approval: true,
4918 rules: false,
4919 skill_approval: false,
4920 request_permissions: false,
4921 mcp_elicitations: true,
4922 }
4923 );
4924 }
4925
4926 #[test]
4927 fn restricted_file_system_policy_reports_full_access_from_root_entries() {
4928 let read_only = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
4929 path: FileSystemPath::Special {
4930 value: FileSystemSpecialPath::Root,
4931 },
4932 access: FileSystemAccessMode::Read,
4933 missing_path_behavior: None,
4934 }]);
4935 assert!(read_only.has_full_disk_read_access());
4936 assert!(!read_only.has_full_disk_write_access());
4937 assert!(!read_only.include_platform_defaults());
4938
4939 let writable = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
4940 path: FileSystemPath::Special {
4941 value: FileSystemSpecialPath::Root,
4942 },
4943 access: FileSystemAccessMode::Write,
4944 missing_path_behavior: None,
4945 }]);
4946 assert!(writable.has_full_disk_read_access());
4947 assert!(writable.has_full_disk_write_access());
4948 }
4949
4950 #[test]
4951 fn restricted_file_system_policy_treats_root_with_carveouts_as_scoped_access() {
4952 let cwd = TempDir::new().expect("tempdir");
4953 let canonical_cwd = codex_utils_absolute_path::canonicalize_preserving_symlinks(cwd.path())
4954 .expect("canonicalize cwd");
4955 let root = AbsolutePathBuf::from_absolute_path(&canonical_cwd)
4956 .expect("absolute canonical tempdir")
4957 .as_path()
4958 .ancestors()
4959 .last()
4960 .and_then(|path| AbsolutePathBuf::from_absolute_path(path).ok())
4961 .expect("filesystem root");
4962 let blocked = AbsolutePathBuf::resolve_path_against_base("blocked", cwd.path());
4963 let expected_blocked = AbsolutePathBuf::from_absolute_path(
4964 codex_utils_absolute_path::canonicalize_preserving_symlinks(cwd.path())
4965 .expect("canonicalize cwd")
4966 .join("blocked"),
4967 )
4968 .expect("canonical blocked");
4969 let policy = FileSystemSandboxPolicy::restricted(vec![
4970 FileSystemSandboxEntry {
4971 path: FileSystemPath::Special {
4972 value: FileSystemSpecialPath::Root,
4973 },
4974 access: FileSystemAccessMode::Write,
4975 missing_path_behavior: None,
4976 },
4977 FileSystemSandboxEntry {
4978 path: FileSystemPath::Path { path: blocked },
4979 access: FileSystemAccessMode::Deny,
4980 missing_path_behavior: None,
4981 },
4982 ]);
4983
4984 assert!(!policy.has_full_disk_read_access());
4985 assert!(!policy.has_full_disk_write_access());
4986 assert_eq!(
4987 policy.get_readable_roots_with_cwd(cwd.path()),
4988 vec![root.clone()]
4989 );
4990 assert_eq!(
4991 policy.get_unreadable_roots_with_cwd(cwd.path()),
4992 vec![expected_blocked.clone()]
4993 );
4994
4995 let writable_roots = policy.get_writable_roots_with_cwd(cwd.path());
4996 assert_eq!(writable_roots.len(), 1);
4997 assert_eq!(writable_roots[0].root, root);
4998 assert!(
4999 writable_roots[0]
5000 .read_only_subpaths
5001 .iter()
5002 .any(|path| path.as_path() == expected_blocked.as_path())
5003 );
5004 }
5005
5006 #[test]
5007 fn restricted_file_system_policy_derives_effective_paths() {
5008 let cwd = TempDir::new().expect("tempdir");
5009 std::fs::create_dir_all(cwd.path().join(".agents")).expect("create .agents");
5010 std::fs::create_dir_all(cwd.path().join(".codex")).expect("create .codex");
5011 let canonical_cwd = codex_utils_absolute_path::canonicalize_preserving_symlinks(cwd.path())
5012 .expect("canonicalize cwd");
5013 let cwd_absolute =
5014 AbsolutePathBuf::from_absolute_path(&canonical_cwd).expect("absolute tempdir");
5015 let secret = AbsolutePathBuf::resolve_path_against_base("secret", cwd.path());
5016 let expected_secret = AbsolutePathBuf::from_absolute_path(canonical_cwd.join("secret"))
5017 .expect("canonical secret");
5018 let expected_agents = AbsolutePathBuf::from_absolute_path(canonical_cwd.join(".agents"))
5019 .expect("canonical .agents");
5020 let expected_codex = AbsolutePathBuf::from_absolute_path(canonical_cwd.join(".codex"))
5021 .expect("canonical .codex");
5022 let policy = FileSystemSandboxPolicy::restricted(vec![
5023 FileSystemSandboxEntry {
5024 path: FileSystemPath::Special {
5025 value: FileSystemSpecialPath::Minimal,
5026 },
5027 access: FileSystemAccessMode::Read,
5028 missing_path_behavior: None,
5029 },
5030 FileSystemSandboxEntry {
5031 path: FileSystemPath::Special {
5032 value: FileSystemSpecialPath::project_roots(None),
5033 },
5034 access: FileSystemAccessMode::Write,
5035 missing_path_behavior: None,
5036 },
5037 FileSystemSandboxEntry {
5038 path: FileSystemPath::Path { path: secret },
5039 access: FileSystemAccessMode::Deny,
5040 missing_path_behavior: None,
5041 },
5042 ]);
5043
5044 assert!(!policy.has_full_disk_read_access());
5045 assert!(!policy.has_full_disk_write_access());
5046 assert!(policy.include_platform_defaults());
5047 assert_eq!(
5048 policy.get_readable_roots_with_cwd(cwd.path()),
5049 vec![cwd_absolute.clone()]
5050 );
5051 assert_eq!(
5052 policy.get_unreadable_roots_with_cwd(cwd.path()),
5053 vec![expected_secret.clone()]
5054 );
5055
5056 let writable_roots = policy.get_writable_roots_with_cwd(cwd.path());
5057 assert_eq!(writable_roots.len(), 1);
5058 assert_eq!(writable_roots[0].root, cwd_absolute);
5059 assert!(
5060 writable_roots[0]
5061 .read_only_subpaths
5062 .iter()
5063 .any(|path| path.as_path() == expected_secret.as_path())
5064 );
5065 assert!(
5066 writable_roots[0]
5067 .read_only_subpaths
5068 .iter()
5069 .any(|path| path.as_path() == expected_agents.as_path())
5070 );
5071 assert!(
5072 writable_roots[0]
5073 .read_only_subpaths
5074 .iter()
5075 .any(|path| path.as_path() == expected_codex.as_path())
5076 );
5077 }
5078
5079 #[test]
5080 fn restricted_file_system_policy_treats_read_entries_as_read_only_subpaths() {
5081 let cwd = TempDir::new().expect("tempdir");
5082 let canonical_cwd = codex_utils_absolute_path::canonicalize_preserving_symlinks(cwd.path())
5083 .expect("canonicalize cwd");
5084 let docs = AbsolutePathBuf::resolve_path_against_base("docs", cwd.path());
5085 let docs_public = AbsolutePathBuf::resolve_path_against_base("docs/public", cwd.path());
5086 let expected_docs = AbsolutePathBuf::from_absolute_path(canonical_cwd.join("docs"))
5087 .expect("canonical docs");
5088 let expected_docs_public =
5089 AbsolutePathBuf::from_absolute_path(canonical_cwd.join("docs/public"))
5090 .expect("canonical docs/public");
5091 let expected_dot_codex = AbsolutePathBuf::from_absolute_path(canonical_cwd.join(".codex"))
5092 .expect("canonical .codex");
5093 let policy = FileSystemSandboxPolicy::restricted(vec![
5094 FileSystemSandboxEntry {
5095 path: FileSystemPath::Special {
5096 value: FileSystemSpecialPath::project_roots(None),
5097 },
5098 access: FileSystemAccessMode::Write,
5099 missing_path_behavior: None,
5100 },
5101 FileSystemSandboxEntry {
5102 path: FileSystemPath::Path { path: docs },
5103 access: FileSystemAccessMode::Read,
5104 missing_path_behavior: None,
5105 },
5106 FileSystemSandboxEntry {
5107 path: FileSystemPath::Path { path: docs_public },
5108 access: FileSystemAccessMode::Write,
5109 missing_path_behavior: None,
5110 },
5111 ]);
5112
5113 assert!(!policy.has_full_disk_write_access());
5114 assert_eq!(
5115 sorted_writable_roots(policy.get_writable_roots_with_cwd(cwd.path())),
5116 vec![
5117 (
5118 canonical_cwd,
5119 vec![
5120 expected_dot_codex.to_path_buf(),
5121 expected_docs.to_path_buf()
5122 ],
5123 ),
5124 (expected_docs_public.to_path_buf(), Vec::new()),
5125 ]
5126 );
5127 }
5128
5129 #[test]
5130 fn file_system_policy_rejects_legacy_bridge_for_non_workspace_writes() {
5131 let cwd = if cfg!(windows) {
5132 Path::new(r"C:\workspace")
5133 } else {
5134 Path::new("/tmp/workspace")
5135 };
5136 let external_write_path = if cfg!(windows) {
5137 AbsolutePathBuf::from_absolute_path(r"C:\temp").expect("absolute windows temp path")
5138 } else {
5139 AbsolutePathBuf::from_absolute_path("/tmp").expect("absolute tmp path")
5140 };
5141 let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
5142 path: FileSystemPath::Path {
5143 path: external_write_path,
5144 },
5145 access: FileSystemAccessMode::Write,
5146 missing_path_behavior: None,
5147 }]);
5148
5149 let err = policy
5150 .to_legacy_sandbox_policy(NetworkSandboxPolicy::Restricted, cwd)
5151 .expect_err("non-workspace writes should be rejected");
5152
5153 assert!(
5154 err.to_string()
5155 .contains("filesystem writes outside the workspace root"),
5156 "{err}"
5157 );
5158 }
5159
5160 #[test]
5161 fn legacy_sandbox_policy_semantics_survive_split_bridge() {
5162 let cwd = TempDir::new().expect("tempdir");
5163 let writable_root = AbsolutePathBuf::resolve_path_against_base("writable", cwd.path());
5164 let policies = [
5165 SandboxPolicy::DangerFullAccess,
5166 SandboxPolicy::ExternalSandbox {
5167 network_access: NetworkAccess::Restricted,
5168 },
5169 SandboxPolicy::ExternalSandbox {
5170 network_access: NetworkAccess::Enabled,
5171 },
5172 SandboxPolicy::ReadOnly {
5173 network_access: false,
5174 },
5175 SandboxPolicy::WorkspaceWrite {
5176 writable_roots: vec![],
5177 network_access: false,
5178 exclude_tmpdir_env_var: true,
5179 exclude_slash_tmp: true,
5180 },
5181 SandboxPolicy::WorkspaceWrite {
5182 writable_roots: vec![writable_root],
5183 network_access: true,
5184 exclude_tmpdir_env_var: false,
5185 exclude_slash_tmp: true,
5186 },
5187 ];
5188
5189 for expected in policies {
5190 let actual =
5191 FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&expected, cwd.path())
5192 .to_legacy_sandbox_policy(NetworkSandboxPolicy::from(&expected), cwd.path())
5193 .expect("legacy bridge should preserve legacy policy semantics");
5194
5195 assert_same_sandbox_policy_semantics(&expected, &actual, cwd.path());
5196 }
5197 }
5198
5199 #[test]
5200 fn item_started_event_from_web_search_emits_begin_event() {
5201 let event = ItemStartedEvent {
5202 thread_id: ThreadId::new(),
5203 turn_id: "turn-1".into(),
5204 item: TurnItem::WebSearch(WebSearchItem {
5205 id: "search-1".into(),
5206 query: "find docs".into(),
5207 action: WebSearchAction::Search {
5208 query: Some("find docs".into()),
5209 queries: None,
5210 },
5211 results: None,
5212 }),
5213 started_at_ms: 0,
5214 };
5215
5216 let legacy_events = event.as_legacy_events(false);
5217 assert_eq!(legacy_events.len(), 1);
5218 match &legacy_events[0] {
5219 EventMsg::WebSearchBegin(event) => assert_eq!(event.call_id, "search-1"),
5220 _ => panic!("expected WebSearchBegin event"),
5221 }
5222 }
5223
5224 #[test]
5225 fn item_started_event_from_non_web_search_emits_no_legacy_events() {
5226 let event = ItemStartedEvent {
5227 thread_id: ThreadId::new(),
5228 turn_id: "turn-1".into(),
5229 item: TurnItem::UserMessage(UserMessageItem::new(&[])),
5230 started_at_ms: 0,
5231 };
5232
5233 assert!(
5234 event
5235 .as_legacy_events(false)
5236 .is_empty()
5237 );
5238 }
5239
5240 #[test]
5241 fn item_started_event_from_image_generation_emits_begin_event() {
5242 let event = ItemStartedEvent {
5243 thread_id: ThreadId::new(),
5244 turn_id: "turn-1".into(),
5245 item: TurnItem::ImageGeneration(ImageGenerationItem {
5246 id: "ig-1".into(),
5247 status: "in_progress".into(),
5248 revised_prompt: None,
5249 result: String::new(),
5250 saved_path: None,
5251 }),
5252 started_at_ms: 0,
5253 };
5254
5255 let legacy_events = event.as_legacy_events(false);
5256 assert_eq!(legacy_events.len(), 1);
5257 match &legacy_events[0] {
5258 EventMsg::ImageGenerationBegin(event) => assert_eq!(event.call_id, "ig-1"),
5259 _ => panic!("expected ImageGenerationBegin event"),
5260 }
5261 }
5262
5263 #[test]
5264 fn item_started_event_from_file_change_emits_patch_begin_event() {
5265 let event = ItemStartedEvent {
5266 thread_id: ThreadId::new(),
5267 turn_id: "turn-1".into(),
5268 started_at_ms: 0,
5269 item: TurnItem::FileChange(FileChangeItem {
5270 id: "patch-1".into(),
5271 changes: [(
5272 PathBuf::from("new.txt"),
5273 FileChange::Add {
5274 content: "hello".into(),
5275 },
5276 )]
5277 .into_iter()
5278 .collect(),
5279 status: None,
5280 auto_approved: Some(true),
5281 stdout: None,
5282 stderr: None,
5283 }),
5284 };
5285
5286 let legacy_events = event.as_legacy_events(false);
5287 assert_eq!(legacy_events.len(), 1);
5288 match &legacy_events[0] {
5289 EventMsg::PatchApplyBegin(event) => {
5290 assert_eq!(event.call_id, "patch-1");
5291 assert_eq!(event.turn_id, "turn-1");
5292 assert!(event.auto_approved);
5293 assert!(event.changes.contains_key(&PathBuf::from("new.txt")));
5294 }
5295 _ => panic!("expected PatchApplyBegin event"),
5296 }
5297 }
5298
5299 #[test]
5300 fn item_started_event_from_mcp_tool_call_emits_begin_event() {
5301 let event = ItemStartedEvent {
5302 thread_id: ThreadId::new(),
5303 turn_id: "turn-1".into(),
5304 started_at_ms: 0,
5305 item: TurnItem::McpToolCall(McpToolCallItem {
5306 id: "mcp-1".into(),
5307 server: "server".into(),
5308 tool: "tool".into(),
5309 arguments: json!({"arg": "value"}),
5310 connector_id: Some("connector".into()),
5311 mcp_app_resource_uri: Some("app://connector".into()),
5312 link_id: Some("link_123".into()),
5313 app_name: Some("Calendar".into()),
5314 action_name: Some("create_event".into()),
5315 plugin_id: Some("sample@test".into()),
5316 status: McpToolCallStatus::InProgress,
5317 result: None,
5318 error: None,
5319 duration: None,
5320 }),
5321 };
5322
5323 let legacy_events = event.as_legacy_events(false);
5324 assert_eq!(legacy_events.len(), 1);
5325 match &legacy_events[0] {
5326 EventMsg::McpToolCallBegin(event) => {
5327 assert_eq!(event.call_id, "mcp-1");
5328 assert_eq!(event.invocation.server, "server");
5329 assert_eq!(event.invocation.tool, "tool");
5330 assert_eq!(event.connector_id.as_deref(), Some("connector"));
5331 assert_eq!(
5332 event.mcp_app_resource_uri.as_deref(),
5333 Some("app://connector")
5334 );
5335 assert_eq!(event.link_id.as_deref(), Some("link_123"));
5336 assert_eq!(event.app_name.as_deref(), Some("Calendar"));
5337 assert_eq!(event.action_name.as_deref(), Some("create_event"));
5338 assert_eq!(event.plugin_id.as_deref(), Some("sample@test"));
5339 }
5340 _ => panic!("expected McpToolCallBegin event"),
5341 }
5342 }
5343
5344 #[test]
5345 fn item_completed_event_from_image_generation_emits_end_event() {
5346 let event = ItemCompletedEvent {
5347 thread_id: ThreadId::new(),
5348 turn_id: "turn-1".into(),
5349 item: TurnItem::ImageGeneration(ImageGenerationItem {
5350 id: "ig-1".into(),
5351 status: "completed".into(),
5352 revised_prompt: Some("A tiny blue square".into()),
5353 result: "Zm9v".into(),
5354 saved_path: Some(test_path_buf("/tmp/ig-1.png").abs()),
5355 }),
5356 completed_at_ms: 0,
5357 };
5358
5359 let legacy_events = event.as_legacy_events(false);
5360 assert_eq!(legacy_events.len(), 1);
5361 match &legacy_events[0] {
5362 EventMsg::ImageGenerationEnd(event) => {
5363 assert_eq!(event.call_id, "ig-1");
5364 assert_eq!(event.status, "completed");
5365 assert_eq!(event.revised_prompt.as_deref(), Some("A tiny blue square"));
5366 assert_eq!(event.result, "Zm9v");
5367 assert_eq!(
5368 event.saved_path.as_ref().map(AbsolutePathBuf::as_path),
5369 Some(test_path_buf("/tmp/ig-1.png").as_path())
5370 );
5371 }
5372 _ => panic!("expected ImageGenerationEnd event"),
5373 }
5374 }
5375
5376 #[test]
5377 fn item_completed_event_from_file_change_emits_patch_end_event() {
5378 let event = ItemCompletedEvent {
5379 thread_id: ThreadId::new(),
5380 turn_id: "turn-1".into(),
5381 completed_at_ms: 0,
5382 item: TurnItem::FileChange(FileChangeItem {
5383 id: "patch-1".into(),
5384 changes: [(
5385 PathBuf::from("new.txt"),
5386 FileChange::Add {
5387 content: "hello".into(),
5388 },
5389 )]
5390 .into_iter()
5391 .collect(),
5392 status: Some(PatchApplyStatus::Completed),
5393 auto_approved: None,
5394 stdout: Some("Done!".into()),
5395 stderr: Some(String::new()),
5396 }),
5397 };
5398
5399 let legacy_events = event.as_legacy_events(false);
5400 assert_eq!(legacy_events.len(), 1);
5401 match &legacy_events[0] {
5402 EventMsg::PatchApplyEnd(event) => {
5403 assert_eq!(event.call_id, "patch-1");
5404 assert_eq!(event.turn_id, "turn-1");
5405 assert_eq!(event.stdout, "Done!");
5406 assert!(event.success);
5407 assert_eq!(event.status, PatchApplyStatus::Completed);
5408 assert!(event.changes.contains_key(&PathBuf::from("new.txt")));
5409 }
5410 _ => panic!("expected PatchApplyEnd event"),
5411 }
5412 }
5413
5414 #[test]
5415 fn item_completed_event_from_mcp_tool_call_emits_end_event() {
5416 let event = ItemCompletedEvent {
5417 thread_id: ThreadId::new(),
5418 turn_id: "turn-1".into(),
5419 completed_at_ms: 0,
5420 item: TurnItem::McpToolCall(McpToolCallItem {
5421 id: "mcp-1".into(),
5422 server: "server".into(),
5423 tool: "tool".into(),
5424 arguments: json!({"arg": "value"}),
5425 connector_id: Some("connector".into()),
5426 mcp_app_resource_uri: Some("app://connector".into()),
5427 link_id: Some("link_123".into()),
5428 app_name: Some("Calendar".into()),
5429 action_name: Some("create_event".into()),
5430 plugin_id: Some("sample@test".into()),
5431 status: McpToolCallStatus::Completed,
5432 result: Some(CallToolResult {
5433 content: vec![json!({"type": "text", "text": "ok"})],
5434 structured_content: None,
5435 is_error: Some(false),
5436 meta: None,
5437 }),
5438 error: None,
5439 duration: Some(Duration::from_millis(42)),
5440 }),
5441 };
5442
5443 let legacy_events = event.as_legacy_events(false);
5444 assert_eq!(legacy_events.len(), 1);
5445 match &legacy_events[0] {
5446 EventMsg::McpToolCallEnd(event) => {
5447 assert_eq!(event.call_id, "mcp-1");
5448 assert_eq!(event.invocation.server, "server");
5449 assert_eq!(event.invocation.tool, "tool");
5450 assert_eq!(event.connector_id.as_deref(), Some("connector"));
5451 assert_eq!(
5452 event.mcp_app_resource_uri.as_deref(),
5453 Some("app://connector")
5454 );
5455 assert_eq!(event.link_id.as_deref(), Some("link_123"));
5456 assert_eq!(event.app_name.as_deref(), Some("Calendar"));
5457 assert_eq!(event.action_name.as_deref(), Some("create_event"));
5458 assert_eq!(event.plugin_id.as_deref(), Some("sample@test"));
5459 assert_eq!(event.duration, Duration::from_millis(42));
5460 assert!(event.is_success());
5461 }
5462 _ => panic!("expected McpToolCallEnd event"),
5463 }
5464 }
5465
5466 #[test]
5467 fn command_execution_item_lifecycle_emits_legacy_exec_events() {
5468 let cwd = PathUri::from_abs_path(&test_path_buf("/tmp").abs());
5469 let started = ItemStartedEvent {
5470 thread_id: ThreadId::new(),
5471 turn_id: "turn-1".into(),
5472 started_at_ms: 10,
5473 item: TurnItem::CommandExecution(CommandExecutionItem {
5474 id: "exec-1".into(),
5475 process_id: Some("pid-1".into()),
5476 command: vec!["echo".into(), "done".into()],
5477 cwd: cwd.clone(),
5478 parsed_cmd: vec![ParsedCommand::Unknown {
5479 cmd: "echo done".into(),
5480 }],
5481 source: ExecCommandSource::Agent,
5482 interaction_input: None,
5483 status: CommandExecutionStatus::InProgress,
5484 stdout: None,
5485 stderr: None,
5486 aggregated_output: None,
5487 exit_code: None,
5488 duration: None,
5489 formatted_output: None,
5490 }),
5491 };
5492 let completed = ItemCompletedEvent {
5493 thread_id: ThreadId::new(),
5494 turn_id: "turn-1".into(),
5495 completed_at_ms: 20,
5496 item: TurnItem::CommandExecution(CommandExecutionItem {
5497 id: "exec-1".into(),
5498 process_id: Some("pid-1".into()),
5499 command: vec!["echo".into(), "done".into()],
5500 cwd,
5501 parsed_cmd: vec![ParsedCommand::Unknown {
5502 cmd: "echo done".into(),
5503 }],
5504 source: ExecCommandSource::Agent,
5505 interaction_input: None,
5506 status: CommandExecutionStatus::Completed,
5507 stdout: Some("done\n".into()),
5508 stderr: Some(String::new()),
5509 aggregated_output: Some("done\n".into()),
5510 exit_code: Some(0),
5511 duration: Some(Duration::from_millis(5)),
5512 formatted_output: Some("done\n".into()),
5513 }),
5514 };
5515
5516 assert!(matches!(
5517 started.as_legacy_events(false).as_slice(),
5518 [EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
5519 call_id,
5520 turn_id,
5521 started_at_ms: 10,
5522 ..
5523 })] if call_id == "exec-1" && turn_id == "turn-1"
5524 ));
5525 assert!(matches!(
5526 completed
5527 .as_legacy_events(false)
5528 .as_slice(),
5529 [EventMsg::ExecCommandEnd(ExecCommandEndEvent {
5530 call_id,
5531 turn_id,
5532 completed_at_ms: 20,
5533 aggregated_output,
5534 ..
5535 })] if call_id == "exec-1" && turn_id == "turn-1" && aggregated_output == "done\n"
5536 ));
5537 }
5538
5539 #[test]
5540 fn dynamic_tool_call_item_lifecycle_emits_legacy_dynamic_tool_events() {
5541 let started = ItemStartedEvent {
5542 thread_id: ThreadId::new(),
5543 turn_id: "turn-1".into(),
5544 started_at_ms: 10,
5545 item: TurnItem::DynamicToolCall(DynamicToolCallItem {
5546 id: "dynamic-1".into(),
5547 namespace: Some("apps".into()),
5548 tool: "lookup".into(),
5549 arguments: json!({"id": "123"}),
5550 status: DynamicToolCallStatus::InProgress,
5551 content_items: None,
5552 success: None,
5553 error: None,
5554 duration: None,
5555 }),
5556 };
5557 let completed = ItemCompletedEvent {
5558 thread_id: ThreadId::new(),
5559 turn_id: "turn-1".into(),
5560 completed_at_ms: 20,
5561 item: TurnItem::DynamicToolCall(DynamicToolCallItem {
5562 id: "dynamic-1".into(),
5563 namespace: Some("apps".into()),
5564 tool: "lookup".into(),
5565 arguments: json!({"id": "123"}),
5566 status: DynamicToolCallStatus::Completed,
5567 content_items: Some(vec![DynamicToolCallOutputContentItem::InputText {
5568 text: "ok".into(),
5569 }]),
5570 success: Some(true),
5571 error: None,
5572 duration: Some(Duration::from_millis(5)),
5573 }),
5574 };
5575
5576 assert!(matches!(
5577 started.as_legacy_events(false).as_slice(),
5578 [EventMsg::DynamicToolCallRequest(DynamicToolCallRequest {
5579 call_id,
5580 turn_id,
5581 started_at_ms: 10,
5582 ..
5583 })] if call_id == "dynamic-1" && turn_id == "turn-1"
5584 ));
5585 assert!(matches!(
5586 completed
5587 .as_legacy_events(false)
5588 .as_slice(),
5589 [EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent {
5590 call_id,
5591 turn_id,
5592 completed_at_ms: 20,
5593 success: true,
5594 ..
5595 })] if call_id == "dynamic-1" && turn_id == "turn-1"
5596 ));
5597 }
5598
5599 #[test]
5600 fn review_mode_item_completion_emits_legacy_events_with_ids() {
5601 let entered = ItemCompletedEvent {
5602 thread_id: ThreadId::new(),
5603 turn_id: "turn-1".into(),
5604 completed_at_ms: 0,
5605 item: TurnItem::EnteredReviewMode(EnteredReviewModeItem {
5606 id: "entered-review".into(),
5607 target: ReviewTarget::Custom {
5608 instructions: "review this".into(),
5609 },
5610 user_facing_hint: "Review requested.".into(),
5611 }),
5612 };
5613 let exited = ItemCompletedEvent {
5614 thread_id: ThreadId::new(),
5615 turn_id: "turn-1".into(),
5616 completed_at_ms: 0,
5617 item: TurnItem::ExitedReviewMode(ExitedReviewModeItem {
5618 id: "exited-review".into(),
5619 review_output: Some(ReviewOutputEvent {
5620 overall_explanation: "Looks good.".into(),
5621 ..Default::default()
5622 }),
5623 }),
5624 };
5625
5626 assert!(matches!(
5627 entered
5628 .as_legacy_events(false)
5629 .as_slice(),
5630 [EventMsg::EnteredReviewMode(EnteredReviewModeEvent {
5631 target: ReviewTarget::Custom { instructions },
5632 user_facing_hint: Some(user_facing_hint),
5633 turn_id: Some(turn_id),
5634 item_id: Some(item_id),
5635 })]
5636 if instructions == "review this"
5637 && user_facing_hint == "Review requested."
5638 && turn_id == "turn-1"
5639 && item_id == "entered-review"
5640 ));
5641 assert!(matches!(
5642 exited
5643 .as_legacy_events(false)
5644 .as_slice(),
5645 [EventMsg::ExitedReviewMode(ExitedReviewModeEvent {
5646 turn_id: Some(turn_id),
5647 item_id: Some(item_id),
5648 review_output: Some(review_output),
5649 })]
5650 if turn_id == "turn-1"
5651 && item_id == "exited-review"
5652 && review_output.overall_explanation == "Looks good."
5653 ));
5654 }
5655
5656 #[test]
5657 fn item_started_event_requires_started_at_ms() {
5658 let mut value = serde_json::to_value(ItemStartedEvent {
5659 thread_id: ThreadId::new(),
5660 turn_id: "turn-1".into(),
5661 item: TurnItem::UserMessage(UserMessageItem::new(&[])),
5662 started_at_ms: 123,
5663 })
5664 .unwrap();
5665 value.as_object_mut().unwrap().remove("started_at_ms");
5666
5667 assert!(serde_json::from_value::<ItemStartedEvent>(value).is_err());
5668 }
5669
5670 #[test]
5671 fn item_completed_event_defaults_missing_completed_at_ms() {
5672 let mut value = serde_json::to_value(ItemCompletedEvent {
5673 thread_id: ThreadId::new(),
5674 turn_id: "turn-1".into(),
5675 item: TurnItem::UserMessage(UserMessageItem::new(&[])),
5676 completed_at_ms: 123,
5677 })
5678 .unwrap();
5679 value.as_object_mut().unwrap().remove("completed_at_ms");
5680
5681 let event = serde_json::from_value::<ItemCompletedEvent>(value).unwrap();
5682 assert_eq!(event.completed_at_ms, 0);
5683 }
5684
5685 #[test]
5686 fn review_mode_events_deserialize_legacy_payloads() {
5687 let entered = serde_json::from_value::<EnteredReviewModeEvent>(json!({
5688 "target": {
5689 "type": "custom",
5690 "instructions": "review this"
5691 },
5692 "user_facing_hint": "hint"
5693 }))
5694 .unwrap();
5695 assert_eq!(entered.turn_id, None);
5696 assert_eq!(entered.item_id, None);
5697
5698 let exited = serde_json::from_value::<ExitedReviewModeEvent>(json!({
5699 "review_output": null
5700 }))
5701 .unwrap();
5702 assert_eq!(exited.turn_id, None);
5703 assert_eq!(exited.item_id, None);
5704 }
5705
5706 #[test]
5707 fn rollback_failed_error_does_not_affect_turn_status() {
5708 let event = ErrorEvent {
5709 message: "rollback failed".into(),
5710 codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
5711 };
5712 assert!(!event.affects_turn_status());
5713 }
5714
5715 #[test]
5716 fn active_turn_not_steerable_error_does_not_affect_turn_status() {
5717 let event = ErrorEvent {
5718 message: "cannot steer a review turn".into(),
5719 codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable {
5720 turn_kind: NonSteerableTurnKind::Review,
5721 }),
5722 };
5723 assert!(!event.affects_turn_status());
5724 }
5725
5726 #[test]
5727 fn generic_error_affects_turn_status() {
5728 let event = ErrorEvent {
5729 message: "generic".into(),
5730 codex_error_info: Some(CodexErrorInfo::Other),
5731 };
5732 assert!(event.affects_turn_status());
5733 }
5734
5735 #[test]
5736 fn realtime_conversation_started_event_uses_realtime_session_id() {
5737 let event = RealtimeConversationStartedEvent {
5738 realtime_session_id: Some("conv_1".to_string()),
5739 version: RealtimeConversationVersion::V2,
5740 };
5741
5742 assert_eq!(
5743 serde_json::to_value(&event).unwrap(),
5744 json!({
5745 "realtime_session_id": "conv_1",
5746 "version": "v2"
5747 })
5748 );
5749 }
5750
5751 #[test]
5752 fn realtime_voice_list_is_stable() {
5753 assert_eq!(
5754 RealtimeVoicesList::builtin(),
5755 RealtimeVoicesList {
5756 v1: vec![
5757 RealtimeVoice::Juniper,
5758 RealtimeVoice::Maple,
5759 RealtimeVoice::Spruce,
5760 RealtimeVoice::Ember,
5761 RealtimeVoice::Vale,
5762 RealtimeVoice::Breeze,
5763 RealtimeVoice::Arbor,
5764 RealtimeVoice::Sol,
5765 RealtimeVoice::Cove,
5766 ],
5767 v2: vec![
5768 RealtimeVoice::Alloy,
5769 RealtimeVoice::Ash,
5770 RealtimeVoice::Ballad,
5771 RealtimeVoice::Coral,
5772 RealtimeVoice::Echo,
5773 RealtimeVoice::Sage,
5774 RealtimeVoice::Shimmer,
5775 RealtimeVoice::Verse,
5776 RealtimeVoice::Marin,
5777 RealtimeVoice::Cedar,
5778 ],
5779 default_v1: RealtimeVoice::Cove,
5780 default_v2: RealtimeVoice::Marin,
5781 }
5782 );
5783 }
5784
5785 #[test]
5786 fn user_input_text_serializes_empty_text_elements() -> Result<()> {
5787 let input = UserInput::Text {
5788 text: "hello".to_string(),
5789 text_elements: Vec::new(),
5790 };
5791
5792 let json_input = serde_json::to_value(input)?;
5793 assert_eq!(
5794 json_input,
5795 json!({
5796 "type": "text",
5797 "text": "hello",
5798 "text_elements": [],
5799 })
5800 );
5801
5802 Ok(())
5803 }
5804
5805 #[test]
5806 fn user_message_event_serializes_empty_metadata_vectors() -> Result<()> {
5807 let event = UserMessageEvent {
5808 client_id: None,
5809 message: "hello".to_string(),
5810 images: None,
5811 local_images: Vec::new(),
5812 text_elements: Vec::new(),
5813 ..Default::default()
5814 };
5815
5816 let json_event = serde_json::to_value(event)?;
5817 assert_eq!(
5818 json_event,
5819 json!({
5820 "message": "hello",
5821 "local_images": [],
5822 "local_audio": [],
5823 "text_elements": [],
5824 })
5825 );
5826
5827 Ok(())
5828 }
5829
5830 #[test]
5831 fn user_message_event_deserializes_without_image_detail_fields() -> Result<()> {
5832 let event: UserMessageEvent = serde_json::from_value(json!({
5833 "message": "hello",
5834 "images": ["https://example.com/image.png"],
5835 "local_images": ["/tmp/local.png"],
5836 "text_elements": [],
5837 }))?;
5838
5839 assert_eq!(event.message, "hello");
5840 assert_eq!(
5841 event.images,
5842 Some(vec!["https://example.com/image.png".to_string()])
5843 );
5844 assert_eq!(event.image_details, Vec::<Option<ImageDetail>>::new());
5845 assert_eq!(event.local_images, vec![PathBuf::from("/tmp/local.png")]);
5846 assert_eq!(event.local_image_details, Vec::<Option<ImageDetail>>::new());
5847 assert_eq!(event.audio, None);
5848 assert_eq!(event.local_audio, Vec::<PathBuf>::new());
5849 assert_eq!(event.text_elements, Vec::new());
5850
5851 Ok(())
5852 }
5853
5854 #[test]
5855 fn user_message_item_legacy_event_preserves_attachments() {
5856 let local_path = PathBuf::from("/tmp/local.png");
5857 let local_audio_path = PathBuf::from("/tmp/local.wav");
5858 let mut item = UserMessageItem::new(&[
5859 crate::user_input::UserInput::Image {
5860 image_url: "https://example.com/first.png".to_string(),
5861 detail: Some(ImageDetail::Original),
5862 },
5863 crate::user_input::UserInput::Image {
5864 image_url: "https://example.com/second.png".to_string(),
5865 detail: None,
5866 },
5867 crate::user_input::UserInput::LocalImage {
5868 path: local_path.clone(),
5869 detail: Some(ImageDetail::Original),
5870 },
5871 crate::user_input::UserInput::Audio {
5872 audio_url: "https://example.com/remote.mp3".to_string(),
5873 },
5874 crate::user_input::UserInput::LocalAudio {
5875 path: local_audio_path.clone(),
5876 },
5877 ]);
5878 item.client_id = Some("client-message-1".to_string());
5879
5880 let EventMsg::UserMessage(event) = item.as_legacy_event() else {
5881 panic!("expected user message event");
5882 };
5883
5884 assert_eq!(
5885 event.images,
5886 Some(vec![
5887 "https://example.com/first.png".to_string(),
5888 "https://example.com/second.png".to_string(),
5889 ])
5890 );
5891 assert_eq!(event.client_id, Some("client-message-1".to_string()));
5892 assert_eq!(event.image_details, vec![Some(ImageDetail::Original)]);
5893 assert_eq!(event.local_images, vec![local_path]);
5894 assert_eq!(event.local_image_details, vec![Some(ImageDetail::Original)]);
5895 assert_eq!(
5896 event.audio,
5897 Some(vec!["https://example.com/remote.mp3".to_string()])
5898 );
5899 assert_eq!(event.local_audio, vec![local_audio_path]);
5900 }
5901
5902 #[test]
5903 fn audio_only_user_message_has_placeholder_preview() {
5904 let event = UserMessageEvent {
5905 audio: Some(vec!["https://example.com/remote.mp3".to_string()]),
5906 ..Default::default()
5907 };
5908
5909 assert_eq!(user_message_preview(&event), Some("[Audio]".to_string()));
5910 }
5911
5912 #[test]
5913 fn turn_aborted_event_deserializes_without_turn_id() -> Result<()> {
5914 let event: EventMsg = serde_json::from_value(json!({
5915 "type": "turn_aborted",
5916 "reason": "interrupted",
5917 }))?;
5918
5919 match event {
5920 EventMsg::TurnAborted(TurnAbortedEvent {
5921 turn_id, reason, ..
5922 }) => {
5923 assert_eq!(turn_id, None);
5924 assert_eq!(reason, TurnAbortReason::Interrupted);
5925 }
5926 _ => panic!("expected turn_aborted event"),
5927 }
5928
5929 Ok(())
5930 }
5931
5932 #[test]
5933 fn session_meta_defaults_legacy_history_mode() -> Result<()> {
5934 let session_meta: SessionMeta = serde_json::from_value(json!({
5935 "session_id": "00000000-0000-0000-0000-000000000001",
5936 "id": "00000000-0000-0000-0000-000000000001",
5937 "timestamp": "2026-01-01T00:00:00Z",
5938 "cwd": "/tmp",
5939 "originator": "codex",
5940 "cli_version": "0.0.0",
5941 "model_provider": null,
5942 "base_instructions": null
5943 }))?;
5944
5945 assert_eq!(session_meta.history_mode, ThreadHistoryMode::Legacy);
5946 assert_eq!(session_meta.history_base, None);
5947 let serialized = serde_json::to_value(&session_meta)?;
5948 assert_eq!(serialized["history_mode"], json!("legacy"));
5949 let mut unknown = serialized;
5950 unknown["history_mode"] = json!("future");
5951 assert!(serde_json::from_value::<SessionMeta>(unknown).is_err());
5952 Ok(())
5953 }
5954
5955 #[test]
5956 fn copied_history_uses_persisted_history_mode() -> Result<()> {
5957 let thread_id = ThreadId::from_string("00000000-0000-0000-0000-000000000001")?;
5958 let session_meta = RolloutItem::SessionMeta(SessionMetaLine {
5959 meta: SessionMeta {
5960 session_id: thread_id.into(),
5961 id: thread_id,
5962 history_mode: ThreadHistoryMode::Legacy,
5963 ..SessionMeta::default()
5964 },
5965 git: None,
5966 });
5967 let history = InitialHistory::Resumed(ResumedHistory {
5968 conversation_id: thread_id,
5969 history: Arc::new(vec![session_meta.clone()]),
5970 rollout_path: None,
5971 });
5972
5973 assert_eq!(
5974 history.get_history_mode(ThreadHistoryMode::Paginated),
5975 ThreadHistoryMode::Legacy
5976 );
5977 assert_eq!(
5978 InitialHistory::Forked(vec![session_meta])
5979 .get_history_mode(ThreadHistoryMode::Paginated),
5980 ThreadHistoryMode::Legacy
5981 );
5982 assert_eq!(
5983 InitialHistory::New.get_history_mode(ThreadHistoryMode::Paginated),
5984 ThreadHistoryMode::Paginated
5985 );
5986 assert_eq!(
5987 InitialHistory::Resumed(ResumedHistory {
5988 conversation_id: thread_id,
5989 history: Arc::new(Vec::new()),
5990 rollout_path: None,
5991 })
5992 .get_history_mode(ThreadHistoryMode::Paginated),
5993 ThreadHistoryMode::Paginated
5994 );
5995 Ok(())
5996 }
5997
5998 #[test]
5999 fn turn_context_item_deserializes_without_network() -> Result<()> {
6000 let item: TurnContextItem = serde_json::from_value(json!({
6001 "cwd": test_path_buf("/tmp"),
6002 "approval_policy": "never",
6003 "sandbox_policy": { "type": "danger-full-access" },
6004 "model": "gpt-5",
6005 "summary": "auto",
6006 }))?;
6007
6008 assert_eq!(item.network, None);
6009 assert_eq!(item.file_system_sandbox_policy, None);
6010 assert_eq!(item.comp_hash, None);
6011 Ok(())
6012 }
6013
6014 #[test]
6015 fn turn_context_item_deserializes_legacy_on_failure_as_on_request() -> Result<()> {
6016 let item: TurnContextItem = serde_json::from_value(json!({
6017 "cwd": test_path_buf("/tmp"),
6018 "approval_policy": "on-failure",
6019 "sandbox_policy": { "type": "danger-full-access" },
6020 "model": "gpt-5",
6021 "summary": "auto",
6022 }))?;
6023
6024 assert_eq!(item.approval_policy, AskForApproval::OnRequest);
6025 Ok(())
6026 }
6027
6028 #[test]
6029 fn multi_agent_version_uses_newest_present_session_meta_value() -> Result<()> {
6030 let thread_id = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;
6031 let older_meta = SessionMetaLine {
6032 meta: SessionMeta {
6033 session_id: thread_id.into(),
6034 id: thread_id,
6035 multi_agent_version: Some(MultiAgentVersion::V2),
6036 ..Default::default()
6037 },
6038 git: None,
6039 };
6040 let newer_meta_without_version = SessionMetaLine {
6041 meta: SessionMeta {
6042 session_id: thread_id.into(),
6043 id: thread_id,
6044 multi_agent_version: None,
6045 ..Default::default()
6046 },
6047 git: None,
6048 };
6049
6050 assert_eq!(
6051 multi_agent_version_from_items(
6052 &[
6053 RolloutItem::SessionMeta(older_meta),
6054 RolloutItem::SessionMeta(newer_meta_without_version),
6055 ],
6056 Some(thread_id),
6057 ),
6058 Some(MultiAgentVersion::V2)
6059 );
6060 Ok(())
6061 }
6062
6063 #[test]
6064 fn latest_effective_multi_agent_mode_uses_latest_turn_context_even_when_unset() -> Result<()> {
6065 let turn_context_item = |multi_agent_mode| -> Result<RolloutItem> {
6066 let mut value = json!({
6067 "cwd": test_path_buf("/tmp"),
6068 "approval_policy": "never",
6069 "sandbox_policy": { "type": "danger-full-access" },
6070 "model": "gpt-5",
6071 "summary": "auto",
6072 });
6073 value["multi_agent_mode"] = serde_json::to_value(multi_agent_mode)?;
6074 Ok(RolloutItem::TurnContext(serde_json::from_value(value)?))
6075 };
6076
6077 assert_eq!(
6078 InitialHistory::Forked(vec![
6079 turn_context_item(Some(MultiAgentMode::Proactive))?,
6080 turn_context_item(None)?,
6081 ])
6082 .get_latest_effective_multi_agent_mode(),
6083 None
6084 );
6085 Ok(())
6086 }
6087
6088 #[test]
6089 fn latest_effective_multi_agent_mode_maps_legacy_none_to_empty_custom() -> Result<()> {
6090 let value = json!({
6091 "cwd": test_path_buf("/tmp"),
6092 "approval_policy": "never",
6093 "sandbox_policy": { "type": "danger-full-access" },
6094 "model": "gpt-5",
6095 "multi_agent_mode": "none",
6096 "summary": "auto",
6097 });
6098 let item = RolloutItem::TurnContext(serde_json::from_value(value)?);
6099
6100 assert_eq!(
6101 InitialHistory::Forked(vec![item]).get_latest_effective_multi_agent_mode(),
6102 Some(MultiAgentMode::Custom(String::new()))
6103 );
6104 Ok(())
6105 }
6106
6107 #[test]
6108 fn turn_context_item_serializes_network_when_present() -> Result<()> {
6109 let item = TurnContextItem {
6110 turn_id: None,
6111 cwd: test_path_buf("/tmp").abs(),
6112 workspace_roots: None,
6113 current_date: None,
6114 timezone: None,
6115 approval_policy: AskForApproval::Never,
6116 approvals_reviewer: None,
6117 sandbox_policy: SandboxPolicy::DangerFullAccess,
6118 permission_profile: None,
6119 network: Some(TurnContextNetworkItem {
6120 allowed_domains: vec!["api.example.com".to_string()],
6121 denied_domains: vec!["blocked.example.com".to_string()],
6122 }),
6123 file_system_sandbox_policy: Some(FileSystemSandboxPolicy::restricted(vec![
6124 FileSystemSandboxEntry {
6125 path: FileSystemPath::GlobPattern {
6126 pattern: "/tmp/private/**/*.txt".to_string(),
6127 },
6128 access: FileSystemAccessMode::Deny,
6129 missing_path_behavior: None,
6130 },
6131 ])),
6132 model: "gpt-5".to_string(),
6133 comp_hash: None,
6134 personality: None,
6135 collaboration_mode: None,
6136 multi_agent_version: None,
6137 multi_agent_mode: None,
6138 realtime_active: None,
6139 effort: None,
6140 summary: ReasoningSummaryConfig::Auto,
6141 };
6142
6143 let value = serde_json::to_value(item)?;
6144 assert_eq!(
6145 value["network"],
6146 json!({
6147 "allowed_domains": ["api.example.com"],
6148 "denied_domains": ["blocked.example.com"],
6149 })
6150 );
6151 assert_eq!(
6152 value["file_system_sandbox_policy"],
6153 json!({
6154 "kind": "restricted",
6155 "entries": [{
6156 "path": {
6157 "type": "glob_pattern",
6158 "pattern": "/tmp/private/**/*.txt"
6159 },
6160 "access": "deny"
6161 }]
6162 })
6163 );
6164 assert_eq!(value["summary"], json!("auto"));
6165 Ok(())
6166 }
6167
6168 #[test]
6171 fn serialize_event() -> Result<()> {
6172 let session_id = SessionId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c7")?;
6173 let thread_id = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;
6174 let rollout_file = NamedTempFile::new()?;
6175 let permission_profile = PermissionProfile::read_only();
6176 let event = Event {
6177 id: "1234".to_string(),
6178 msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
6179 session_id,
6180 thread_id,
6181 forked_from_id: None,
6182 parent_thread_id: None,
6183 thread_source: None,
6184 thread_name: None,
6185 model: "codex-mini-latest".to_string(),
6186 model_provider_id: "openai".to_string(),
6187 service_tier: None,
6188 approval_policy: AskForApproval::Never,
6189 approvals_reviewer: ApprovalsReviewer::User,
6190 permission_profile: permission_profile.clone(),
6191 active_permission_profile: None,
6192 cwd: test_path_buf("/home/user/project").abs(),
6193 reasoning_effort: Some(ReasoningEffortConfig::default()),
6194 initial_messages: None,
6195 network_proxy: None,
6196 rollout_path: Some(rollout_file.path().to_path_buf()),
6197 }),
6198 };
6199
6200 let expected = json!({
6201 "id": "1234",
6202 "msg": {
6203 "type": "session_configured",
6204 "session_id": "67e55044-10b1-426f-9247-bb680e5fe0c7",
6205 "thread_id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
6206 "model": "codex-mini-latest",
6207 "model_provider_id": "openai",
6208 "approval_policy": "never",
6209 "approvals_reviewer": "user",
6210 "permission_profile": permission_profile,
6211 "cwd": test_path_buf("/home/user/project"),
6212 "reasoning_effort": "medium",
6213 "rollout_path": format!("{}", rollout_file.path().display()),
6214 }
6215 });
6216 assert_eq!(expected, serde_json::to_value(&event)?);
6217 Ok(())
6218 }
6219
6220 #[test]
6221 fn deserialize_legacy_session_configured_event_uses_sandbox_policy() -> Result<()> {
6222 let cwd = test_path_buf("/home/user/project");
6223 let value = json!({
6224 "session_id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
6225 "model": "codex-mini-latest",
6226 "model_provider_id": "openai",
6227 "approval_policy": "never",
6228 "approvals_reviewer": "user",
6229 "sandbox_policy": {
6230 "type": "read-only"
6231 },
6232 "cwd": cwd,
6233 });
6234
6235 let event: SessionConfiguredEvent = serde_json::from_value(value)?;
6236 assert_eq!(event.permission_profile, PermissionProfile::read_only());
6237 Ok(())
6238 }
6239
6240 #[test]
6241 fn vec_u8_as_base64_serialization_and_deserialization() -> Result<()> {
6242 let event = ExecCommandOutputDeltaEvent {
6243 call_id: "call21".to_string(),
6244 stream: ExecOutputStream::Stdout,
6245 chunk: vec![1, 2, 3, 4, 5],
6246 };
6247 let serialized = serde_json::to_string(&event)?;
6248 assert_eq!(
6249 r#"{"call_id":"call21","stream":"stdout","chunk":"AQIDBAU="}"#,
6250 serialized,
6251 );
6252
6253 let deserialized: ExecCommandOutputDeltaEvent = serde_json::from_str(&serialized)?;
6254 assert_eq!(deserialized, event);
6255 Ok(())
6256 }
6257
6258 #[test]
6259 fn serialize_mcp_startup_update_event() -> Result<()> {
6260 let event = Event {
6261 id: "init".to_string(),
6262 msg: EventMsg::McpStartupUpdate(McpStartupUpdateEvent {
6263 server: "srv".to_string(),
6264 status: McpStartupStatus::Failed {
6265 error: "boom".to_string(),
6266 reason: Some(McpStartupFailureReason::ReauthenticationRequired),
6267 },
6268 }),
6269 };
6270
6271 let value = serde_json::to_value(&event)?;
6272 assert_eq!(value["msg"]["type"], "mcp_startup_update");
6273 assert_eq!(value["msg"]["server"], "srv");
6274 assert_eq!(value["msg"]["status"]["state"], "failed");
6275 assert_eq!(value["msg"]["status"]["error"], "boom");
6276 assert_eq!(
6277 value["msg"]["status"]["reason"],
6278 "reauthentication_required"
6279 );
6280 Ok(())
6281 }
6282
6283 #[test]
6284 fn serialize_mcp_startup_complete_event() -> Result<()> {
6285 let event = Event {
6286 id: "init".to_string(),
6287 msg: EventMsg::McpStartupComplete(McpStartupCompleteEvent {
6288 ready: vec!["a".to_string()],
6289 failed: vec![McpStartupFailure {
6290 server: "b".to_string(),
6291 error: "bad".to_string(),
6292 }],
6293 cancelled: vec!["c".to_string()],
6294 }),
6295 };
6296
6297 let value = serde_json::to_value(&event)?;
6298 assert_eq!(value["msg"]["type"], "mcp_startup_complete");
6299 assert_eq!(value["msg"]["ready"][0], "a");
6300 assert_eq!(value["msg"]["failed"][0]["server"], "b");
6301 assert_eq!(value["msg"]["failed"][0]["error"], "bad");
6302 assert_eq!(value["msg"]["cancelled"][0], "c");
6303 Ok(())
6304 }
6305
6306 #[test]
6307 fn token_usage_info_new_or_append_updates_context_window_when_provided() {
6308 let initial = Some(TokenUsageInfo {
6309 total_token_usage: TokenUsage::default(),
6310 last_token_usage: TokenUsage::default(),
6311 model_context_window: Some(258_400),
6312 });
6313 let last = Some(TokenUsage {
6314 input_tokens: 10,
6315 cached_input_tokens: 0,
6316 cache_write_input_tokens: 0,
6317 output_tokens: 0,
6318 reasoning_output_tokens: 0,
6319 total_tokens: 10,
6320 });
6321
6322 let info = TokenUsageInfo::new_or_append(&initial, &last, Some(128_000))
6323 .expect("new_or_append should return info");
6324
6325 assert_eq!(info.model_context_window, Some(128_000));
6326 }
6327
6328 #[test]
6329 fn token_usage_info_new_or_append_preserves_context_window_when_not_provided() {
6330 let initial = Some(TokenUsageInfo {
6331 total_token_usage: TokenUsage::default(),
6332 last_token_usage: TokenUsage::default(),
6333 model_context_window: Some(258_400),
6334 });
6335 let last = Some(TokenUsage {
6336 input_tokens: 10,
6337 cached_input_tokens: 0,
6338 cache_write_input_tokens: 0,
6339 output_tokens: 0,
6340 reasoning_output_tokens: 0,
6341 total_tokens: 10,
6342 });
6343
6344 let info =
6345 TokenUsageInfo::new_or_append(&initial, &last, None)
6346 .expect("new_or_append should return info");
6347
6348 assert_eq!(info.model_context_window, Some(258_400));
6349 }
6350}