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::items::TurnItem;
18use crate::message_history::HistoryEntry;
19use crate::models::ContentItem;
20use crate::models::ResponseItem;
21use crate::num_format::format_with_separators;
22use crate::parse_command::ParsedCommand;
23use crate::plan_tool::UpdatePlanArgs;
24use crate::user_input::UserInput;
25use mcp_types::CallToolResult;
26use mcp_types::Resource as McpResource;
27use mcp_types::ResourceTemplate as McpResourceTemplate;
28use mcp_types::Tool as McpTool;
29use schemars::JsonSchema;
30use serde::Deserialize;
31use serde::Serialize;
32use serde_json::Value;
33use serde_with::serde_as;
34use strum_macros::Display;
35use ts_rs::TS;
36
37pub use crate::approvals::ApplyPatchApprovalRequestEvent;
38pub use crate::approvals::ExecApprovalRequestEvent;
39pub use crate::approvals::SandboxCommandAssessment;
40pub use crate::approvals::SandboxRiskLevel;
41
42pub const USER_INSTRUCTIONS_OPEN_TAG: &str = "<user_instructions>";
45pub const USER_INSTRUCTIONS_CLOSE_TAG: &str = "</user_instructions>";
46pub const ENVIRONMENT_CONTEXT_OPEN_TAG: &str = "<environment_context>";
47pub const ENVIRONMENT_CONTEXT_CLOSE_TAG: &str = "</environment_context>";
48pub const USER_MESSAGE_BEGIN: &str = "## My request for Codex:";
49
50#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
52pub struct Submission {
53 pub id: String,
55 pub op: Op,
57}
58
59#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema)]
61#[serde(tag = "type", rename_all = "snake_case")]
62#[allow(clippy::large_enum_variant)]
63#[non_exhaustive]
64pub enum Op {
65 Interrupt,
68
69 UserInput {
71 items: Vec<UserInput>,
73 },
74
75 UserTurn {
78 items: Vec<UserInput>,
80
81 cwd: PathBuf,
84
85 approval_policy: AskForApproval,
87
88 sandbox_policy: SandboxPolicy,
90
91 model: String,
94
95 #[serde(skip_serializing_if = "Option::is_none")]
97 effort: Option<ReasoningEffortConfig>,
98
99 summary: ReasoningSummaryConfig,
101 final_output_json_schema: Option<Value>,
103 },
104
105 OverrideTurnContext {
111 #[serde(skip_serializing_if = "Option::is_none")]
113 cwd: Option<PathBuf>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
117 approval_policy: Option<AskForApproval>,
118
119 #[serde(skip_serializing_if = "Option::is_none")]
121 sandbox_policy: Option<SandboxPolicy>,
122
123 #[serde(skip_serializing_if = "Option::is_none")]
126 model: Option<String>,
127
128 #[serde(skip_serializing_if = "Option::is_none")]
133 effort: Option<Option<ReasoningEffortConfig>>,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
137 summary: Option<ReasoningSummaryConfig>,
138 },
139
140 ExecApproval {
142 id: String,
144 decision: ReviewDecision,
146 },
147
148 PatchApproval {
150 id: String,
152 decision: ReviewDecision,
154 },
155
156 AddToHistory {
161 text: String,
163 },
164
165 GetHistoryEntryRequest { offset: usize, log_id: u64 },
167
168 ListMcpTools,
171
172 ListCustomPrompts,
174
175 Compact,
179
180 Undo,
182
183 Review { review_request: ReviewRequest },
185
186 Shutdown,
188
189 RunUserShellCommand {
195 command: String,
197 },
198}
199
200#[derive(
203 Debug,
204 Clone,
205 Copy,
206 Default,
207 PartialEq,
208 Eq,
209 Hash,
210 Serialize,
211 Deserialize,
212 Display,
213 JsonSchema,
214 TS,
215)]
216#[serde(rename_all = "kebab-case")]
217#[strum(serialize_all = "kebab-case")]
218pub enum AskForApproval {
219 #[serde(rename = "untrusted")]
223 #[strum(serialize = "untrusted")]
224 UnlessTrusted,
225
226 OnFailure,
231
232 #[default]
234 OnRequest,
235
236 Never,
239}
240
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, JsonSchema, TS)]
243#[strum(serialize_all = "kebab-case")]
244#[serde(tag = "type", rename_all = "kebab-case")]
245pub enum SandboxPolicy {
246 #[serde(rename = "danger-full-access")]
248 DangerFullAccess,
249
250 #[serde(rename = "read-only")]
252 ReadOnly,
253
254 #[serde(rename = "workspace-write")]
257 WorkspaceWrite {
258 #[serde(default, skip_serializing_if = "Vec::is_empty")]
261 writable_roots: Vec<PathBuf>,
262
263 #[serde(default)]
266 network_access: bool,
267
268 #[serde(default)]
272 exclude_tmpdir_env_var: bool,
273
274 #[serde(default)]
277 exclude_slash_tmp: bool,
278 },
279}
280
281#[derive(Debug, Clone, PartialEq, Eq, JsonSchema)]
286pub struct WritableRoot {
287 pub root: PathBuf,
289
290 pub read_only_subpaths: Vec<PathBuf>,
292}
293
294impl WritableRoot {
295 pub fn is_path_writable(&self, path: &Path) -> bool {
296 if !path.starts_with(&self.root) {
298 return false;
299 }
300
301 for subpath in &self.read_only_subpaths {
303 if path.starts_with(subpath) {
304 return false;
305 }
306 }
307
308 true
309 }
310}
311
312impl FromStr for SandboxPolicy {
313 type Err = serde_json::Error;
314
315 fn from_str(s: &str) -> Result<Self, Self::Err> {
316 serde_json::from_str(s)
317 }
318}
319
320impl SandboxPolicy {
321 pub fn new_read_only_policy() -> Self {
323 SandboxPolicy::ReadOnly
324 }
325
326 pub fn new_workspace_write_policy() -> Self {
330 SandboxPolicy::WorkspaceWrite {
331 writable_roots: vec![],
332 network_access: false,
333 exclude_tmpdir_env_var: false,
334 exclude_slash_tmp: false,
335 }
336 }
337
338 pub fn has_full_disk_read_access(&self) -> bool {
340 true
341 }
342
343 pub fn has_full_disk_write_access(&self) -> bool {
344 match self {
345 SandboxPolicy::DangerFullAccess => true,
346 SandboxPolicy::ReadOnly => false,
347 SandboxPolicy::WorkspaceWrite { .. } => false,
348 }
349 }
350
351 pub fn has_full_network_access(&self) -> bool {
352 match self {
353 SandboxPolicy::DangerFullAccess => true,
354 SandboxPolicy::ReadOnly => false,
355 SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access,
356 }
357 }
358
359 pub fn get_writable_roots_with_cwd(&self, cwd: &Path) -> Vec<WritableRoot> {
363 match self {
364 SandboxPolicy::DangerFullAccess => Vec::new(),
365 SandboxPolicy::ReadOnly => Vec::new(),
366 SandboxPolicy::WorkspaceWrite {
367 writable_roots,
368 exclude_tmpdir_env_var,
369 exclude_slash_tmp,
370 network_access: _,
371 } => {
372 let mut roots: Vec<PathBuf> = writable_roots.clone();
374
375 roots.push(cwd.to_path_buf());
378
379 if cfg!(unix) && !exclude_slash_tmp {
381 let slash_tmp = PathBuf::from("/tmp");
382 if slash_tmp.is_dir() {
383 roots.push(slash_tmp);
384 }
385 }
386
387 if !exclude_tmpdir_env_var
396 && let Some(tmpdir) = std::env::var_os("TMPDIR")
397 && !tmpdir.is_empty()
398 {
399 roots.push(PathBuf::from(tmpdir));
400 }
401
402 roots
404 .into_iter()
405 .map(|writable_root| {
406 let mut subpaths = Vec::new();
407 let top_level_git = writable_root.join(".git");
408 if top_level_git.is_dir() {
409 subpaths.push(top_level_git);
410 }
411 WritableRoot {
412 root: writable_root,
413 read_only_subpaths: subpaths,
414 }
415 })
416 .collect()
417 }
418 }
419 }
420}
421
422#[derive(Debug, Clone, Deserialize, Serialize)]
424pub struct Event {
425 pub id: String,
427 pub msg: EventMsg,
429}
430
431#[derive(Debug, Clone, Deserialize, Serialize, Display, JsonSchema, TS)]
434#[serde(tag = "type", rename_all = "snake_case")]
435#[ts(tag = "type")]
436#[strum(serialize_all = "snake_case")]
437pub enum EventMsg {
438 Error(ErrorEvent),
440
441 Warning(WarningEvent),
444
445 TaskStarted(TaskStartedEvent),
447
448 TaskComplete(TaskCompleteEvent),
450
451 TokenCount(TokenCountEvent),
454
455 AgentMessage(AgentMessageEvent),
457
458 UserMessage(UserMessageEvent),
460
461 AgentMessageDelta(AgentMessageDeltaEvent),
463
464 AgentReasoning(AgentReasoningEvent),
466
467 AgentReasoningDelta(AgentReasoningDeltaEvent),
469
470 AgentReasoningRawContent(AgentReasoningRawContentEvent),
472
473 AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent),
475 AgentReasoningSectionBreak(AgentReasoningSectionBreakEvent),
477
478 SessionConfigured(SessionConfiguredEvent),
480
481 McpStartupUpdate(McpStartupUpdateEvent),
483
484 McpStartupComplete(McpStartupCompleteEvent),
486
487 McpToolCallBegin(McpToolCallBeginEvent),
488
489 McpToolCallEnd(McpToolCallEndEvent),
490
491 WebSearchBegin(WebSearchBeginEvent),
492
493 WebSearchEnd(WebSearchEndEvent),
494
495 ExecCommandBegin(ExecCommandBeginEvent),
497
498 ExecCommandOutputDelta(ExecCommandOutputDeltaEvent),
500
501 ExecCommandEnd(ExecCommandEndEvent),
502
503 ViewImageToolCall(ViewImageToolCallEvent),
505
506 ExecApprovalRequest(ExecApprovalRequestEvent),
507
508 ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent),
509
510 DeprecationNotice(DeprecationNoticeEvent),
513
514 BackgroundEvent(BackgroundEventEvent),
515
516 UndoStarted(UndoStartedEvent),
517
518 UndoCompleted(UndoCompletedEvent),
519
520 StreamError(StreamErrorEvent),
523
524 PatchApplyBegin(PatchApplyBeginEvent),
527
528 PatchApplyEnd(PatchApplyEndEvent),
530
531 TurnDiff(TurnDiffEvent),
532
533 GetHistoryEntryResponse(GetHistoryEntryResponseEvent),
535
536 McpListToolsResponse(McpListToolsResponseEvent),
538
539 ListCustomPromptsResponse(ListCustomPromptsResponseEvent),
541
542 PlanUpdate(UpdatePlanArgs),
543
544 TurnAborted(TurnAbortedEvent),
545
546 ShutdownComplete,
548
549 EnteredReviewMode(ReviewRequest),
551
552 ExitedReviewMode(ExitedReviewModeEvent),
554
555 RawResponseItem(RawResponseItemEvent),
556
557 ItemStarted(ItemStartedEvent),
558 ItemCompleted(ItemCompletedEvent),
559
560 AgentMessageContentDelta(AgentMessageContentDeltaEvent),
561 ReasoningContentDelta(ReasoningContentDeltaEvent),
562 ReasoningRawContentDelta(ReasoningRawContentDeltaEvent),
563}
564
565#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
567#[serde(rename_all = "snake_case")]
568#[ts(rename_all = "snake_case")]
569pub enum CodexErrorInfo {
570 ContextWindowExceeded,
571 UsageLimitExceeded,
572 HttpConnectionFailed {
573 http_status_code: Option<u16>,
574 },
575 ResponseStreamConnectionFailed {
577 http_status_code: Option<u16>,
578 },
579 InternalServerError,
580 Unauthorized,
581 BadRequest,
582 SandboxError,
583 ResponseStreamDisconnected {
585 http_status_code: Option<u16>,
586 },
587 ResponseTooManyFailedAttempts {
589 http_status_code: Option<u16>,
590 },
591 Other,
592}
593
594#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
595pub struct RawResponseItemEvent {
596 pub item: ResponseItem,
597}
598
599#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
600pub struct ItemStartedEvent {
601 pub thread_id: ConversationId,
602 pub turn_id: String,
603 pub item: TurnItem,
604}
605
606impl HasLegacyEvent for ItemStartedEvent {
607 fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
608 match &self.item {
609 TurnItem::WebSearch(item) => vec![EventMsg::WebSearchBegin(WebSearchBeginEvent {
610 call_id: item.id.clone(),
611 })],
612 _ => Vec::new(),
613 }
614 }
615}
616
617#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
618pub struct ItemCompletedEvent {
619 pub thread_id: ConversationId,
620 pub turn_id: String,
621 pub item: TurnItem,
622}
623
624pub trait HasLegacyEvent {
625 fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg>;
626}
627
628impl HasLegacyEvent for ItemCompletedEvent {
629 fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg> {
630 self.item.as_legacy_events(show_raw_agent_reasoning)
631 }
632}
633
634#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
635pub struct AgentMessageContentDeltaEvent {
636 pub thread_id: String,
637 pub turn_id: String,
638 pub item_id: String,
639 pub delta: String,
640}
641
642impl HasLegacyEvent for AgentMessageContentDeltaEvent {
643 fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
644 vec![EventMsg::AgentMessageDelta(AgentMessageDeltaEvent {
645 delta: self.delta.clone(),
646 })]
647 }
648}
649
650#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
651pub struct ReasoningContentDeltaEvent {
652 pub thread_id: String,
653 pub turn_id: String,
654 pub item_id: String,
655 pub delta: String,
656 #[serde(default)]
658 pub summary_index: i64,
659}
660
661impl HasLegacyEvent for ReasoningContentDeltaEvent {
662 fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
663 vec![EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent {
664 delta: self.delta.clone(),
665 })]
666 }
667}
668
669#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
670pub struct ReasoningRawContentDeltaEvent {
671 pub thread_id: String,
672 pub turn_id: String,
673 pub item_id: String,
674 pub delta: String,
675 #[serde(default)]
677 pub content_index: i64,
678}
679
680impl HasLegacyEvent for ReasoningRawContentDeltaEvent {
681 fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
682 vec![EventMsg::AgentReasoningRawContentDelta(
683 AgentReasoningRawContentDeltaEvent {
684 delta: self.delta.clone(),
685 },
686 )]
687 }
688}
689
690impl HasLegacyEvent for EventMsg {
691 fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg> {
692 match self {
693 EventMsg::ItemCompleted(event) => event.as_legacy_events(show_raw_agent_reasoning),
694 EventMsg::AgentMessageContentDelta(event) => {
695 event.as_legacy_events(show_raw_agent_reasoning)
696 }
697 EventMsg::ReasoningContentDelta(event) => {
698 event.as_legacy_events(show_raw_agent_reasoning)
699 }
700 EventMsg::ReasoningRawContentDelta(event) => {
701 event.as_legacy_events(show_raw_agent_reasoning)
702 }
703 _ => Vec::new(),
704 }
705 }
706}
707
708#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
709pub struct ExitedReviewModeEvent {
710 pub review_output: Option<ReviewOutputEvent>,
711}
712
713#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
716pub struct ErrorEvent {
717 pub message: String,
718 #[serde(default)]
719 pub codex_error_info: Option<CodexErrorInfo>,
720}
721
722#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
723pub struct WarningEvent {
724 pub message: String,
725}
726
727#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
728pub struct TaskCompleteEvent {
729 pub last_agent_message: Option<String>,
730}
731
732#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
733pub struct TaskStartedEvent {
734 pub model_context_window: Option<i64>,
735}
736
737#[derive(Debug, Clone, Deserialize, Serialize, Default, JsonSchema, TS)]
738pub struct TokenUsage {
739 #[ts(type = "number")]
740 pub input_tokens: i64,
741 #[ts(type = "number")]
742 pub cached_input_tokens: i64,
743 #[ts(type = "number")]
744 pub output_tokens: i64,
745 #[ts(type = "number")]
746 pub reasoning_output_tokens: i64,
747 #[ts(type = "number")]
748 pub total_tokens: i64,
749}
750
751#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
752pub struct TokenUsageInfo {
753 pub total_token_usage: TokenUsage,
754 pub last_token_usage: TokenUsage,
755 #[ts(type = "number | null")]
756 pub model_context_window: Option<i64>,
757}
758
759impl TokenUsageInfo {
760 pub fn new_or_append(
761 info: &Option<TokenUsageInfo>,
762 last: &Option<TokenUsage>,
763 model_context_window: Option<i64>,
764 ) -> Option<Self> {
765 if info.is_none() && last.is_none() {
766 return None;
767 }
768
769 let mut info = match info {
770 Some(info) => info.clone(),
771 None => Self {
772 total_token_usage: TokenUsage::default(),
773 last_token_usage: TokenUsage::default(),
774 model_context_window,
775 },
776 };
777 if let Some(last) = last {
778 info.append_last_usage(last);
779 }
780 Some(info)
781 }
782
783 pub fn append_last_usage(&mut self, last: &TokenUsage) {
784 self.total_token_usage.add_assign(last);
785 self.last_token_usage = last.clone();
786 }
787
788 pub fn fill_to_context_window(&mut self, context_window: i64) {
789 let previous_total = self.total_token_usage.total_tokens;
790 let delta = (context_window - previous_total).max(0);
791
792 self.model_context_window = Some(context_window);
793 self.total_token_usage = TokenUsage {
794 total_tokens: context_window,
795 ..TokenUsage::default()
796 };
797 self.last_token_usage = TokenUsage {
798 total_tokens: delta,
799 ..TokenUsage::default()
800 };
801 }
802
803 pub fn full_context_window(context_window: i64) -> Self {
804 let mut info = Self {
805 total_token_usage: TokenUsage::default(),
806 last_token_usage: TokenUsage::default(),
807 model_context_window: Some(context_window),
808 };
809 info.fill_to_context_window(context_window);
810 info
811 }
812}
813
814#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
815pub struct TokenCountEvent {
816 pub info: Option<TokenUsageInfo>,
817 pub rate_limits: Option<RateLimitSnapshot>,
818}
819
820#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
821pub struct RateLimitSnapshot {
822 pub primary: Option<RateLimitWindow>,
823 pub secondary: Option<RateLimitWindow>,
824 pub credits: Option<CreditsSnapshot>,
825}
826
827#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
828pub struct RateLimitWindow {
829 pub used_percent: f64,
831 #[ts(type = "number | null")]
833 pub window_minutes: Option<i64>,
834 #[ts(type = "number | null")]
836 pub resets_at: Option<i64>,
837}
838
839#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
840pub struct CreditsSnapshot {
841 pub has_credits: bool,
842 pub unlimited: bool,
843 pub balance: Option<String>,
844}
845
846const BASELINE_TOKENS: i64 = 12000;
848
849impl TokenUsage {
850 pub fn is_zero(&self) -> bool {
851 self.total_tokens == 0
852 }
853
854 pub fn cached_input(&self) -> i64 {
855 self.cached_input_tokens.max(0)
856 }
857
858 pub fn non_cached_input(&self) -> i64 {
859 (self.input_tokens - self.cached_input()).max(0)
860 }
861
862 pub fn blended_total(&self) -> i64 {
864 (self.non_cached_input() + self.output_tokens.max(0)).max(0)
865 }
866
867 pub fn tokens_in_context_window(&self) -> i64 {
868 self.total_tokens
869 }
870
871 pub fn percent_of_context_window_remaining(&self, context_window: i64) -> i64 {
882 if context_window <= BASELINE_TOKENS {
883 return 0;
884 }
885
886 let effective_window = context_window - BASELINE_TOKENS;
887 let used = (self.tokens_in_context_window() - BASELINE_TOKENS).max(0);
888 let remaining = (effective_window - used).max(0);
889 ((remaining as f64 / effective_window as f64) * 100.0)
890 .clamp(0.0, 100.0)
891 .round() as i64
892 }
893
894 pub fn add_assign(&mut self, other: &TokenUsage) {
896 self.input_tokens += other.input_tokens;
897 self.cached_input_tokens += other.cached_input_tokens;
898 self.output_tokens += other.output_tokens;
899 self.reasoning_output_tokens += other.reasoning_output_tokens;
900 self.total_tokens += other.total_tokens;
901 }
902}
903
904#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
905pub struct FinalOutput {
906 pub token_usage: TokenUsage,
907}
908
909impl From<TokenUsage> for FinalOutput {
910 fn from(token_usage: TokenUsage) -> Self {
911 Self { token_usage }
912 }
913}
914
915impl fmt::Display for FinalOutput {
916 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
917 let token_usage = &self.token_usage;
918
919 write!(
920 f,
921 "Token usage: total={} input={}{} output={}{}",
922 format_with_separators(token_usage.blended_total()),
923 format_with_separators(token_usage.non_cached_input()),
924 if token_usage.cached_input() > 0 {
925 format!(
926 " (+ {} cached)",
927 format_with_separators(token_usage.cached_input())
928 )
929 } else {
930 String::new()
931 },
932 format_with_separators(token_usage.output_tokens),
933 if token_usage.reasoning_output_tokens > 0 {
934 format!(
935 " (reasoning {})",
936 format_with_separators(token_usage.reasoning_output_tokens)
937 )
938 } else {
939 String::new()
940 }
941 )
942 }
943}
944
945#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
946pub struct AgentMessageEvent {
947 pub message: String,
948}
949
950#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
951pub struct UserMessageEvent {
952 pub message: String,
953 #[serde(skip_serializing_if = "Option::is_none")]
954 pub images: Option<Vec<String>>,
955}
956
957#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
958pub struct AgentMessageDeltaEvent {
959 pub delta: String,
960}
961
962#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
963pub struct AgentReasoningEvent {
964 pub text: String,
965}
966
967#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
968pub struct AgentReasoningRawContentEvent {
969 pub text: String,
970}
971
972#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
973pub struct AgentReasoningRawContentDeltaEvent {
974 pub delta: String,
975}
976
977#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
978pub struct AgentReasoningSectionBreakEvent {
979 #[serde(default)]
981 pub item_id: String,
982 #[serde(default)]
983 pub summary_index: i64,
984}
985
986#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
987pub struct AgentReasoningDeltaEvent {
988 pub delta: String,
989}
990
991#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
992pub struct McpInvocation {
993 pub server: String,
995 pub tool: String,
997 pub arguments: Option<serde_json::Value>,
999}
1000
1001#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
1002pub struct McpToolCallBeginEvent {
1003 pub call_id: String,
1005 pub invocation: McpInvocation,
1006}
1007
1008#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
1009pub struct McpToolCallEndEvent {
1010 pub call_id: String,
1012 pub invocation: McpInvocation,
1013 #[ts(type = "string")]
1014 pub duration: Duration,
1015 pub result: Result<CallToolResult, String>,
1017}
1018
1019impl McpToolCallEndEvent {
1020 pub fn is_success(&self) -> bool {
1021 match &self.result {
1022 Ok(result) => !result.is_error.unwrap_or(false),
1023 Err(_) => false,
1024 }
1025 }
1026}
1027
1028#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1029pub struct WebSearchBeginEvent {
1030 pub call_id: String,
1031}
1032
1033#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1034pub struct WebSearchEndEvent {
1035 pub call_id: String,
1036 pub query: String,
1037}
1038
1039#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1042pub struct ConversationPathResponseEvent {
1043 pub conversation_id: ConversationId,
1044 pub path: PathBuf,
1045}
1046
1047#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1048pub struct ResumedHistory {
1049 pub conversation_id: ConversationId,
1050 pub history: Vec<RolloutItem>,
1051 pub rollout_path: PathBuf,
1052}
1053
1054#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1055pub enum InitialHistory {
1056 New,
1057 Resumed(ResumedHistory),
1058 Forked(Vec<RolloutItem>),
1059}
1060
1061impl InitialHistory {
1062 pub fn get_rollout_items(&self) -> Vec<RolloutItem> {
1063 match self {
1064 InitialHistory::New => Vec::new(),
1065 InitialHistory::Resumed(resumed) => resumed.history.clone(),
1066 InitialHistory::Forked(items) => items.clone(),
1067 }
1068 }
1069
1070 pub fn get_event_msgs(&self) -> Option<Vec<EventMsg>> {
1071 match self {
1072 InitialHistory::New => None,
1073 InitialHistory::Resumed(resumed) => Some(
1074 resumed
1075 .history
1076 .iter()
1077 .filter_map(|ri| match ri {
1078 RolloutItem::EventMsg(ev) => Some(ev.clone()),
1079 _ => None,
1080 })
1081 .collect(),
1082 ),
1083 InitialHistory::Forked(items) => Some(
1084 items
1085 .iter()
1086 .filter_map(|ri| match ri {
1087 RolloutItem::EventMsg(ev) => Some(ev.clone()),
1088 _ => None,
1089 })
1090 .collect(),
1091 ),
1092 }
1093 }
1094}
1095
1096#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
1097#[serde(rename_all = "lowercase")]
1098#[ts(rename_all = "lowercase")]
1099pub enum SessionSource {
1100 Cli,
1101 #[default]
1102 VSCode,
1103 Exec,
1104 Mcp,
1105 SubAgent(SubAgentSource),
1106 #[serde(other)]
1107 Unknown,
1108}
1109
1110#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
1111#[serde(rename_all = "snake_case")]
1112#[ts(rename_all = "snake_case")]
1113pub enum SubAgentSource {
1114 Review,
1115 Compact,
1116 Other(String),
1117}
1118
1119#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
1120pub struct SessionMeta {
1121 pub id: ConversationId,
1122 pub timestamp: String,
1123 pub cwd: PathBuf,
1124 pub originator: String,
1125 pub cli_version: String,
1126 pub instructions: Option<String>,
1127 #[serde(default)]
1128 pub source: SessionSource,
1129 pub model_provider: Option<String>,
1130}
1131
1132impl Default for SessionMeta {
1133 fn default() -> Self {
1134 SessionMeta {
1135 id: ConversationId::default(),
1136 timestamp: String::new(),
1137 cwd: PathBuf::new(),
1138 originator: String::new(),
1139 cli_version: String::new(),
1140 instructions: None,
1141 source: SessionSource::default(),
1142 model_provider: None,
1143 }
1144 }
1145}
1146
1147#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
1148pub struct SessionMetaLine {
1149 #[serde(flatten)]
1150 pub meta: SessionMeta,
1151 #[serde(skip_serializing_if = "Option::is_none")]
1152 pub git: Option<GitInfo>,
1153}
1154
1155#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
1156#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
1157pub enum RolloutItem {
1158 SessionMeta(SessionMetaLine),
1159 ResponseItem(ResponseItem),
1160 Compacted(CompactedItem),
1161 TurnContext(TurnContextItem),
1162 EventMsg(EventMsg),
1163}
1164
1165#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
1166pub struct CompactedItem {
1167 pub message: String,
1168 #[serde(default, skip_serializing_if = "Option::is_none")]
1169 pub replacement_history: Option<Vec<ResponseItem>>,
1170}
1171
1172impl From<CompactedItem> for ResponseItem {
1173 fn from(value: CompactedItem) -> Self {
1174 ResponseItem::Message {
1175 id: None,
1176 role: "assistant".to_string(),
1177 content: vec![ContentItem::OutputText {
1178 text: value.message,
1179 }],
1180 }
1181 }
1182}
1183
1184#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
1185pub struct TurnContextItem {
1186 pub cwd: PathBuf,
1187 pub approval_policy: AskForApproval,
1188 pub sandbox_policy: SandboxPolicy,
1189 pub model: String,
1190 #[serde(skip_serializing_if = "Option::is_none")]
1191 pub effort: Option<ReasoningEffortConfig>,
1192 pub summary: ReasoningSummaryConfig,
1193}
1194
1195#[derive(Serialize, Deserialize, Clone, JsonSchema)]
1196pub struct RolloutLine {
1197 pub timestamp: String,
1198 #[serde(flatten)]
1199 pub item: RolloutItem,
1200}
1201
1202#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
1203pub struct GitInfo {
1204 #[serde(skip_serializing_if = "Option::is_none")]
1206 pub commit_hash: Option<String>,
1207 #[serde(skip_serializing_if = "Option::is_none")]
1209 pub branch: Option<String>,
1210 #[serde(skip_serializing_if = "Option::is_none")]
1212 pub repository_url: Option<String>,
1213}
1214
1215#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1216pub struct ReviewRequest {
1218 pub prompt: String,
1219 pub user_facing_hint: String,
1220 #[serde(default)]
1221 pub append_to_original_thread: bool,
1222}
1223
1224#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1226pub struct ReviewOutputEvent {
1227 pub findings: Vec<ReviewFinding>,
1228 pub overall_correctness: String,
1229 pub overall_explanation: String,
1230 pub overall_confidence_score: f32,
1231}
1232
1233impl Default for ReviewOutputEvent {
1234 fn default() -> Self {
1235 Self {
1236 findings: Vec::new(),
1237 overall_correctness: String::default(),
1238 overall_explanation: String::default(),
1239 overall_confidence_score: 0.0,
1240 }
1241 }
1242}
1243
1244#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1246pub struct ReviewFinding {
1247 pub title: String,
1248 pub body: String,
1249 pub confidence_score: f32,
1250 pub priority: i32,
1251 pub code_location: ReviewCodeLocation,
1252}
1253
1254#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1256pub struct ReviewCodeLocation {
1257 pub absolute_file_path: PathBuf,
1258 pub line_range: ReviewLineRange,
1259}
1260
1261#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1263pub struct ReviewLineRange {
1264 pub start: u32,
1265 pub end: u32,
1266}
1267
1268#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1269#[serde(rename_all = "snake_case")]
1270pub enum ExecCommandSource {
1271 Agent,
1272 UserShell,
1273 UnifiedExecStartup,
1274 UnifiedExecInteraction,
1275}
1276
1277impl Default for ExecCommandSource {
1278 fn default() -> Self {
1279 Self::Agent
1280 }
1281}
1282
1283#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1284pub struct ExecCommandBeginEvent {
1285 pub call_id: String,
1287 pub turn_id: String,
1289 pub command: Vec<String>,
1291 pub cwd: PathBuf,
1293 pub parsed_cmd: Vec<ParsedCommand>,
1294 #[serde(default)]
1296 pub source: ExecCommandSource,
1297 #[serde(default, skip_serializing_if = "Option::is_none")]
1299 #[ts(optional)]
1300 pub interaction_input: Option<String>,
1301}
1302
1303#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1304pub struct ExecCommandEndEvent {
1305 pub call_id: String,
1307 pub turn_id: String,
1309 pub command: Vec<String>,
1311 pub cwd: PathBuf,
1313 pub parsed_cmd: Vec<ParsedCommand>,
1314 #[serde(default)]
1316 pub source: ExecCommandSource,
1317 #[serde(default, skip_serializing_if = "Option::is_none")]
1319 #[ts(optional)]
1320 pub interaction_input: Option<String>,
1321
1322 pub stdout: String,
1324 pub stderr: String,
1326 #[serde(default)]
1328 pub aggregated_output: String,
1329 pub exit_code: i32,
1331 #[ts(type = "string")]
1333 pub duration: Duration,
1334 pub formatted_output: String,
1336}
1337
1338#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1339pub struct ViewImageToolCallEvent {
1340 pub call_id: String,
1342 pub path: PathBuf,
1344}
1345
1346#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1347#[serde(rename_all = "snake_case")]
1348pub enum ExecOutputStream {
1349 Stdout,
1350 Stderr,
1351}
1352
1353#[serde_as]
1354#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1355pub struct ExecCommandOutputDeltaEvent {
1356 pub call_id: String,
1358 pub stream: ExecOutputStream,
1360 #[serde_as(as = "serde_with::base64::Base64")]
1362 #[schemars(with = "String")]
1363 #[ts(type = "string")]
1364 pub chunk: Vec<u8>,
1365}
1366
1367#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1368pub struct BackgroundEventEvent {
1369 pub message: String,
1370}
1371
1372#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1373pub struct DeprecationNoticeEvent {
1374 pub summary: String,
1376 #[serde(skip_serializing_if = "Option::is_none")]
1378 pub details: Option<String>,
1379}
1380
1381#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1382pub struct UndoStartedEvent {
1383 #[serde(skip_serializing_if = "Option::is_none")]
1384 pub message: Option<String>,
1385}
1386
1387#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1388pub struct UndoCompletedEvent {
1389 pub success: bool,
1390 #[serde(skip_serializing_if = "Option::is_none")]
1391 pub message: Option<String>,
1392}
1393
1394#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1395pub struct StreamErrorEvent {
1396 pub message: String,
1397 #[serde(default)]
1398 pub codex_error_info: Option<CodexErrorInfo>,
1399}
1400
1401#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1402pub struct StreamInfoEvent {
1403 pub message: String,
1404}
1405
1406#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1407pub struct PatchApplyBeginEvent {
1408 pub call_id: String,
1410 #[serde(default)]
1413 pub turn_id: String,
1414 pub auto_approved: bool,
1416 pub changes: HashMap<PathBuf, FileChange>,
1418}
1419
1420#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1421pub struct PatchApplyEndEvent {
1422 pub call_id: String,
1424 #[serde(default)]
1427 pub turn_id: String,
1428 pub stdout: String,
1430 pub stderr: String,
1432 pub success: bool,
1434 #[serde(default)]
1436 pub changes: HashMap<PathBuf, FileChange>,
1437}
1438
1439#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1440pub struct TurnDiffEvent {
1441 pub unified_diff: String,
1442}
1443
1444#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1445pub struct GetHistoryEntryResponseEvent {
1446 pub offset: usize,
1447 pub log_id: u64,
1448 #[serde(skip_serializing_if = "Option::is_none")]
1450 pub entry: Option<HistoryEntry>,
1451}
1452
1453#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1454pub struct McpListToolsResponseEvent {
1455 pub tools: std::collections::HashMap<String, McpTool>,
1457 pub resources: std::collections::HashMap<String, Vec<McpResource>>,
1459 pub resource_templates: std::collections::HashMap<String, Vec<McpResourceTemplate>>,
1461 pub auth_statuses: std::collections::HashMap<String, McpAuthStatus>,
1463}
1464
1465#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1466pub struct McpStartupUpdateEvent {
1467 pub server: String,
1469 pub status: McpStartupStatus,
1471}
1472
1473#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1474#[serde(rename_all = "snake_case", tag = "state")]
1475#[ts(rename_all = "snake_case", tag = "state")]
1476pub enum McpStartupStatus {
1477 Starting,
1478 Ready,
1479 Failed { error: String },
1480 Cancelled,
1481}
1482
1483#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, Default)]
1484pub struct McpStartupCompleteEvent {
1485 pub ready: Vec<String>,
1486 pub failed: Vec<McpStartupFailure>,
1487 pub cancelled: Vec<String>,
1488}
1489
1490#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1491pub struct McpStartupFailure {
1492 pub server: String,
1493 pub error: String,
1494}
1495
1496#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
1497#[serde(rename_all = "snake_case")]
1498#[ts(rename_all = "snake_case")]
1499pub enum McpAuthStatus {
1500 Unsupported,
1501 NotLoggedIn,
1502 BearerToken,
1503 OAuth,
1504}
1505
1506impl fmt::Display for McpAuthStatus {
1507 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1508 let text = match self {
1509 McpAuthStatus::Unsupported => "Unsupported",
1510 McpAuthStatus::NotLoggedIn => "Not logged in",
1511 McpAuthStatus::BearerToken => "Bearer token",
1512 McpAuthStatus::OAuth => "OAuth",
1513 };
1514 f.write_str(text)
1515 }
1516}
1517
1518#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1520pub struct ListCustomPromptsResponseEvent {
1521 pub custom_prompts: Vec<CustomPrompt>,
1522}
1523
1524#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1525pub struct SessionConfiguredEvent {
1526 pub session_id: ConversationId,
1528
1529 pub model: String,
1531
1532 pub model_provider_id: String,
1533
1534 pub approval_policy: AskForApproval,
1536
1537 pub sandbox_policy: SandboxPolicy,
1539
1540 pub cwd: PathBuf,
1543
1544 #[serde(skip_serializing_if = "Option::is_none")]
1546 pub reasoning_effort: Option<ReasoningEffortConfig>,
1547
1548 pub history_log_id: u64,
1550
1551 pub history_entry_count: usize,
1553
1554 #[serde(skip_serializing_if = "Option::is_none")]
1557 pub initial_messages: Option<Vec<EventMsg>>,
1558
1559 pub rollout_path: PathBuf,
1560}
1561
1562#[derive(
1564 Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Display, JsonSchema, TS,
1565)]
1566#[serde(rename_all = "snake_case")]
1567pub enum ReviewDecision {
1568 Approved,
1570
1571 ApprovedForSession,
1575
1576 #[default]
1579 Denied,
1580
1581 Abort,
1584}
1585
1586#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1587#[serde(tag = "type", rename_all = "snake_case")]
1588#[ts(tag = "type")]
1589pub enum FileChange {
1590 Add {
1591 content: String,
1592 },
1593 Delete {
1594 content: String,
1595 },
1596 Update {
1597 unified_diff: String,
1598 move_path: Option<PathBuf>,
1599 },
1600}
1601
1602#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1603pub struct Chunk {
1604 pub orig_index: u32,
1606 pub deleted_lines: Vec<String>,
1607 pub inserted_lines: Vec<String>,
1608}
1609
1610#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1611pub struct TurnAbortedEvent {
1612 pub reason: TurnAbortReason,
1613}
1614
1615#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1616#[serde(rename_all = "snake_case")]
1617pub enum TurnAbortReason {
1618 Interrupted,
1619 Replaced,
1620 ReviewEnded,
1621}
1622
1623#[cfg(test)]
1624mod tests {
1625 use super::*;
1626 use crate::items::UserMessageItem;
1627 use crate::items::WebSearchItem;
1628 use anyhow::Result;
1629 use pretty_assertions::assert_eq;
1630 use serde_json::json;
1631 use tempfile::NamedTempFile;
1632
1633 #[test]
1634 fn item_started_event_from_web_search_emits_begin_event() {
1635 let event = ItemStartedEvent {
1636 thread_id: ConversationId::new(),
1637 turn_id: "turn-1".into(),
1638 item: TurnItem::WebSearch(WebSearchItem {
1639 id: "search-1".into(),
1640 query: "find docs".into(),
1641 }),
1642 };
1643
1644 let legacy_events = event.as_legacy_events(false);
1645 assert_eq!(legacy_events.len(), 1);
1646 match &legacy_events[0] {
1647 EventMsg::WebSearchBegin(event) => assert_eq!(event.call_id, "search-1"),
1648 _ => panic!("expected WebSearchBegin event"),
1649 }
1650 }
1651
1652 #[test]
1653 fn item_started_event_from_non_web_search_emits_no_legacy_events() {
1654 let event = ItemStartedEvent {
1655 thread_id: ConversationId::new(),
1656 turn_id: "turn-1".into(),
1657 item: TurnItem::UserMessage(UserMessageItem::new(&[])),
1658 };
1659
1660 assert!(event.as_legacy_events(false).is_empty());
1661 }
1662
1663 #[test]
1666 fn serialize_event() -> Result<()> {
1667 let conversation_id = ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;
1668 let rollout_file = NamedTempFile::new()?;
1669 let event = Event {
1670 id: "1234".to_string(),
1671 msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
1672 session_id: conversation_id,
1673 model: "codex-mini-latest".to_string(),
1674 model_provider_id: "openai".to_string(),
1675 approval_policy: AskForApproval::Never,
1676 sandbox_policy: SandboxPolicy::ReadOnly,
1677 cwd: PathBuf::from("/home/user/project"),
1678 reasoning_effort: Some(ReasoningEffortConfig::default()),
1679 history_log_id: 0,
1680 history_entry_count: 0,
1681 initial_messages: None,
1682 rollout_path: rollout_file.path().to_path_buf(),
1683 }),
1684 };
1685
1686 let expected = json!({
1687 "id": "1234",
1688 "msg": {
1689 "type": "session_configured",
1690 "session_id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
1691 "model": "codex-mini-latest",
1692 "model_provider_id": "openai",
1693 "approval_policy": "never",
1694 "sandbox_policy": {
1695 "type": "read-only"
1696 },
1697 "cwd": "/home/user/project",
1698 "reasoning_effort": "medium",
1699 "history_log_id": 0,
1700 "history_entry_count": 0,
1701 "rollout_path": format!("{}", rollout_file.path().display()),
1702 }
1703 });
1704 assert_eq!(expected, serde_json::to_value(&event)?);
1705 Ok(())
1706 }
1707
1708 #[test]
1709 fn vec_u8_as_base64_serialization_and_deserialization() -> Result<()> {
1710 let event = ExecCommandOutputDeltaEvent {
1711 call_id: "call21".to_string(),
1712 stream: ExecOutputStream::Stdout,
1713 chunk: vec![1, 2, 3, 4, 5],
1714 };
1715 let serialized = serde_json::to_string(&event)?;
1716 assert_eq!(
1717 r#"{"call_id":"call21","stream":"stdout","chunk":"AQIDBAU="}"#,
1718 serialized,
1719 );
1720
1721 let deserialized: ExecCommandOutputDeltaEvent = serde_json::from_str(&serialized)?;
1722 assert_eq!(deserialized, event);
1723 Ok(())
1724 }
1725
1726 #[test]
1727 fn serialize_mcp_startup_update_event() -> Result<()> {
1728 let event = Event {
1729 id: "init".to_string(),
1730 msg: EventMsg::McpStartupUpdate(McpStartupUpdateEvent {
1731 server: "srv".to_string(),
1732 status: McpStartupStatus::Failed {
1733 error: "boom".to_string(),
1734 },
1735 }),
1736 };
1737
1738 let value = serde_json::to_value(&event)?;
1739 assert_eq!(value["msg"]["type"], "mcp_startup_update");
1740 assert_eq!(value["msg"]["server"], "srv");
1741 assert_eq!(value["msg"]["status"]["state"], "failed");
1742 assert_eq!(value["msg"]["status"]["error"], "boom");
1743 Ok(())
1744 }
1745
1746 #[test]
1747 fn serialize_mcp_startup_complete_event() -> Result<()> {
1748 let event = Event {
1749 id: "init".to_string(),
1750 msg: EventMsg::McpStartupComplete(McpStartupCompleteEvent {
1751 ready: vec!["a".to_string()],
1752 failed: vec![McpStartupFailure {
1753 server: "b".to_string(),
1754 error: "bad".to_string(),
1755 }],
1756 cancelled: vec!["c".to_string()],
1757 }),
1758 };
1759
1760 let value = serde_json::to_value(&event)?;
1761 assert_eq!(value["msg"]["type"], "mcp_startup_complete");
1762 assert_eq!(value["msg"]["ready"][0], "a");
1763 assert_eq!(value["msg"]["failed"][0]["server"], "b");
1764 assert_eq!(value["msg"]["failed"][0]["error"], "bad");
1765 assert_eq!(value["msg"]["cancelled"][0], "c");
1766 Ok(())
1767 }
1768}