1use std::collections::HashMap;
7use std::fmt;
8use std::path::Path;
9use std::path::PathBuf;
10use std::str::FromStr;
11use std::time::Duration;
12
13use crate::ConversationId;
14use crate::config_types::ReasoningEffort as ReasoningEffortConfig;
15use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;
16use crate::custom_prompts::CustomPrompt;
17use crate::message_history::HistoryEntry;
18use crate::models::ContentItem;
19use crate::models::ResponseItem;
20use crate::num_format::format_with_separators;
21use crate::parse_command::ParsedCommand;
22use crate::plan_tool::UpdatePlanArgs;
23use crate::skills::Skill;
24use mcp_types::CallToolResult;
25use mcp_types::Tool as McpTool;
26use serde::Deserialize;
27use serde::Serialize;
28use serde_json::Value;
29use serde_with::serde_as;
30use strum_macros::Display;
31use ts_rs::TS;
32
33pub const USER_INSTRUCTIONS_OPEN_TAG: &str = "<user_instructions>";
36pub const USER_INSTRUCTIONS_CLOSE_TAG: &str = "</user_instructions>";
37pub const ENVIRONMENT_CONTEXT_OPEN_TAG: &str = "<environment_context>";
38pub const ENVIRONMENT_CONTEXT_CLOSE_TAG: &str = "</environment_context>";
39pub const ENVIRONMENT_CONTEXT_DELTA_OPEN_TAG: &str = "<environment_context_delta>";
40pub const ENVIRONMENT_CONTEXT_DELTA_CLOSE_TAG: &str = "</environment_context_delta>";
41pub const BROWSER_SNAPSHOT_OPEN_TAG: &str = "<browser_snapshot>";
42pub const BROWSER_SNAPSHOT_CLOSE_TAG: &str = "</browser_snapshot>";
43pub const USER_MESSAGE_BEGIN: &str = "## My request for Codex:";
44
45#[derive(Debug, Clone, Deserialize, Serialize)]
47pub struct Submission {
48 pub id: String,
50 pub op: Op,
52}
53
54#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
56#[serde(tag = "type", rename_all = "snake_case")]
57#[allow(clippy::large_enum_variant)]
58#[non_exhaustive]
59pub enum Op {
60 Interrupt,
63
64 UserInput {
66 items: Vec<InputItem>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 final_output_json_schema: Option<Value>,
71 },
72
73 QueueUserInput {
76 items: Vec<InputItem>,
78 },
79
80 UserTurn {
83 items: Vec<InputItem>,
85
86 cwd: PathBuf,
89
90 approval_policy: AskForApproval,
92
93 sandbox_policy: SandboxPolicy,
95
96 model: String,
99
100 #[serde(skip_serializing_if = "Option::is_none")]
102 effort: Option<ReasoningEffortConfig>,
103
104 summary: ReasoningSummaryConfig,
106 final_output_json_schema: Option<Value>,
108 },
109
110 OverrideTurnContext {
116 #[serde(skip_serializing_if = "Option::is_none")]
118 cwd: Option<PathBuf>,
119
120 #[serde(skip_serializing_if = "Option::is_none")]
122 approval_policy: Option<AskForApproval>,
123
124 #[serde(skip_serializing_if = "Option::is_none")]
126 sandbox_policy: Option<SandboxPolicy>,
127
128 #[serde(skip_serializing_if = "Option::is_none")]
131 model: Option<String>,
132
133 #[serde(skip_serializing_if = "Option::is_none")]
138 effort: Option<Option<ReasoningEffortConfig>>,
139
140 #[serde(skip_serializing_if = "Option::is_none")]
142 summary: Option<ReasoningSummaryConfig>,
143 },
144
145 ExecApproval {
147 id: String,
149 decision: ReviewDecision,
151 },
152
153 RegisterApprovedCommand {
155 command: Vec<String>,
156 match_kind: ApprovedCommandMatchKind,
157 #[serde(skip_serializing_if = "Option::is_none")]
158 #[serde(default)]
159 semantic_prefix: Option<Vec<String>>,
160 },
161
162 PatchApproval {
164 id: String,
166 decision: ReviewDecision,
168 },
169
170 AddToHistory {
175 text: String,
177 },
178
179 PersistHistorySnapshot { snapshot: serde_json::Value },
181
182 GetHistoryEntryRequest { offset: usize, log_id: u64 },
184
185 GetPath,
188
189 ListMcpTools,
192
193 ListCustomPrompts,
195
196 ListSkills,
199
200 Compact,
204
205 Review { review_request: ReviewRequest },
207
208 Shutdown,
210}
211
212#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Display, TS)]
215#[serde(rename_all = "kebab-case")]
216#[strum(serialize_all = "kebab-case")]
217pub enum AskForApproval {
218 #[serde(rename = "untrusted")]
222 #[strum(serialize = "untrusted")]
223 UnlessTrusted,
224
225 OnFailure,
230
231 #[default]
233 OnRequest,
234
235 Never,
238}
239
240#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash, TS)]
241#[serde(rename_all = "kebab-case")]
242pub enum ApprovedCommandMatchKind {
243 Exact,
244 Prefix,
245}
246
247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, TS)]
249#[strum(serialize_all = "kebab-case")]
250#[serde(tag = "mode", rename_all = "kebab-case")]
251pub enum SandboxPolicy {
252 #[serde(rename = "danger-full-access")]
254 DangerFullAccess,
255
256 #[serde(rename = "read-only")]
258 ReadOnly,
259
260 #[serde(rename = "workspace-write")]
263 WorkspaceWrite {
264 #[serde(default, skip_serializing_if = "Vec::is_empty")]
267 writable_roots: Vec<PathBuf>,
268
269 #[serde(default)]
272 network_access: bool,
273
274 #[serde(default)]
278 exclude_tmpdir_env_var: bool,
279
280 #[serde(default)]
283 exclude_slash_tmp: bool,
284
285 #[serde(default = "default_true_bool")]
288 allow_git_writes: bool,
289 },
290}
291
292const fn default_true_bool() -> bool {
293 true
294}
295
296#[derive(Debug, Clone, PartialEq, Eq)]
301pub struct WritableRoot {
302 pub root: PathBuf,
304
305 pub read_only_subpaths: Vec<PathBuf>,
307}
308
309impl WritableRoot {
310 pub fn is_path_writable(&self, path: &Path) -> bool {
311 if !path.starts_with(&self.root) {
313 return false;
314 }
315
316 for subpath in &self.read_only_subpaths {
318 if path.starts_with(subpath) {
319 return false;
320 }
321 }
322
323 true
324 }
325}
326
327impl FromStr for SandboxPolicy {
328 type Err = serde_json::Error;
329
330 fn from_str(s: &str) -> Result<Self, Self::Err> {
331 serde_json::from_str(s)
332 }
333}
334
335impl SandboxPolicy {
336 pub fn new_read_only_policy() -> Self {
338 SandboxPolicy::ReadOnly
339 }
340
341 pub fn new_workspace_write_policy() -> Self {
345 SandboxPolicy::WorkspaceWrite {
346 writable_roots: vec![],
347 network_access: false,
348 exclude_tmpdir_env_var: false,
349 exclude_slash_tmp: false,
350 allow_git_writes: true,
351 }
352 }
353
354 pub fn has_full_disk_read_access(&self) -> bool {
356 true
357 }
358
359 pub fn has_full_disk_write_access(&self) -> bool {
360 match self {
361 SandboxPolicy::DangerFullAccess => true,
362 SandboxPolicy::ReadOnly => false,
363 SandboxPolicy::WorkspaceWrite { .. } => false,
364 }
365 }
366
367 pub fn has_full_network_access(&self) -> bool {
368 match self {
369 SandboxPolicy::DangerFullAccess => true,
370 SandboxPolicy::ReadOnly => false,
371 SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access,
372 }
373 }
374
375 pub fn get_writable_roots_with_cwd(&self, cwd: &Path) -> Vec<WritableRoot> {
379 match self {
380 SandboxPolicy::DangerFullAccess => Vec::new(),
381 SandboxPolicy::ReadOnly => Vec::new(),
382 SandboxPolicy::WorkspaceWrite {
383 writable_roots,
384 exclude_tmpdir_env_var,
385 exclude_slash_tmp,
386 allow_git_writes,
387 network_access: _,
388 } => {
389 let mut roots: Vec<PathBuf> = writable_roots.clone();
391
392 roots.push(cwd.to_path_buf());
395
396 if cfg!(unix) && !exclude_slash_tmp {
398 let slash_tmp = PathBuf::from("/tmp");
399 if slash_tmp.is_dir() {
400 roots.push(slash_tmp);
401 }
402 }
403
404 if !exclude_tmpdir_env_var
413 && let Some(tmpdir) = std::env::var_os("TMPDIR")
414 && !tmpdir.is_empty()
415 {
416 roots.push(PathBuf::from(tmpdir));
417 }
418
419 roots
421 .into_iter()
422 .map(|writable_root| {
423 let mut subpaths = Vec::new();
424 if !allow_git_writes {
425 let top_level_git = writable_root.join(".git");
426 if top_level_git.is_dir() {
427 subpaths.push(top_level_git);
428 }
429 }
430 WritableRoot {
431 root: writable_root,
432 read_only_subpaths: subpaths,
433 }
434 })
435 .collect()
436 }
437 }
438 }
439}
440
441#[non_exhaustive]
443#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
444#[serde(tag = "type", rename_all = "snake_case")]
445pub enum InputItem {
446 Text {
447 text: String,
448 },
449 Image {
451 image_url: String,
452 },
453
454 LocalImage {
457 path: std::path::PathBuf,
458 },
459}
460
461#[derive(Debug, Clone, Deserialize, Serialize, TS)]
463pub struct Event {
464 pub id: String,
466 pub event_seq: u64,
469 pub msg: EventMsg,
471 #[serde(skip_serializing_if = "Option::is_none")]
473 pub order: Option<OrderMeta>,
474}
475
476#[derive(Debug, Clone, Deserialize, Serialize, TS)]
478pub struct RecordedEvent {
479 pub id: String,
480 pub event_seq: u64,
481 #[serde(skip_serializing_if = "Option::is_none")]
482 pub order: Option<OrderMeta>,
483 pub msg: EventMsg,
484}
485
486#[derive(Debug, Clone, Deserialize, Serialize, TS)]
487pub struct OrderMeta {
488 pub request_ordinal: u64,
490 #[serde(skip_serializing_if = "Option::is_none")]
492 pub output_index: Option<u32>,
493 #[serde(skip_serializing_if = "Option::is_none")]
495 pub sequence_number: Option<u64>,
496}
497
498#[derive(Debug, Clone, Deserialize, Serialize, Display, TS)]
501#[serde(tag = "type", rename_all = "snake_case")]
502#[strum(serialize_all = "snake_case")]
503pub enum EventMsg {
504 Error(ErrorEvent),
506
507 TaskStarted(TaskStartedEvent),
509
510 TaskComplete(TaskCompleteEvent),
512
513 TokenCount(TokenCountEvent),
516
517 AgentMessage(AgentMessageEvent),
519
520 UserMessage(UserMessageEvent),
522
523 AgentMessageDelta(AgentMessageDeltaEvent),
525
526 AgentReasoning(AgentReasoningEvent),
528
529 AgentReasoningDelta(AgentReasoningDeltaEvent),
531
532 AgentReasoningRawContent(AgentReasoningRawContentEvent),
534
535 AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent),
537 AgentReasoningSectionBreak(AgentReasoningSectionBreakEvent),
539
540 EnvironmentContextFull(EnvironmentContextFullEvent),
542
543 EnvironmentContextDelta(EnvironmentContextDeltaEvent),
545
546 BrowserSnapshot(BrowserSnapshotEvent),
548
549 CompactionCheckpointWarning(CompactionCheckpointWarningEvent),
551
552 SessionConfigured(SessionConfiguredEvent),
554
555 McpToolCallBegin(McpToolCallBeginEvent),
556
557 McpToolCallEnd(McpToolCallEndEvent),
558
559 WebSearchBegin(WebSearchBeginEvent),
560
561 WebSearchEnd(WebSearchEndEvent),
562
563 ExecCommandBegin(ExecCommandBeginEvent),
565
566 ExecCommandOutputDelta(ExecCommandOutputDeltaEvent),
568
569 ExecCommandEnd(ExecCommandEndEvent),
570
571 ViewImageToolCall(ViewImageToolCallEvent),
573
574 ExecApprovalRequest(ExecApprovalRequestEvent),
575
576 ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent),
577
578 BackgroundEvent(BackgroundEventEvent),
579
580 StreamError(StreamErrorEvent),
583
584 PatchApplyBegin(PatchApplyBeginEvent),
587
588 PatchApplyEnd(PatchApplyEndEvent),
590
591 TurnDiff(TurnDiffEvent),
592
593 GetHistoryEntryResponse(GetHistoryEntryResponseEvent),
595
596 McpListToolsResponse(McpListToolsResponseEvent),
598
599 ListCustomPromptsResponse(ListCustomPromptsResponseEvent),
601
602 ListSkillsResponse(ListSkillsResponseEvent),
604
605 PlanUpdate(UpdatePlanArgs),
606
607 TurnAborted(TurnAbortedEvent),
608
609 ShutdownComplete,
611
612 ConversationPath(ConversationPathResponseEvent),
613
614 EnteredReviewMode(ReviewRequest),
616
617 ExitedReviewMode(ExitedReviewModeEvent),
619}
620
621#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, TS)]
623#[serde(rename_all = "snake_case")]
624#[ts(rename_all = "snake_case")]
625pub enum CodexErrorInfo {
626 ContextWindowExceeded,
627 UsageLimitExceeded,
628 HttpConnectionFailed {
629 http_status_code: Option<u16>,
630 },
631 ResponseStreamConnectionFailed {
633 http_status_code: Option<u16>,
634 },
635 InternalServerError,
636 Unauthorized,
637 BadRequest,
638 SandboxError,
639 ResponseStreamDisconnected {
641 http_status_code: Option<u16>,
642 },
643 ResponseTooManyFailedAttempts {
645 http_status_code: Option<u16>,
646 },
647 Other,
648}
649
650#[derive(Debug, Clone, Deserialize, Serialize, TS)]
651pub struct ExitedReviewModeEvent {
652 pub review_output: Option<ReviewOutputEvent>,
653 #[serde(skip_serializing_if = "Option::is_none")]
654 #[ts(optional)]
655 pub snapshot: Option<ReviewSnapshotInfo>,
656}
657
658#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)]
660pub struct ReviewSnapshotInfo {
661 #[serde(skip_serializing_if = "Option::is_none")]
662 #[ts(optional)]
663 pub snapshot_commit: Option<String>,
664 #[serde(skip_serializing_if = "Option::is_none")]
665 #[ts(optional)]
666 pub branch: Option<String>,
667 #[serde(skip_serializing_if = "Option::is_none")]
668 #[ts(optional)]
669 pub worktree_path: Option<std::path::PathBuf>,
670 #[serde(skip_serializing_if = "Option::is_none")]
671 #[ts(optional)]
672 pub repo_root: Option<std::path::PathBuf>,
673}
674
675#[derive(Debug, Clone, Deserialize, Serialize, TS)]
678pub struct ErrorEvent {
679 pub message: String,
680}
681
682#[derive(Debug, Clone, Deserialize, Serialize, TS)]
683pub struct TaskCompleteEvent {
684 pub last_agent_message: Option<String>,
685}
686
687#[derive(Debug, Clone, Deserialize, Serialize, TS)]
688pub struct TaskStartedEvent {
689 pub model_context_window: Option<u64>,
690}
691
692#[derive(Debug, Clone, Deserialize, Serialize, TS)]
693pub struct CompactionCheckpointWarningEvent {
694 pub message: String,
695}
696
697#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize, TS)]
698pub struct TokenUsage {
699 pub input_tokens: u64,
700 pub cached_input_tokens: u64,
701 pub output_tokens: u64,
702 pub reasoning_output_tokens: u64,
703 pub total_tokens: u64,
704}
705
706#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, TS)]
707pub struct TokenUsageInfo {
708 pub total_token_usage: TokenUsage,
709 pub last_token_usage: TokenUsage,
710 pub model_context_window: Option<u64>,
711}
712
713impl TokenUsageInfo {
714 pub fn new_or_append(
715 info: &Option<TokenUsageInfo>,
716 last: &Option<TokenUsage>,
717 model_context_window: Option<u64>,
718 ) -> Option<Self> {
719 if info.is_none() && last.is_none() {
720 return None;
721 }
722
723 let mut info = match info {
724 Some(info) => info.clone(),
725 None => Self {
726 total_token_usage: TokenUsage::default(),
727 last_token_usage: TokenUsage::default(),
728 model_context_window,
729 },
730 };
731 if let Some(last) = last {
732 info.append_last_usage(last);
733 }
734 Some(info)
735 }
736
737 pub fn append_last_usage(&mut self, last: &TokenUsage) {
738 self.total_token_usage.add_assign(last);
739 self.last_token_usage = last.clone();
740 }
741}
742
743#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
744pub struct TokenCountEvent {
745 pub info: Option<TokenUsageInfo>,
746 pub rate_limits: Option<RateLimitSnapshot>,
747}
748
749#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
750pub struct RateLimitSnapshot {
751 pub primary: Option<RateLimitWindow>,
752 pub secondary: Option<RateLimitWindow>,
753}
754
755#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]
756pub struct RateLimitWindow {
757 pub used_percent: f64,
759 pub window_minutes: Option<u64>,
761 pub resets_in_seconds: Option<u64>,
763}
764
765#[derive(Debug, Clone, Deserialize, Serialize, TS)]
767pub struct ReplayHistoryEvent {
768 pub items: Vec<crate::models::ResponseItem>,
769 #[serde(default, skip_serializing_if = "Option::is_none")]
770 pub history_snapshot: Option<serde_json::Value>,
771}
772
773const BASELINE_TOKENS: u64 = 12000;
775
776impl TokenUsage {
777 pub fn is_zero(&self) -> bool {
778 self.total_tokens == 0
779 }
780
781 pub fn cached_input(&self) -> u64 {
782 self.cached_input_tokens
783 }
784
785 pub fn non_cached_input(&self) -> u64 {
786 self.input_tokens.saturating_sub(self.cached_input())
787 }
788
789 pub fn blended_total(&self) -> u64 {
791 self.non_cached_input() + self.output_tokens
792 }
793
794 pub fn tokens_in_context_window(&self) -> u64 {
799 self.total_tokens
800 .saturating_sub(self.reasoning_output_tokens)
801 }
802
803 pub fn percent_of_context_window_remaining(&self, context_window: u64) -> u8 {
814 if context_window <= BASELINE_TOKENS {
815 return 0;
816 }
817
818 let effective_window = context_window - BASELINE_TOKENS;
819 let used = self
820 .tokens_in_context_window()
821 .saturating_sub(BASELINE_TOKENS);
822 let remaining = effective_window.saturating_sub(used);
823 ((remaining as f32 / effective_window as f32) * 100.0).clamp(0.0, 100.0) as u8
824 }
825
826 pub fn add_assign(&mut self, other: &TokenUsage) {
828 self.input_tokens += other.input_tokens;
829 self.cached_input_tokens += other.cached_input_tokens;
830 self.output_tokens += other.output_tokens;
831 self.reasoning_output_tokens += other.reasoning_output_tokens;
832 self.total_tokens += other.total_tokens;
833 }
834}
835
836#[derive(Debug, Clone, Deserialize, Serialize)]
837pub struct FinalOutput {
838 pub token_usage: TokenUsage,
839}
840
841impl From<TokenUsage> for FinalOutput {
842 fn from(token_usage: TokenUsage) -> Self {
843 Self { token_usage }
844 }
845}
846
847impl fmt::Display for FinalOutput {
848 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
849 let token_usage = &self.token_usage;
850
851 write!(
852 f,
853 "Token usage: total={} input={}{} output={}{}",
854 format_with_separators(token_usage.blended_total()),
855 format_with_separators(token_usage.non_cached_input()),
856 if token_usage.cached_input() > 0 {
857 format!(
858 " (+ {} cached)",
859 format_with_separators(token_usage.cached_input())
860 )
861 } else {
862 String::new()
863 },
864 format_with_separators(token_usage.output_tokens),
865 if token_usage.reasoning_output_tokens > 0 {
866 format!(
867 " (reasoning {})",
868 format_with_separators(token_usage.reasoning_output_tokens)
869 )
870 } else {
871 String::new()
872 }
873 )
874 }
875}
876
877#[derive(Debug, Clone, Deserialize, Serialize, TS)]
878pub struct AgentMessageEvent {
879 pub message: String,
880}
881
882#[derive(Debug, Clone, Deserialize, Serialize, TS)]
883#[serde(rename_all = "snake_case")]
884pub enum InputMessageKind {
885 Plain,
887 UserInstructions,
889 EnvironmentContext,
891}
892
893#[derive(Debug, Clone, Deserialize, Serialize, TS)]
894pub struct UserMessageEvent {
895 pub message: String,
896 #[serde(skip_serializing_if = "Option::is_none")]
897 pub kind: Option<InputMessageKind>,
898 #[serde(skip_serializing_if = "Option::is_none")]
899 pub images: Option<Vec<String>>,
900}
901
902impl<T, U> From<(T, U)> for InputMessageKind
903where
904 T: AsRef<str>,
905 U: AsRef<str>,
906{
907 fn from(value: (T, U)) -> Self {
908 let (_role, message) = value;
909 let message = message.as_ref();
910 let trimmed = message.trim();
911 if starts_with_ignore_ascii_case(trimmed, ENVIRONMENT_CONTEXT_OPEN_TAG)
912 && ends_with_ignore_ascii_case(trimmed, ENVIRONMENT_CONTEXT_CLOSE_TAG)
913 {
914 InputMessageKind::EnvironmentContext
915 } else if starts_with_ignore_ascii_case(trimmed, USER_INSTRUCTIONS_OPEN_TAG)
916 && ends_with_ignore_ascii_case(trimmed, USER_INSTRUCTIONS_CLOSE_TAG)
917 {
918 InputMessageKind::UserInstructions
919 } else {
920 InputMessageKind::Plain
921 }
922 }
923}
924
925fn starts_with_ignore_ascii_case(text: &str, prefix: &str) -> bool {
926 let text_bytes = text.as_bytes();
927 let prefix_bytes = prefix.as_bytes();
928 text_bytes.len() >= prefix_bytes.len()
929 && text_bytes
930 .iter()
931 .zip(prefix_bytes.iter())
932 .all(|(a, b)| a.eq_ignore_ascii_case(b))
933}
934
935fn ends_with_ignore_ascii_case(text: &str, suffix: &str) -> bool {
936 let text_bytes = text.as_bytes();
937 let suffix_bytes = suffix.as_bytes();
938 text_bytes.len() >= suffix_bytes.len()
939 && text_bytes[text_bytes.len() - suffix_bytes.len()..]
940 .iter()
941 .zip(suffix_bytes.iter())
942 .all(|(a, b)| a.eq_ignore_ascii_case(b))
943}
944
945#[derive(Debug, Clone, Deserialize, Serialize, TS)]
946pub struct AgentMessageDeltaEvent {
947 pub delta: String,
948}
949
950#[derive(Debug, Clone, Deserialize, Serialize, TS)]
951pub struct AgentReasoningEvent {
952 pub text: String,
953}
954
955#[derive(Debug, Clone, Deserialize, Serialize, TS)]
956pub struct AgentReasoningRawContentEvent {
957 pub text: String,
958}
959
960#[derive(Debug, Clone, Deserialize, Serialize, TS)]
961pub struct AgentReasoningRawContentDeltaEvent {
962 pub delta: String,
963}
964
965#[derive(Debug, Clone, Deserialize, Serialize, TS)]
966pub struct AgentReasoningSectionBreakEvent {}
967
968#[derive(Debug, Clone, Deserialize, Serialize, TS)]
969pub struct AgentReasoningDeltaEvent {
970 pub delta: String,
971}
972
973#[derive(Debug, Clone, Deserialize, Serialize, TS)]
974pub struct McpInvocation {
975 pub server: String,
977 pub tool: String,
979 pub arguments: Option<serde_json::Value>,
981}
982
983#[derive(Debug, Clone, Deserialize, Serialize, TS)]
984pub struct McpToolCallBeginEvent {
985 pub call_id: String,
987 pub invocation: McpInvocation,
988}
989
990#[derive(Debug, Clone, Deserialize, Serialize, TS)]
991pub struct McpToolCallEndEvent {
992 pub call_id: String,
994 pub invocation: McpInvocation,
995 #[ts(type = "string")]
996 pub duration: Duration,
997 pub result: Result<CallToolResult, String>,
999}
1000
1001impl McpToolCallEndEvent {
1002 pub fn is_success(&self) -> bool {
1003 match &self.result {
1004 Ok(result) => !result.is_error.unwrap_or(false),
1005 Err(_) => false,
1006 }
1007 }
1008}
1009
1010#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1011pub struct WebSearchBeginEvent {
1012 pub call_id: String,
1013}
1014
1015#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1016pub struct WebSearchEndEvent {
1017 pub call_id: String,
1018 pub query: String,
1019}
1020
1021#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1024pub struct ConversationPathResponseEvent {
1025 pub conversation_id: ConversationId,
1026 pub path: PathBuf,
1027}
1028
1029#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1030pub struct ResumedHistory {
1031 pub conversation_id: ConversationId,
1032 pub history: Vec<RolloutItem>,
1033 pub rollout_path: PathBuf,
1034}
1035
1036#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1037pub enum InitialHistory {
1038 New,
1039 Resumed(ResumedHistory),
1040 Forked(Vec<RolloutItem>),
1041}
1042
1043impl InitialHistory {
1044 pub fn get_rollout_items(&self) -> Vec<RolloutItem> {
1045 match self {
1046 InitialHistory::New => Vec::new(),
1047 InitialHistory::Resumed(resumed) => resumed.history.clone(),
1048 InitialHistory::Forked(items) => items.clone(),
1049 }
1050 }
1051
1052 pub fn get_event_msgs(&self) -> Option<Vec<EventMsg>> {
1053 match self {
1054 InitialHistory::New => None,
1055 InitialHistory::Resumed(resumed) => Some(
1056 resumed
1057 .history
1058 .iter()
1059 .filter_map(|ri| match ri {
1060 RolloutItem::Event(ev) => Some(ev.msg.clone()),
1061 _ => None,
1062 })
1063 .collect(),
1064 ),
1065 InitialHistory::Forked(items) => Some(
1066 items
1067 .iter()
1068 .filter_map(|ri| match ri {
1069 RolloutItem::Event(ev) => Some(ev.msg.clone()),
1070 _ => None,
1071 })
1072 .collect(),
1073 ),
1074 }
1075 }
1076}
1077
1078#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, TS, Default)]
1079#[serde(rename_all = "lowercase")]
1080#[ts(rename_all = "lowercase")]
1081pub enum SessionSource {
1082 Cli,
1083 #[default]
1084 VSCode,
1085 Exec,
1086 Mcp,
1087 #[serde(other)]
1088 Unknown,
1089}
1090
1091#[derive(Serialize, Deserialize, Clone, Debug, TS)]
1092pub struct SessionMeta {
1093 pub id: ConversationId,
1094 pub timestamp: String,
1095 pub cwd: PathBuf,
1096 pub originator: String,
1097 pub cli_version: String,
1098 pub instructions: Option<String>,
1099 #[serde(default)]
1100 pub source: SessionSource,
1101}
1102
1103impl Default for SessionMeta {
1104 fn default() -> Self {
1105 SessionMeta {
1106 id: ConversationId::default(),
1107 timestamp: String::new(),
1108 cwd: PathBuf::new(),
1109 originator: String::new(),
1110 cli_version: String::new(),
1111 instructions: None,
1112 source: SessionSource::default(),
1113 }
1114 }
1115}
1116
1117#[derive(Serialize, Deserialize, Debug, Clone, TS)]
1118pub struct SessionMetaLine {
1119 #[serde(flatten)]
1120 pub meta: SessionMeta,
1121 #[serde(skip_serializing_if = "Option::is_none")]
1122 pub git: Option<GitInfo>,
1123}
1124
1125#[derive(Serialize, Deserialize, Debug, Clone, TS)]
1126#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
1127pub enum RolloutItem {
1128 SessionMeta(SessionMetaLine),
1129 ResponseItem(ResponseItem),
1130 Compacted(CompactedItem),
1131 TurnContext(TurnContextItem),
1132 Event(RecordedEvent),
1133}
1134
1135#[derive(Serialize, Deserialize, Clone, Debug, TS)]
1136pub struct CompactedItem {
1137 pub message: String,
1138}
1139
1140impl From<CompactedItem> for ResponseItem {
1141 fn from(value: CompactedItem) -> Self {
1142 ResponseItem::Message {
1143 id: None,
1144 role: "assistant".to_string(),
1145 content: vec![ContentItem::OutputText {
1146 text: value.message,
1147 }],
1148 }
1149 }
1150}
1151
1152#[derive(Serialize, Deserialize, Clone, Debug, TS)]
1153pub struct TurnContextItem {
1154 pub cwd: PathBuf,
1155 pub approval_policy: AskForApproval,
1156 pub sandbox_policy: SandboxPolicy,
1157 pub model: String,
1158 #[serde(skip_serializing_if = "Option::is_none")]
1159 pub effort: Option<ReasoningEffortConfig>,
1160 pub summary: ReasoningSummaryConfig,
1161 #[serde(skip_serializing_if = "Option::is_none")]
1162 pub base_instructions: Option<String>,
1163 #[serde(skip_serializing_if = "Option::is_none")]
1164 pub user_instructions: Option<String>,
1165 #[serde(skip_serializing_if = "Option::is_none")]
1166 pub developer_instructions: Option<String>,
1167 #[serde(skip_serializing_if = "Option::is_none")]
1168 pub final_output_json_schema: Option<Value>,
1169 #[serde(skip_serializing_if = "Option::is_none")]
1170 pub truncation_policy: Option<TruncationPolicy>,
1171}
1172
1173#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, TS)]
1174#[serde(tag = "mode", content = "limit", rename_all = "snake_case")]
1175pub enum TruncationPolicy {
1176 Bytes(usize),
1177 Tokens(usize),
1178}
1179
1180#[derive(Serialize, Deserialize, Clone)]
1181pub struct RolloutLine {
1182 pub timestamp: String,
1183 #[serde(flatten)]
1184 pub item: RolloutItem,
1185}
1186
1187#[derive(Serialize, Deserialize, Clone, Debug, TS)]
1188pub struct GitInfo {
1189 #[serde(skip_serializing_if = "Option::is_none")]
1191 pub commit_hash: Option<String>,
1192 #[serde(skip_serializing_if = "Option::is_none")]
1194 pub branch: Option<String>,
1195 #[serde(skip_serializing_if = "Option::is_none")]
1197 pub repository_url: Option<String>,
1198}
1199
1200#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)]
1202pub struct ReviewRequest {
1203 pub prompt: String,
1204 pub user_facing_hint: String,
1205 #[serde(skip_serializing_if = "Option::is_none")]
1206 #[ts(optional)]
1207 pub metadata: Option<ReviewContextMetadata>,
1208}
1209
1210#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS, Default)]
1211pub struct ReviewContextMetadata {
1212 #[serde(skip_serializing_if = "Option::is_none")]
1213 #[ts(optional)]
1214 pub scope: Option<String>,
1215 #[serde(skip_serializing_if = "Option::is_none")]
1216 #[ts(optional)]
1217 pub commit: Option<String>,
1218 #[serde(skip_serializing_if = "Option::is_none")]
1219 #[ts(optional)]
1220 pub base_branch: Option<String>,
1221 #[serde(skip_serializing_if = "Option::is_none")]
1222 #[ts(optional)]
1223 pub current_branch: Option<String>,
1224 #[serde(skip_serializing_if = "Option::is_none")]
1225 #[ts(optional)]
1226 pub auto_review: Option<bool>,
1227}
1228
1229#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)]
1231pub struct ReviewOutputEvent {
1232 pub findings: Vec<ReviewFinding>,
1233 pub overall_correctness: String,
1234 pub overall_explanation: String,
1235 pub overall_confidence_score: f32,
1236}
1237
1238impl Default for ReviewOutputEvent {
1239 fn default() -> Self {
1240 Self {
1241 findings: Vec::new(),
1242 overall_correctness: String::default(),
1243 overall_explanation: String::default(),
1244 overall_confidence_score: 0.0,
1245 }
1246 }
1247}
1248
1249#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)]
1251pub struct ReviewFinding {
1252 pub title: String,
1253 pub body: String,
1254 pub confidence_score: f32,
1255 pub priority: i32,
1256 pub code_location: ReviewCodeLocation,
1257}
1258
1259#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)]
1261pub struct ReviewCodeLocation {
1262 pub absolute_file_path: PathBuf,
1263 pub line_range: ReviewLineRange,
1264}
1265
1266#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)]
1268pub struct ReviewLineRange {
1269 pub start: u32,
1270 pub end: u32,
1271}
1272
1273#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1274pub struct ExecCommandBeginEvent {
1275 pub call_id: String,
1277 pub command: Vec<String>,
1279 pub cwd: PathBuf,
1281 pub parsed_cmd: Vec<ParsedCommand>,
1282}
1283
1284#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1285pub struct ExecCommandEndEvent {
1286 pub call_id: String,
1288 pub stdout: String,
1290 pub stderr: String,
1292 #[serde(default)]
1294 pub aggregated_output: String,
1295 pub exit_code: i32,
1297 #[ts(type = "string")]
1299 pub duration: Duration,
1300 pub formatted_output: String,
1302}
1303
1304#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1305pub struct ViewImageToolCallEvent {
1306 pub call_id: String,
1308 pub path: PathBuf,
1310}
1311
1312#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)]
1313#[serde(rename_all = "snake_case")]
1314pub enum ExecOutputStream {
1315 Stdout,
1316 Stderr,
1317}
1318
1319#[serde_as]
1320#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)]
1321pub struct ExecCommandOutputDeltaEvent {
1322 pub call_id: String,
1324 pub stream: ExecOutputStream,
1326 #[serde_as(as = "serde_with::base64::Base64")]
1328 #[ts(type = "string")]
1329 pub chunk: Vec<u8>,
1330}
1331
1332#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1333pub struct ExecApprovalRequestEvent {
1334 pub call_id: String,
1336 pub command: Vec<String>,
1338 pub cwd: PathBuf,
1340 #[serde(skip_serializing_if = "Option::is_none")]
1342 pub reason: Option<String>,
1343}
1344
1345#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1346pub struct ApplyPatchApprovalRequestEvent {
1347 pub call_id: String,
1349 pub changes: HashMap<PathBuf, FileChange>,
1350 #[serde(skip_serializing_if = "Option::is_none")]
1352 pub reason: Option<String>,
1353 #[serde(skip_serializing_if = "Option::is_none")]
1355 pub grant_root: Option<PathBuf>,
1356}
1357
1358#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1359pub struct BackgroundEventEvent {
1360 pub message: String,
1361}
1362
1363#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1364pub struct StreamErrorEvent {
1365 pub message: String,
1366 #[serde(default)]
1367 pub codex_error_info: Option<CodexErrorInfo>,
1368 #[serde(default)]
1372 pub additional_details: Option<String>,
1373}
1374
1375#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1376pub struct PatchApplyBeginEvent {
1377 pub call_id: String,
1379 pub auto_approved: bool,
1381 pub changes: HashMap<PathBuf, FileChange>,
1383}
1384
1385#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1386pub struct PatchApplyEndEvent {
1387 pub call_id: String,
1389 pub stdout: String,
1391 pub stderr: String,
1393 pub success: bool,
1395}
1396
1397#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1398pub struct TurnDiffEvent {
1399 pub unified_diff: String,
1400}
1401
1402#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1403pub struct GetHistoryEntryResponseEvent {
1404 pub offset: usize,
1405 pub log_id: u64,
1406 #[serde(skip_serializing_if = "Option::is_none")]
1408 pub entry: Option<HistoryEntry>,
1409}
1410
1411#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1413pub struct McpListToolsResponseEvent {
1414 pub tools: std::collections::HashMap<String, McpTool>,
1416}
1417
1418#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1420pub struct ListCustomPromptsResponseEvent {
1421 pub custom_prompts: Vec<CustomPrompt>,
1422}
1423
1424#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1426pub struct ListSkillsResponseEvent {
1427 pub skills: Vec<Skill>,
1428}
1429
1430#[derive(Debug, Default, Clone, Deserialize, Serialize, TS)]
1431pub struct SessionConfiguredEvent {
1432 pub session_id: ConversationId,
1434
1435 pub model: String,
1437
1438 #[serde(skip_serializing_if = "Option::is_none")]
1440 pub reasoning_effort: Option<ReasoningEffortConfig>,
1441
1442 pub history_log_id: u64,
1444
1445 pub history_entry_count: usize,
1447
1448 #[serde(skip_serializing_if = "Option::is_none")]
1451 pub initial_messages: Option<Vec<EventMsg>>,
1452
1453 pub rollout_path: PathBuf,
1454}
1455
1456#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Display, TS)]
1458#[serde(rename_all = "snake_case")]
1459pub enum ReviewDecision {
1460 Approved,
1462
1463 ApprovedForSession,
1467
1468 #[default]
1471 Denied,
1472
1473 Abort,
1476}
1477
1478#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, TS)]
1479#[serde(rename_all = "snake_case")]
1480pub enum FileChange {
1481 Add {
1482 content: String,
1483 },
1484 Delete {
1485 content: String,
1486 },
1487 Update {
1488 unified_diff: String,
1489 move_path: Option<PathBuf>,
1490 original_content: String,
1491 new_content: String,
1492 },
1493}
1494
1495#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1496pub struct Chunk {
1497 pub orig_index: u32,
1499 pub deleted_lines: Vec<String>,
1500 pub inserted_lines: Vec<String>,
1501}
1502
1503#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1504pub struct EnvironmentContextFullEvent {
1505 pub snapshot: serde_json::Value,
1507 #[serde(skip_serializing_if = "Option::is_none")]
1509 pub sequence: Option<u64>,
1510}
1511
1512#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1513pub struct EnvironmentContextDeltaEvent {
1514 pub delta: serde_json::Value,
1516 #[serde(skip_serializing_if = "Option::is_none")]
1518 pub sequence: Option<u64>,
1519 #[serde(skip_serializing_if = "Option::is_none")]
1521 pub base_fingerprint: Option<String>,
1522}
1523
1524#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1525pub struct BrowserSnapshotEvent {
1526 pub snapshot: serde_json::Value,
1528 #[serde(skip_serializing_if = "Option::is_none")]
1530 pub url: Option<String>,
1531 #[serde(skip_serializing_if = "Option::is_none")]
1533 pub captured_at: Option<String>,
1534}
1535
1536#[derive(Debug, Clone, Deserialize, Serialize, TS)]
1537pub struct TurnAbortedEvent {
1538 pub reason: TurnAbortReason,
1539}
1540
1541#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)]
1542#[serde(rename_all = "snake_case")]
1543pub enum TurnAbortReason {
1544 Interrupted,
1545 Replaced,
1546 ReviewEnded,
1547}
1548
1549#[cfg(test)]
1550mod tests {
1551 use super::*;
1552 use anyhow::Result;
1553 use serde_json::json;
1554 use tempfile::NamedTempFile;
1555
1556 #[test]
1559 fn serialize_event() -> Result<()> {
1560 let conversation_id = ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;
1561 let rollout_file = NamedTempFile::new()?;
1562 let event = Event {
1563 id: "1234".to_string(),
1564 event_seq: 1,
1565 msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
1566 session_id: conversation_id,
1567 model: "codex-mini-latest".to_string(),
1568 reasoning_effort: Some(ReasoningEffortConfig::default()),
1569 history_log_id: 0,
1570 history_entry_count: 0,
1571 initial_messages: None,
1572 rollout_path: rollout_file.path().to_path_buf(),
1573 }),
1574 order: None,
1575 };
1576
1577 let expected = json!({
1578 "id": "1234",
1579 "event_seq": 1,
1580 "msg": {
1581 "type": "session_configured",
1582 "session_id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
1583 "model": "codex-mini-latest",
1584 "reasoning_effort": "medium",
1585 "history_log_id": 0,
1586 "history_entry_count": 0,
1587 "rollout_path": format!("{}", rollout_file.path().display()),
1588 }
1589 });
1590 assert_eq!(expected, serde_json::to_value(&event)?);
1591 Ok(())
1592 }
1593
1594 #[test]
1595 fn vec_u8_as_base64_serialization_and_deserialization() -> Result<()> {
1596 let event = ExecCommandOutputDeltaEvent {
1597 call_id: "call21".to_string(),
1598 stream: ExecOutputStream::Stdout,
1599 chunk: vec![1, 2, 3, 4, 5],
1600 };
1601 let serialized = serde_json::to_string(&event)?;
1602 assert_eq!(
1603 r#"{"call_id":"call21","stream":"stdout","chunk":"AQIDBAU="}"#,
1604 serialized,
1605 );
1606
1607 let deserialized: ExecCommandOutputDeltaEvent = serde_json::from_str(&serialized)?;
1608 assert_eq!(deserialized, event);
1609 Ok(())
1610 }
1611
1612 #[test]
1613 fn compaction_checkpoint_warning_round_trips() -> Result<()> {
1614 let event = EventMsg::CompactionCheckpointWarning(CompactionCheckpointWarningEvent {
1615 message: "History checkpoint: earlier conversation compacted.".to_string(),
1616 });
1617
1618 let serialized = serde_json::to_string(&event)?;
1619 let restored: EventMsg = serde_json::from_str(&serialized)?;
1620
1621 match restored {
1622 EventMsg::CompactionCheckpointWarning(payload) => {
1623 assert!(payload.message.contains("checkpoint"));
1624 }
1625 other => panic!("unexpected variant: {other:?}"),
1626 }
1627
1628 Ok(())
1629 }
1630}