1use std::collections::HashMap;
7use std::ffi::OsStr;
8use std::fmt;
9use std::path::Path;
10use std::path::PathBuf;
11use std::str::FromStr;
12use std::time::Duration;
13
14use crate::ThreadId;
15use crate::approvals::ElicitationRequestEvent;
16use crate::config_types::CollaborationMode;
17use crate::config_types::ModeKind;
18use crate::config_types::Personality;
19use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;
20use crate::config_types::WindowsSandboxLevel;
21use crate::custom_prompts::CustomPrompt;
22use crate::dynamic_tools::DynamicToolCallRequest;
23use crate::dynamic_tools::DynamicToolResponse;
24use crate::dynamic_tools::DynamicToolSpec;
25use crate::items::TurnItem;
26use crate::mcp::CallToolResult;
27use crate::mcp::RequestId;
28use crate::mcp::Resource as McpResource;
29use crate::mcp::ResourceTemplate as McpResourceTemplate;
30use crate::mcp::Tool as McpTool;
31use crate::message_history::HistoryEntry;
32use crate::models::BaseInstructions;
33use crate::models::ContentItem;
34use crate::models::ResponseItem;
35use crate::models::WebSearchAction;
36use crate::num_format::format_with_separators;
37use crate::openai_models::ReasoningEffort as ReasoningEffortConfig;
38use crate::parse_command::ParsedCommand;
39use crate::plan_tool::UpdatePlanArgs;
40use crate::request_user_input::RequestUserInputResponse;
41use crate::user_input::UserInput;
42use hanzo_utils_absolute_path::AbsolutePathBuf;
43use schemars::JsonSchema;
44use serde::Deserialize;
45use serde::Serialize;
46use serde_json::Value;
47use serde_with::serde_as;
48use strum_macros::Display;
49use tracing::error;
50use ts_rs::TS;
51
52pub use crate::approvals::ApplyPatchApprovalRequestEvent;
53pub use crate::approvals::ElicitationAction;
54pub use crate::approvals::ExecApprovalRequestEvent;
55pub use crate::approvals::ExecPolicyAmendment;
56pub use crate::approvals::NetworkApprovalContext;
57pub use crate::approvals::NetworkApprovalProtocol;
58pub use crate::mcp_protocol::InputItem;
59pub use crate::request_user_input::RequestUserInputEvent;
60
61pub const USER_INSTRUCTIONS_OPEN_TAG: &str = "<user_instructions>";
64pub const USER_INSTRUCTIONS_CLOSE_TAG: &str = "</user_instructions>";
65pub const ENVIRONMENT_CONTEXT_OPEN_TAG: &str = "<environment_context>";
66pub const ENVIRONMENT_CONTEXT_CLOSE_TAG: &str = "</environment_context>";
67pub const ENVIRONMENT_CONTEXT_DELTA_OPEN_TAG: &str = "<environment_context_delta>";
68pub const ENVIRONMENT_CONTEXT_DELTA_CLOSE_TAG: &str = "</environment_context_delta>";
69pub const BROWSER_SNAPSHOT_OPEN_TAG: &str = "<browser_snapshot>";
70pub const BROWSER_SNAPSHOT_CLOSE_TAG: &str = "</browser_snapshot>";
71pub const COLLABORATION_MODE_OPEN_TAG: &str = "<collaboration_mode>";
72pub const COLLABORATION_MODE_CLOSE_TAG: &str = "</collaboration_mode>";
73pub const USER_MESSAGE_BEGIN: &str = "## My request for Codex:";
74
75#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
77pub struct Submission {
78 pub id: String,
80 pub op: Op,
82}
83
84#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema)]
86pub struct McpServerRefreshConfig {
87 pub mcp_servers: Value,
88 pub mcp_oauth_credentials_store_mode: Value,
89}
90
91#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema)]
93#[serde(tag = "type", rename_all = "snake_case")]
94#[allow(clippy::large_enum_variant)]
95#[non_exhaustive]
96pub enum Op {
97 Interrupt,
100
101 CleanBackgroundTerminals,
103
104 UserInput {
109 items: Vec<UserInput>,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 final_output_json_schema: Option<Value>,
114 },
115
116 UserTurn {
119 items: Vec<UserInput>,
121
122 cwd: PathBuf,
125
126 approval_policy: AskForApproval,
128
129 sandbox_policy: SandboxPolicy,
131
132 model: String,
135
136 #[serde(skip_serializing_if = "Option::is_none")]
138 effort: Option<ReasoningEffortConfig>,
139
140 summary: ReasoningSummaryConfig,
142 final_output_json_schema: Option<Value>,
144
145 #[serde(skip_serializing_if = "Option::is_none")]
148 collaboration_mode: Option<CollaborationMode>,
149
150 #[serde(skip_serializing_if = "Option::is_none")]
152 personality: Option<Personality>,
153 },
154
155 OverrideTurnContext {
162 #[serde(skip_serializing_if = "Option::is_none")]
164 cwd: Option<PathBuf>,
165
166 #[serde(skip_serializing_if = "Option::is_none")]
168 approval_policy: Option<AskForApproval>,
169
170 #[serde(skip_serializing_if = "Option::is_none")]
172 sandbox_policy: Option<SandboxPolicy>,
173
174 #[serde(skip_serializing_if = "Option::is_none")]
176 windows_sandbox_level: Option<WindowsSandboxLevel>,
177
178 #[serde(skip_serializing_if = "Option::is_none")]
181 model: Option<String>,
182
183 #[serde(skip_serializing_if = "Option::is_none")]
188 effort: Option<Option<ReasoningEffortConfig>>,
189
190 #[serde(skip_serializing_if = "Option::is_none")]
192 summary: Option<ReasoningSummaryConfig>,
193
194 #[serde(skip_serializing_if = "Option::is_none")]
197 collaboration_mode: Option<CollaborationMode>,
198
199 #[serde(skip_serializing_if = "Option::is_none")]
201 personality: Option<Personality>,
202 },
203
204 ExecApproval {
206 id: String,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
210 turn_id: Option<String>,
211 decision: ReviewDecision,
213 },
214
215 PatchApproval {
217 id: String,
219 decision: ReviewDecision,
221 },
222
223 ResolveElicitation {
225 server_name: String,
227 request_id: RequestId,
229 decision: ElicitationAction,
231 },
232
233 #[serde(rename = "user_input_answer", alias = "request_user_input_response")]
235 UserInputAnswer {
236 id: String,
238 response: RequestUserInputResponse,
240 },
241
242 DynamicToolResponse {
244 id: String,
246 response: DynamicToolResponse,
248 },
249
250 AddToHistory {
255 text: String,
257 },
258
259 GetHistoryEntryRequest { offset: usize, log_id: u64 },
261
262 ListMcpTools,
265
266 RefreshMcpServers { config: McpServerRefreshConfig },
268
269 ListCustomPrompts,
271
272 ListSkills {
274 #[serde(default, skip_serializing_if = "Vec::is_empty")]
278 cwds: Vec<PathBuf>,
279
280 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
282 force_reload: bool,
283 },
284
285 ListRemoteSkills,
287
288 DownloadRemoteSkill {
290 hazelnut_id: String,
291 is_preload: bool,
292 },
293
294 Compact,
298
299 SetThreadName { name: String },
303
304 Undo,
306
307 ThreadRollback { num_turns: u32 },
312
313 Review { review_request: ReviewRequest },
315
316 Shutdown,
318
319 RunUserShellCommand {
325 command: String,
327 },
328
329 ListModels,
331}
332
333#[derive(
336 Debug,
337 Clone,
338 Copy,
339 Default,
340 PartialEq,
341 Eq,
342 Hash,
343 Serialize,
344 Deserialize,
345 Display,
346 JsonSchema,
347 TS,
348)]
349#[serde(rename_all = "kebab-case")]
350#[strum(serialize_all = "kebab-case")]
351pub enum AskForApproval {
352 #[serde(rename = "untrusted")]
356 #[strum(serialize = "untrusted")]
357 UnlessTrusted,
358
359 OnFailure,
366
367 #[default]
369 OnRequest,
370
371 Reject(RejectConfig),
376
377 Never,
380}
381
382#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)]
383pub struct RejectConfig {
384 pub sandbox_approval: bool,
386 pub rules: bool,
388 pub mcp_elicitations: bool,
390}
391
392impl RejectConfig {
393 pub const fn rejects_sandbox_approval(self) -> bool {
394 self.sandbox_approval
395 }
396
397 pub const fn rejects_rules_approval(self) -> bool {
398 self.rules
399 }
400
401 pub const fn rejects_mcp_elicitations(self) -> bool {
402 self.mcp_elicitations
403 }
404}
405
406#[derive(
408 Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, Default, JsonSchema, TS,
409)]
410#[serde(rename_all = "kebab-case")]
411#[strum(serialize_all = "kebab-case")]
412pub enum NetworkAccess {
413 #[default]
414 Restricted,
415 Enabled,
416}
417
418impl NetworkAccess {
419 pub fn is_enabled(self) -> bool {
420 matches!(self, NetworkAccess::Enabled)
421 }
422}
423
424#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, JsonSchema, TS)]
426#[strum(serialize_all = "kebab-case")]
427#[serde(tag = "type", rename_all = "kebab-case")]
428pub enum SandboxPolicy {
429 #[serde(rename = "danger-full-access")]
431 DangerFullAccess,
432
433 #[serde(rename = "read-only")]
435 ReadOnly,
436
437 #[serde(rename = "external-sandbox")]
440 ExternalSandbox {
441 #[serde(default)]
443 network_access: NetworkAccess,
444 },
445
446 #[serde(rename = "workspace-write")]
449 WorkspaceWrite {
450 #[serde(default, skip_serializing_if = "Vec::is_empty")]
453 writable_roots: Vec<AbsolutePathBuf>,
454
455 #[serde(default)]
458 network_access: bool,
459
460 #[serde(default)]
464 exclude_tmpdir_env_var: bool,
465
466 #[serde(default)]
469 exclude_slash_tmp: bool,
470
471 #[serde(default)]
473 allow_git_writes: bool,
474 },
475}
476
477#[derive(Debug, Clone, PartialEq, Eq, JsonSchema)]
483pub struct WritableRoot {
484 pub root: AbsolutePathBuf,
485
486 pub read_only_subpaths: Vec<AbsolutePathBuf>,
488}
489
490impl WritableRoot {
491 pub fn is_path_writable(&self, path: &Path) -> bool {
492 if !path.starts_with(&self.root) {
494 return false;
495 }
496
497 for subpath in &self.read_only_subpaths {
499 if path.starts_with(subpath) {
500 return false;
501 }
502 }
503
504 true
505 }
506}
507
508impl FromStr for SandboxPolicy {
509 type Err = serde_json::Error;
510
511 fn from_str(s: &str) -> Result<Self, Self::Err> {
512 serde_json::from_str(s)
513 }
514}
515
516impl SandboxPolicy {
517 pub fn new_read_only_policy() -> Self {
519 SandboxPolicy::ReadOnly
520 }
521
522 pub fn new_workspace_write_policy() -> Self {
526 SandboxPolicy::WorkspaceWrite {
527 writable_roots: vec![],
528 network_access: false,
529 exclude_tmpdir_env_var: false,
530 exclude_slash_tmp: false,
531 allow_git_writes: false,
532 }
533 }
534
535 pub fn has_full_disk_read_access(&self) -> bool {
537 true
538 }
539
540 pub fn has_full_disk_write_access(&self) -> bool {
541 match self {
542 SandboxPolicy::DangerFullAccess => true,
543 SandboxPolicy::ExternalSandbox { .. } => true,
544 SandboxPolicy::ReadOnly => false,
545 SandboxPolicy::WorkspaceWrite { .. } => false,
546 }
547 }
548
549 pub fn has_full_network_access(&self) -> bool {
550 match self {
551 SandboxPolicy::DangerFullAccess => true,
552 SandboxPolicy::ExternalSandbox { network_access } => network_access.is_enabled(),
553 SandboxPolicy::ReadOnly => false,
554 SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access,
555 }
556 }
557
558 pub fn get_writable_roots_with_cwd(&self, cwd: &Path) -> Vec<WritableRoot> {
562 match self {
563 SandboxPolicy::DangerFullAccess => Vec::new(),
564 SandboxPolicy::ExternalSandbox { .. } => Vec::new(),
565 SandboxPolicy::ReadOnly => Vec::new(),
566 SandboxPolicy::WorkspaceWrite {
567 writable_roots,
568 exclude_tmpdir_env_var,
569 exclude_slash_tmp,
570 network_access: _,
571 allow_git_writes: _,
572 } => {
573 let mut roots: Vec<AbsolutePathBuf> = writable_roots.clone();
575
576 let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd);
580 match cwd_absolute {
581 Ok(cwd) => {
582 roots.push(cwd);
583 }
584 Err(e) => {
585 error!(
586 "Ignoring invalid cwd {:?} for sandbox writable root: {}",
587 cwd, e
588 );
589 }
590 }
591
592 if cfg!(unix) && !exclude_slash_tmp {
594 #[allow(clippy::expect_used)]
595 let slash_tmp =
596 AbsolutePathBuf::from_absolute_path("/tmp").expect("/tmp is absolute");
597 if slash_tmp.as_path().is_dir() {
598 roots.push(slash_tmp);
599 }
600 }
601
602 if !exclude_tmpdir_env_var
611 && let Some(tmpdir) = std::env::var_os("TMPDIR")
612 && !tmpdir.is_empty()
613 {
614 match AbsolutePathBuf::from_absolute_path(PathBuf::from(&tmpdir)) {
615 Ok(tmpdir_path) => {
616 roots.push(tmpdir_path);
617 }
618 Err(e) => {
619 error!(
620 "Ignoring invalid TMPDIR value {tmpdir:?} for sandbox writable root: {e}",
621 );
622 }
623 }
624 }
625
626 roots
628 .into_iter()
629 .map(|writable_root| {
630 let mut subpaths: Vec<AbsolutePathBuf> = Vec::new();
631 #[allow(clippy::expect_used)]
632 let top_level_git = writable_root
633 .join(".git")
634 .expect(".git is a valid relative path");
635 let top_level_git_is_file = top_level_git.as_path().is_file();
639 let top_level_git_is_dir = top_level_git.as_path().is_dir();
640 if top_level_git_is_dir || top_level_git_is_file {
641 if top_level_git_is_file
642 && is_git_pointer_file(&top_level_git)
643 && let Some(gitdir) = resolve_gitdir_from_file(&top_level_git)
644 && !subpaths
645 .iter()
646 .any(|subpath| subpath.as_path() == gitdir.as_path())
647 {
648 subpaths.push(gitdir);
649 }
650 subpaths.push(top_level_git);
651 }
652
653 for subdir in &[".agents", ".codex"] {
656 #[allow(clippy::expect_used)]
657 let top_level_codex =
658 writable_root.join(subdir).expect("valid relative path");
659 if top_level_codex.as_path().is_dir() {
660 subpaths.push(top_level_codex);
661 }
662 }
663
664 WritableRoot {
665 root: writable_root,
666 read_only_subpaths: subpaths,
667 }
668 })
669 .collect()
670 }
671 }
672 }
673}
674
675fn is_git_pointer_file(path: &AbsolutePathBuf) -> bool {
676 path.as_path().is_file() && path.as_path().file_name() == Some(OsStr::new(".git"))
677}
678
679fn resolve_gitdir_from_file(dot_git: &AbsolutePathBuf) -> Option<AbsolutePathBuf> {
680 let contents = match std::fs::read_to_string(dot_git.as_path()) {
681 Ok(contents) => contents,
682 Err(err) => {
683 error!(
684 "Failed to read {path} for gitdir pointer: {err}",
685 path = dot_git.as_path().display()
686 );
687 return None;
688 }
689 };
690
691 let trimmed = contents.trim();
692 let (_, gitdir_raw) = match trimmed.split_once(':') {
693 Some(parts) => parts,
694 None => {
695 error!(
696 "Expected {path} to contain a gitdir pointer, but it did not match `gitdir: <path>`.",
697 path = dot_git.as_path().display()
698 );
699 return None;
700 }
701 };
702 let gitdir_raw = gitdir_raw.trim();
703 if gitdir_raw.is_empty() {
704 error!(
705 "Expected {path} to contain a gitdir pointer, but it was empty.",
706 path = dot_git.as_path().display()
707 );
708 return None;
709 }
710 let base = match dot_git.as_path().parent() {
711 Some(base) => base,
712 None => {
713 error!(
714 "Unable to resolve parent directory for {path}.",
715 path = dot_git.as_path().display()
716 );
717 return None;
718 }
719 };
720 let gitdir_path = match AbsolutePathBuf::resolve_path_against_base(gitdir_raw, base) {
721 Ok(path) => path,
722 Err(err) => {
723 error!(
724 "Failed to resolve gitdir path {gitdir_raw} from {path}: {err}",
725 path = dot_git.as_path().display()
726 );
727 return None;
728 }
729 };
730 if !gitdir_path.as_path().exists() {
731 error!(
732 "Resolved gitdir path {path} does not exist.",
733 path = gitdir_path.as_path().display()
734 );
735 return None;
736 }
737 Some(gitdir_path)
738}
739
740#[derive(Debug, Clone, Deserialize, Serialize)]
742pub struct Event {
743 pub id: String,
745 pub msg: EventMsg,
747}
748
749#[derive(Debug, Clone, Deserialize, Serialize, Display, JsonSchema, TS)]
752#[serde(tag = "type", rename_all = "snake_case")]
753#[ts(tag = "type")]
754#[strum(serialize_all = "snake_case")]
755pub enum EventMsg {
756 Error(ErrorEvent),
758
759 Warning(WarningEvent),
762
763 ContextCompacted(ContextCompactedEvent),
765
766 ThreadRolledBack(ThreadRolledBackEvent),
768
769 #[serde(rename = "task_started", alias = "turn_started")]
772 TurnStarted(TurnStartedEvent),
773
774 #[serde(rename = "task_complete", alias = "turn_complete")]
777 TurnComplete(TurnCompleteEvent),
778
779 TokenCount(TokenCountEvent),
782
783 AutoContextCheck(AutoContextCheckEvent),
785
786 AgentMessage(AgentMessageEvent),
788
789 UserMessage(UserMessageEvent),
791
792 AgentMessageDelta(AgentMessageDeltaEvent),
794
795 AgentReasoning(AgentReasoningEvent),
797
798 AgentReasoningDelta(AgentReasoningDeltaEvent),
800
801 AgentReasoningRawContent(AgentReasoningRawContentEvent),
803
804 AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent),
806 AgentReasoningSectionBreak(AgentReasoningSectionBreakEvent),
808
809 SessionConfigured(SessionConfiguredEvent),
811
812 ThreadNameUpdated(ThreadNameUpdatedEvent),
814
815 McpStartupUpdate(McpStartupUpdateEvent),
817
818 McpStartupComplete(McpStartupCompleteEvent),
820
821 McpToolCallBegin(McpToolCallBeginEvent),
822
823 McpToolCallEnd(McpToolCallEndEvent),
824
825 WebSearchBegin(WebSearchBeginEvent),
826
827 WebSearchEnd(WebSearchEndEvent),
828
829 ExecCommandBegin(ExecCommandBeginEvent),
831
832 ExecCommandOutputDelta(ExecCommandOutputDeltaEvent),
834
835 TerminalInteraction(TerminalInteractionEvent),
837
838 ExecCommandEnd(ExecCommandEndEvent),
839
840 ViewImageToolCall(ViewImageToolCallEvent),
842
843 ExecApprovalRequest(ExecApprovalRequestEvent),
844
845 RequestUserInput(RequestUserInputEvent),
846
847 DynamicToolCallRequest(DynamicToolCallRequest),
848
849 ElicitationRequest(ElicitationRequestEvent),
850
851 ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent),
852
853 DeprecationNotice(DeprecationNoticeEvent),
856
857 BackgroundEvent(BackgroundEventEvent),
858
859 UndoStarted(UndoStartedEvent),
860
861 UndoCompleted(UndoCompletedEvent),
862
863 StreamError(StreamErrorEvent),
866
867 PatchApplyBegin(PatchApplyBeginEvent),
870
871 PatchApplyEnd(PatchApplyEndEvent),
873
874 TurnDiff(TurnDiffEvent),
875
876 GetHistoryEntryResponse(GetHistoryEntryResponseEvent),
878
879 McpListToolsResponse(McpListToolsResponseEvent),
881
882 ListCustomPromptsResponse(ListCustomPromptsResponseEvent),
884
885 ListSkillsResponse(ListSkillsResponseEvent),
887
888 ListRemoteSkillsResponse(ListRemoteSkillsResponseEvent),
890
891 RemoteSkillDownloaded(RemoteSkillDownloadedEvent),
893
894 SkillsUpdateAvailable,
896
897 PlanUpdate(UpdatePlanArgs),
898
899 TurnAborted(TurnAbortedEvent),
900
901 ShutdownComplete,
903
904 EnteredReviewMode(ReviewRequest),
906
907 ExitedReviewMode(ExitedReviewModeEvent),
909
910 RawResponseItem(RawResponseItemEvent),
911
912 ItemStarted(ItemStartedEvent),
913 ItemCompleted(ItemCompletedEvent),
914
915 AgentMessageContentDelta(AgentMessageContentDeltaEvent),
916 PlanDelta(PlanDeltaEvent),
917 ReasoningContentDelta(ReasoningContentDeltaEvent),
918 ReasoningRawContentDelta(ReasoningRawContentDeltaEvent),
919
920 CollabAgentSpawnBegin(CollabAgentSpawnBeginEvent),
922 CollabAgentSpawnEnd(CollabAgentSpawnEndEvent),
924 CollabAgentInteractionBegin(CollabAgentInteractionBeginEvent),
926 CollabAgentInteractionEnd(CollabAgentInteractionEndEvent),
928 CollabWaitingBegin(CollabWaitingBeginEvent),
930 CollabWaitingEnd(CollabWaitingEndEvent),
932 CollabCloseBegin(CollabCloseBeginEvent),
934 CollabCloseEnd(CollabCloseEndEvent),
936 CollabResumeBegin(CollabResumeBeginEvent),
938 CollabResumeEnd(CollabResumeEndEvent),
940}
941
942impl From<CollabAgentSpawnBeginEvent> for EventMsg {
943 fn from(event: CollabAgentSpawnBeginEvent) -> Self {
944 EventMsg::CollabAgentSpawnBegin(event)
945 }
946}
947
948impl From<CollabAgentSpawnEndEvent> for EventMsg {
949 fn from(event: CollabAgentSpawnEndEvent) -> Self {
950 EventMsg::CollabAgentSpawnEnd(event)
951 }
952}
953
954impl From<CollabAgentInteractionBeginEvent> for EventMsg {
955 fn from(event: CollabAgentInteractionBeginEvent) -> Self {
956 EventMsg::CollabAgentInteractionBegin(event)
957 }
958}
959
960impl From<CollabAgentInteractionEndEvent> for EventMsg {
961 fn from(event: CollabAgentInteractionEndEvent) -> Self {
962 EventMsg::CollabAgentInteractionEnd(event)
963 }
964}
965
966impl From<CollabWaitingBeginEvent> for EventMsg {
967 fn from(event: CollabWaitingBeginEvent) -> Self {
968 EventMsg::CollabWaitingBegin(event)
969 }
970}
971
972impl From<CollabWaitingEndEvent> for EventMsg {
973 fn from(event: CollabWaitingEndEvent) -> Self {
974 EventMsg::CollabWaitingEnd(event)
975 }
976}
977
978impl From<CollabCloseBeginEvent> for EventMsg {
979 fn from(event: CollabCloseBeginEvent) -> Self {
980 EventMsg::CollabCloseBegin(event)
981 }
982}
983
984impl From<CollabCloseEndEvent> for EventMsg {
985 fn from(event: CollabCloseEndEvent) -> Self {
986 EventMsg::CollabCloseEnd(event)
987 }
988}
989
990impl From<CollabResumeBeginEvent> for EventMsg {
991 fn from(event: CollabResumeBeginEvent) -> Self {
992 EventMsg::CollabResumeBegin(event)
993 }
994}
995
996impl From<CollabResumeEndEvent> for EventMsg {
997 fn from(event: CollabResumeEndEvent) -> Self {
998 EventMsg::CollabResumeEnd(event)
999 }
1000}
1001
1002#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS, Default)]
1004#[serde(rename_all = "snake_case")]
1005#[ts(rename_all = "snake_case")]
1006pub enum AgentStatus {
1007 #[default]
1009 PendingInit,
1010 Running,
1012 Completed(Option<String>),
1014 Errored(String),
1016 Shutdown,
1018 NotFound,
1020}
1021
1022#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
1024#[serde(rename_all = "snake_case")]
1025#[ts(rename_all = "snake_case")]
1026pub enum CodexErrorInfo {
1027 ContextWindowExceeded,
1028 UsageLimitExceeded,
1029 ModelCap {
1030 model: String,
1031 reset_after_seconds: Option<u64>,
1032 },
1033 HttpConnectionFailed {
1034 http_status_code: Option<u16>,
1035 },
1036 ResponseStreamConnectionFailed {
1038 http_status_code: Option<u16>,
1039 },
1040 InternalServerError,
1041 Unauthorized,
1042 BadRequest,
1043 SandboxError,
1044 ResponseStreamDisconnected {
1046 http_status_code: Option<u16>,
1047 },
1048 ResponseTooManyFailedAttempts {
1050 http_status_code: Option<u16>,
1051 },
1052 ThreadRollbackFailed,
1053 Other,
1054}
1055
1056#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1057pub struct RawResponseItemEvent {
1058 pub item: ResponseItem,
1059}
1060
1061#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1062pub struct ItemStartedEvent {
1063 pub thread_id: ThreadId,
1064 pub turn_id: String,
1065 pub item: TurnItem,
1066}
1067
1068impl HasLegacyEvent for ItemStartedEvent {
1069 fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
1070 match &self.item {
1071 TurnItem::WebSearch(item) => vec![EventMsg::WebSearchBegin(WebSearchBeginEvent {
1072 call_id: item.id.clone(),
1073 })],
1074 _ => Vec::new(),
1075 }
1076 }
1077}
1078
1079#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1080pub struct ItemCompletedEvent {
1081 pub thread_id: ThreadId,
1082 pub turn_id: String,
1083 pub item: TurnItem,
1084}
1085
1086pub trait HasLegacyEvent {
1087 fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg>;
1088}
1089
1090impl HasLegacyEvent for ItemCompletedEvent {
1091 fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg> {
1092 self.item.as_legacy_events(show_raw_agent_reasoning)
1093 }
1094}
1095
1096#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1097pub struct AgentMessageContentDeltaEvent {
1098 pub thread_id: String,
1099 pub turn_id: String,
1100 pub item_id: String,
1101 pub delta: String,
1102}
1103
1104impl HasLegacyEvent for AgentMessageContentDeltaEvent {
1105 fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
1106 vec![EventMsg::AgentMessageDelta(AgentMessageDeltaEvent {
1107 delta: self.delta.clone(),
1108 })]
1109 }
1110}
1111
1112#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1113pub struct PlanDeltaEvent {
1114 pub thread_id: String,
1115 pub turn_id: String,
1116 pub item_id: String,
1117 pub delta: String,
1118}
1119
1120#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1121pub struct ReasoningContentDeltaEvent {
1122 pub thread_id: String,
1123 pub turn_id: String,
1124 pub item_id: String,
1125 pub delta: String,
1126 #[serde(default)]
1128 pub summary_index: i64,
1129}
1130
1131impl HasLegacyEvent for ReasoningContentDeltaEvent {
1132 fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
1133 vec![EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent {
1134 delta: self.delta.clone(),
1135 })]
1136 }
1137}
1138
1139#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
1140pub struct ReasoningRawContentDeltaEvent {
1141 pub thread_id: String,
1142 pub turn_id: String,
1143 pub item_id: String,
1144 pub delta: String,
1145 #[serde(default)]
1147 pub content_index: i64,
1148}
1149
1150impl HasLegacyEvent for ReasoningRawContentDeltaEvent {
1151 fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
1152 vec![EventMsg::AgentReasoningRawContentDelta(
1153 AgentReasoningRawContentDeltaEvent {
1154 delta: self.delta.clone(),
1155 },
1156 )]
1157 }
1158}
1159
1160impl HasLegacyEvent for EventMsg {
1161 fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg> {
1162 match self {
1163 EventMsg::ItemStarted(event) => event.as_legacy_events(show_raw_agent_reasoning),
1164 EventMsg::ItemCompleted(event) => event.as_legacy_events(show_raw_agent_reasoning),
1165 EventMsg::AgentMessageContentDelta(event) => {
1166 event.as_legacy_events(show_raw_agent_reasoning)
1167 }
1168 EventMsg::ReasoningContentDelta(event) => {
1169 event.as_legacy_events(show_raw_agent_reasoning)
1170 }
1171 EventMsg::ReasoningRawContentDelta(event) => {
1172 event.as_legacy_events(show_raw_agent_reasoning)
1173 }
1174 _ => Vec::new(),
1175 }
1176 }
1177}
1178
1179#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1180pub struct ExitedReviewModeEvent {
1181 pub review_output: Option<ReviewOutputEvent>,
1182 #[ts(optional)]
1183 #[serde(default, skip_serializing_if = "Option::is_none")]
1184 pub snapshot: Option<ReviewSnapshotInfo>,
1185}
1186
1187#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1190pub struct ErrorEvent {
1191 pub message: String,
1192 #[serde(default)]
1193 pub codex_error_info: Option<CodexErrorInfo>,
1194}
1195
1196#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1197pub struct WarningEvent {
1198 pub message: String,
1199}
1200
1201#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1202pub struct ContextCompactedEvent;
1203
1204#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1205pub struct TurnCompleteEvent {
1206 pub last_agent_message: Option<String>,
1207}
1208
1209#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1210pub struct TurnStartedEvent {
1211 pub model_context_window: Option<i64>,
1213 #[serde(default)]
1214 pub collaboration_mode_kind: ModeKind,
1215}
1216
1217#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1218#[serde(rename_all = "snake_case")]
1219pub enum AutoContextPhase {
1220 Checking,
1221 Compacting,
1222}
1223
1224#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1225pub struct AutoContextCheckEvent {
1226 pub phase: Option<AutoContextPhase>,
1227}
1228
1229#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, TS)]
1230pub struct TokenUsage {
1231 #[ts(type = "number")]
1232 pub input_tokens: i64,
1233 #[ts(type = "number")]
1234 pub cached_input_tokens: i64,
1235 #[ts(type = "number")]
1236 pub output_tokens: i64,
1237 #[ts(type = "number")]
1238 pub reasoning_output_tokens: i64,
1239 #[ts(type = "number")]
1240 pub total_tokens: i64,
1241}
1242
1243#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1244pub struct TokenUsageInfo {
1245 pub total_token_usage: TokenUsage,
1246 pub last_token_usage: TokenUsage,
1247 #[ts(optional)]
1248 #[serde(default, skip_serializing_if = "Option::is_none")]
1249 pub requested_model: Option<String>,
1250 #[ts(optional)]
1251 #[serde(default, skip_serializing_if = "Option::is_none")]
1252 pub latest_response_model: Option<String>,
1253 #[ts(type = "number | null")]
1255 pub model_context_window: Option<i64>,
1256}
1257
1258impl TokenUsageInfo {
1259 pub fn new_or_append(
1260 info: &Option<TokenUsageInfo>,
1261 last: &Option<TokenUsage>,
1262 model_context_window: Option<i64>,
1263 ) -> Option<Self> {
1264 if info.is_none() && last.is_none() {
1265 return None;
1266 }
1267
1268 let mut info = match info {
1269 Some(info) => info.clone(),
1270 None => Self {
1271 total_token_usage: TokenUsage::default(),
1272 last_token_usage: TokenUsage::default(),
1273 requested_model: None,
1274 latest_response_model: None,
1275 model_context_window,
1276 },
1277 };
1278 if let Some(last) = last {
1279 info.append_last_usage(last);
1280 }
1281 Some(info)
1282 }
1283
1284 pub fn append_last_usage(&mut self, last: &TokenUsage) {
1285 self.total_token_usage.add_assign(last);
1286 self.last_token_usage = last.clone();
1287 }
1288
1289 pub fn fill_to_context_window(&mut self, context_window: i64) {
1290 let previous_total = self.total_token_usage.total_tokens;
1291 let delta = (context_window - previous_total).max(0);
1292
1293 self.model_context_window = Some(context_window);
1294 self.total_token_usage = TokenUsage {
1295 total_tokens: context_window,
1296 ..TokenUsage::default()
1297 };
1298 self.last_token_usage = TokenUsage {
1299 total_tokens: delta,
1300 ..TokenUsage::default()
1301 };
1302 }
1303
1304 pub fn full_context_window(context_window: i64) -> Self {
1305 let mut info = Self {
1306 total_token_usage: TokenUsage::default(),
1307 last_token_usage: TokenUsage::default(),
1308 requested_model: None,
1309 latest_response_model: None,
1310 model_context_window: Some(context_window),
1311 };
1312 info.fill_to_context_window(context_window);
1313 info
1314 }
1315}
1316
1317#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1318pub struct TokenCountEvent {
1319 pub info: Option<TokenUsageInfo>,
1320 pub rate_limits: Option<RateLimitSnapshot>,
1321}
1322
1323#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
1324pub struct RateLimitSnapshot {
1325 #[ts(type = "string | null")]
1326 #[serde(default)]
1327 pub limit_id: Option<String>,
1328 #[ts(type = "string | null")]
1329 #[serde(default)]
1330 pub limit_name: Option<String>,
1331 pub primary: Option<RateLimitWindow>,
1332 pub secondary: Option<RateLimitWindow>,
1333 pub credits: Option<CreditsSnapshot>,
1334 pub plan_type: Option<crate::account::PlanType>,
1335}
1336
1337#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
1338pub struct RateLimitWindow {
1339 pub used_percent: f64,
1341 #[ts(type = "number | null")]
1343 pub window_minutes: Option<u64>,
1344 #[ts(optional)]
1346 #[ts(type = "number")]
1347 #[serde(default, skip_serializing_if = "Option::is_none")]
1348 pub resets_in_seconds: Option<u64>,
1349 #[ts(optional)]
1351 #[ts(type = "number")]
1352 #[serde(default, skip_serializing_if = "Option::is_none")]
1353 pub resets_at: Option<i64>,
1354}
1355
1356#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
1357pub struct CreditsSnapshot {
1358 pub has_credits: bool,
1359 pub unlimited: bool,
1360 pub balance: Option<String>,
1361}
1362
1363const BASELINE_TOKENS: i64 = 12000;
1365
1366impl TokenUsage {
1367 pub fn is_zero(&self) -> bool {
1368 self.total_tokens == 0
1369 }
1370
1371 pub fn cached_input(&self) -> i64 {
1372 self.cached_input_tokens.max(0)
1373 }
1374
1375 pub fn non_cached_input(&self) -> i64 {
1376 (self.input_tokens - self.cached_input()).max(0)
1377 }
1378
1379 pub fn blended_total(&self) -> i64 {
1381 (self.non_cached_input() + self.output_tokens.max(0)).max(0)
1382 }
1383
1384 pub fn tokens_in_context_window(&self) -> i64 {
1385 self.total_tokens
1386 }
1387
1388 pub fn percent_of_context_window_remaining(&self, context_window: i64) -> i64 {
1399 if context_window <= BASELINE_TOKENS {
1400 return 0;
1401 }
1402
1403 let effective_window = context_window - BASELINE_TOKENS;
1404 let used = (self.tokens_in_context_window() - BASELINE_TOKENS).max(0);
1405 let remaining = (effective_window - used).max(0);
1406 ((remaining as f64 / effective_window as f64) * 100.0)
1407 .clamp(0.0, 100.0)
1408 .round() as i64
1409 }
1410
1411 pub fn add_assign(&mut self, other: &TokenUsage) {
1413 self.input_tokens += other.input_tokens;
1414 self.cached_input_tokens += other.cached_input_tokens;
1415 self.output_tokens += other.output_tokens;
1416 self.reasoning_output_tokens += other.reasoning_output_tokens;
1417 self.total_tokens += other.total_tokens;
1418 }
1419}
1420
1421#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1422pub struct FinalOutput {
1423 pub token_usage: TokenUsage,
1424}
1425
1426impl From<TokenUsage> for FinalOutput {
1427 fn from(token_usage: TokenUsage) -> Self {
1428 Self { token_usage }
1429 }
1430}
1431
1432impl fmt::Display for FinalOutput {
1433 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1434 let token_usage = &self.token_usage;
1435
1436 write!(
1437 f,
1438 "Token usage: total={} input={}{} output={}{}",
1439 format_with_separators(token_usage.blended_total()),
1440 format_with_separators(token_usage.non_cached_input()),
1441 if token_usage.cached_input() > 0 {
1442 format!(
1443 " (+ {} cached)",
1444 format_with_separators(token_usage.cached_input())
1445 )
1446 } else {
1447 String::new()
1448 },
1449 format_with_separators(token_usage.output_tokens),
1450 if token_usage.reasoning_output_tokens > 0 {
1451 format!(
1452 " (reasoning {})",
1453 format_with_separators(token_usage.reasoning_output_tokens)
1454 )
1455 } else {
1456 String::new()
1457 }
1458 )
1459 }
1460}
1461
1462#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1463pub struct AgentMessageEvent {
1464 pub message: String,
1465}
1466
1467#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1468pub struct UserMessageEvent {
1469 pub message: String,
1470 #[serde(skip_serializing_if = "Option::is_none")]
1474 pub images: Option<Vec<String>>,
1475 #[serde(default)]
1479 pub local_images: Vec<std::path::PathBuf>,
1480 #[serde(default)]
1482 pub text_elements: Vec<crate::user_input::TextElement>,
1483}
1484
1485#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema, TS)]
1486#[serde(rename_all = "snake_case")]
1487pub enum InputMessageKind {
1488 User,
1489 Assistant,
1490 UserInstructions,
1491 EnvironmentContext,
1492 BrowserSnapshot,
1493 Other,
1494}
1495
1496impl From<(&str, &str)> for InputMessageKind {
1497 fn from((role, text): (&str, &str)) -> Self {
1498 if role == "user" {
1499 if text.contains(USER_INSTRUCTIONS_OPEN_TAG) {
1500 return Self::UserInstructions;
1501 }
1502 if text.contains(ENVIRONMENT_CONTEXT_OPEN_TAG)
1503 || text.contains(ENVIRONMENT_CONTEXT_DELTA_OPEN_TAG)
1504 {
1505 return Self::EnvironmentContext;
1506 }
1507 if text.contains(BROWSER_SNAPSHOT_OPEN_TAG) {
1508 return Self::BrowserSnapshot;
1509 }
1510 return Self::User;
1511 }
1512 if role == "assistant" {
1513 return Self::Assistant;
1514 }
1515 Self::Other
1516 }
1517}
1518
1519#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1520pub struct AgentMessageDeltaEvent {
1521 pub delta: String,
1522}
1523
1524#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1525pub struct AgentReasoningEvent {
1526 pub text: String,
1527}
1528
1529#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1530pub struct AgentReasoningRawContentEvent {
1531 pub text: String,
1532}
1533
1534#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1535pub struct AgentReasoningRawContentDeltaEvent {
1536 pub delta: String,
1537}
1538
1539#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1540pub struct AgentReasoningSectionBreakEvent {
1541 #[serde(default)]
1543 pub item_id: String,
1544 #[serde(default)]
1545 pub summary_index: i64,
1546}
1547
1548#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1549pub struct AgentReasoningDeltaEvent {
1550 pub delta: String,
1551}
1552
1553#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1554pub struct CompactionCheckpointWarningEvent {
1555 pub message: String,
1556}
1557
1558#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
1559pub struct McpInvocation {
1560 pub server: String,
1562 pub tool: String,
1564 pub arguments: Option<serde_json::Value>,
1566}
1567
1568#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
1569pub struct McpToolCallBeginEvent {
1570 pub call_id: String,
1572 pub invocation: McpInvocation,
1573}
1574
1575#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
1576pub struct McpToolCallEndEvent {
1577 pub call_id: String,
1579 pub invocation: McpInvocation,
1580 #[ts(type = "string")]
1581 pub duration: Duration,
1582 pub result: Result<CallToolResult, String>,
1584}
1585
1586impl McpToolCallEndEvent {
1587 pub fn is_success(&self) -> bool {
1588 match &self.result {
1589 Ok(result) => !result.is_error.unwrap_or(false),
1590 Err(_) => false,
1591 }
1592 }
1593}
1594
1595#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1596pub struct WebSearchBeginEvent {
1597 pub call_id: String,
1598}
1599
1600#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1601pub struct WebSearchEndEvent {
1602 pub call_id: String,
1603 pub query: String,
1604 pub action: WebSearchAction,
1605}
1606
1607#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1611pub struct ConversationPathResponseEvent {
1612 pub conversation_id: ThreadId,
1613 pub path: PathBuf,
1614}
1615
1616#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1617pub struct ResumedHistory {
1618 pub conversation_id: ThreadId,
1619 pub history: Vec<RolloutItem>,
1620 pub rollout_path: PathBuf,
1621}
1622
1623#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1624pub enum InitialHistory {
1625 New,
1626 Resumed(ResumedHistory),
1627 Forked(Vec<RolloutItem>),
1628}
1629
1630impl InitialHistory {
1631 pub fn forked_from_id(&self) -> Option<ThreadId> {
1632 match self {
1633 InitialHistory::New => None,
1634 InitialHistory::Resumed(resumed) => {
1635 resumed.history.iter().find_map(|item| match item {
1636 RolloutItem::SessionMeta(meta_line) => meta_line.meta.forked_from_id,
1637 _ => None,
1638 })
1639 }
1640 InitialHistory::Forked(items) => items.iter().find_map(|item| match item {
1641 RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.id),
1642 _ => None,
1643 }),
1644 }
1645 }
1646
1647 pub fn session_cwd(&self) -> Option<PathBuf> {
1648 match self {
1649 InitialHistory::New => None,
1650 InitialHistory::Resumed(resumed) => session_cwd_from_items(&resumed.history),
1651 InitialHistory::Forked(items) => session_cwd_from_items(items),
1652 }
1653 }
1654
1655 pub fn get_rollout_items(&self) -> Vec<RolloutItem> {
1656 match self {
1657 InitialHistory::New => Vec::new(),
1658 InitialHistory::Resumed(resumed) => resumed.history.clone(),
1659 InitialHistory::Forked(items) => items.clone(),
1660 }
1661 }
1662
1663 pub fn get_event_msgs(&self) -> Option<Vec<EventMsg>> {
1664 match self {
1665 InitialHistory::New => None,
1666 InitialHistory::Resumed(resumed) => Some(
1667 resumed
1668 .history
1669 .iter()
1670 .filter_map(|ri| match ri {
1671 RolloutItem::Event(ev) => Some(ev.msg.clone()),
1672 RolloutItem::EventMsg(ev) => Some(ev.clone()),
1673 _ => None,
1674 })
1675 .collect(),
1676 ),
1677 InitialHistory::Forked(items) => Some(
1678 items
1679 .iter()
1680 .filter_map(|ri| match ri {
1681 RolloutItem::Event(ev) => Some(ev.msg.clone()),
1682 RolloutItem::EventMsg(ev) => Some(ev.clone()),
1683 _ => None,
1684 })
1685 .collect(),
1686 ),
1687 }
1688 }
1689
1690 pub fn get_base_instructions(&self) -> Option<BaseInstructions> {
1691 match self {
1693 InitialHistory::New => None,
1694 InitialHistory::Resumed(resumed) => {
1695 resumed.history.iter().find_map(|item| match item {
1696 RolloutItem::SessionMeta(meta_line) => meta_line.meta.base_instructions.clone(),
1697 _ => None,
1698 })
1699 }
1700 InitialHistory::Forked(items) => items.iter().find_map(|item| match item {
1701 RolloutItem::SessionMeta(meta_line) => meta_line.meta.base_instructions.clone(),
1702 _ => None,
1703 }),
1704 }
1705 }
1706
1707 pub fn get_dynamic_tools(&self) -> Option<Vec<DynamicToolSpec>> {
1708 match self {
1709 InitialHistory::New => None,
1710 InitialHistory::Resumed(resumed) => {
1711 resumed.history.iter().find_map(|item| match item {
1712 RolloutItem::SessionMeta(meta_line) => meta_line.meta.dynamic_tools.clone(),
1713 _ => None,
1714 })
1715 }
1716 InitialHistory::Forked(items) => items.iter().find_map(|item| match item {
1717 RolloutItem::SessionMeta(meta_line) => meta_line.meta.dynamic_tools.clone(),
1718 _ => None,
1719 }),
1720 }
1721 }
1722}
1723
1724fn session_cwd_from_items(items: &[RolloutItem]) -> Option<PathBuf> {
1725 items.iter().find_map(|item| match item {
1726 RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.cwd.clone()),
1727 _ => None,
1728 })
1729}
1730
1731#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
1732#[serde(rename_all = "lowercase")]
1733#[ts(rename_all = "lowercase")]
1734pub enum SessionSource {
1735 Cli,
1736 #[default]
1737 VSCode,
1738 Exec,
1739 Mcp,
1740 SubAgent(SubAgentSource),
1741 #[serde(other)]
1742 Unknown,
1743}
1744
1745#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
1746#[serde(rename_all = "snake_case")]
1747#[ts(rename_all = "snake_case")]
1748pub enum SubAgentSource {
1749 Review,
1750 Compact,
1751 ThreadSpawn {
1752 parent_thread_id: ThreadId,
1753 depth: i32,
1754 },
1755 MemoryConsolidation,
1756 Other(String),
1757}
1758
1759impl fmt::Display for SessionSource {
1760 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1761 match self {
1762 SessionSource::Cli => f.write_str("cli"),
1763 SessionSource::VSCode => f.write_str("vscode"),
1764 SessionSource::Exec => f.write_str("exec"),
1765 SessionSource::Mcp => f.write_str("mcp"),
1766 SessionSource::SubAgent(sub_source) => write!(f, "subagent_{sub_source}"),
1767 SessionSource::Unknown => f.write_str("unknown"),
1768 }
1769 }
1770}
1771
1772impl fmt::Display for SubAgentSource {
1773 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1774 match self {
1775 SubAgentSource::Review => f.write_str("review"),
1776 SubAgentSource::Compact => f.write_str("compact"),
1777 SubAgentSource::MemoryConsolidation => f.write_str("memory_consolidation"),
1778 SubAgentSource::ThreadSpawn {
1779 parent_thread_id,
1780 depth,
1781 } => {
1782 write!(f, "thread_spawn_{parent_thread_id}_d{depth}")
1783 }
1784 SubAgentSource::Other(other) => f.write_str(other),
1785 }
1786 }
1787}
1788
1789#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
1795pub struct SessionMeta {
1796 pub id: ThreadId,
1797 #[serde(skip_serializing_if = "Option::is_none")]
1798 pub forked_from_id: Option<ThreadId>,
1799 pub timestamp: String,
1800 pub cwd: PathBuf,
1801 pub originator: String,
1802 pub cli_version: String,
1803 #[serde(default)]
1804 pub source: SessionSource,
1805 pub model_provider: Option<String>,
1806 pub base_instructions: Option<BaseInstructions>,
1810 #[serde(skip_serializing_if = "Option::is_none")]
1811 pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
1812}
1813
1814impl Default for SessionMeta {
1815 fn default() -> Self {
1816 SessionMeta {
1817 id: ThreadId::default(),
1818 forked_from_id: None,
1819 timestamp: String::new(),
1820 cwd: PathBuf::new(),
1821 originator: String::new(),
1822 cli_version: String::new(),
1823 source: SessionSource::default(),
1824 model_provider: None,
1825 base_instructions: None,
1826 dynamic_tools: None,
1827 }
1828 }
1829}
1830
1831#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
1832pub struct SessionMetaLine {
1833 #[serde(flatten)]
1834 pub meta: SessionMeta,
1835 #[serde(skip_serializing_if = "Option::is_none")]
1836 pub git: Option<GitInfo>,
1837}
1838
1839#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1840pub struct OrderMeta {
1841 pub request_ordinal: u64,
1843 #[serde(skip_serializing_if = "Option::is_none")]
1845 pub output_index: Option<u32>,
1846 #[serde(skip_serializing_if = "Option::is_none")]
1848 pub sequence_number: Option<u64>,
1849}
1850
1851#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
1852pub struct RecordedEvent {
1853 pub id: String,
1854 pub event_seq: u64,
1855 #[serde(skip_serializing_if = "Option::is_none")]
1856 pub order: Option<OrderMeta>,
1857 pub msg: EventMsg,
1858}
1859
1860#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
1861#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
1862pub enum RolloutItem {
1863 SessionMeta(SessionMetaLine),
1864 ResponseItem(ResponseItem),
1865 Compacted(CompactedItem),
1866 TurnContext(TurnContextItem),
1867 Event(RecordedEvent),
1868 EventMsg(EventMsg),
1869}
1870
1871#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
1872pub struct CompactedItem {
1873 pub message: String,
1874 #[serde(default, skip_serializing_if = "Option::is_none")]
1875 pub replacement_history: Option<Vec<ResponseItem>>,
1876}
1877
1878impl From<CompactedItem> for ResponseItem {
1879 fn from(value: CompactedItem) -> Self {
1880 ResponseItem::Message {
1881 id: None,
1882 role: "assistant".to_string(),
1883 content: vec![ContentItem::OutputText {
1884 text: value.message,
1885 }],
1886 end_turn: None,
1887 phase: None,
1888 }
1889 }
1890}
1891
1892#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
1893pub struct TurnContextItem {
1894 pub cwd: PathBuf,
1895 pub approval_policy: AskForApproval,
1896 pub sandbox_policy: SandboxPolicy,
1897 pub model: String,
1898 #[serde(skip_serializing_if = "Option::is_none")]
1899 pub personality: Option<Personality>,
1900 #[serde(default, skip_serializing_if = "Option::is_none")]
1901 pub collaboration_mode: Option<CollaborationMode>,
1902 #[serde(skip_serializing_if = "Option::is_none")]
1903 pub effort: Option<ReasoningEffortConfig>,
1904 pub summary: ReasoningSummaryConfig,
1905 #[serde(skip_serializing_if = "Option::is_none")]
1906 pub user_instructions: Option<String>,
1907 #[serde(skip_serializing_if = "Option::is_none")]
1908 pub developer_instructions: Option<String>,
1909 #[serde(skip_serializing_if = "Option::is_none")]
1910 pub final_output_json_schema: Option<Value>,
1911 #[serde(skip_serializing_if = "Option::is_none")]
1912 pub truncation_policy: Option<TruncationPolicy>,
1913}
1914
1915#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1916#[serde(tag = "mode", content = "limit", rename_all = "snake_case")]
1917pub enum TruncationPolicy {
1918 Bytes(usize),
1919 Tokens(usize),
1920}
1921
1922#[derive(Serialize, Deserialize, Clone, JsonSchema)]
1923pub struct RolloutLine {
1924 pub timestamp: String,
1925 #[serde(flatten)]
1926 pub item: RolloutItem,
1927}
1928
1929#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
1930pub struct GitInfo {
1931 #[serde(skip_serializing_if = "Option::is_none")]
1933 pub commit_hash: Option<String>,
1934 #[serde(skip_serializing_if = "Option::is_none")]
1936 pub branch: Option<String>,
1937 #[serde(skip_serializing_if = "Option::is_none")]
1939 pub repository_url: Option<String>,
1940}
1941
1942#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
1943#[serde(rename_all = "snake_case")]
1944pub enum ReviewDelivery {
1945 Inline,
1946 Detached,
1947}
1948
1949#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)]
1950#[serde(tag = "type", rename_all = "camelCase")]
1951#[ts(tag = "type")]
1952pub enum ReviewTarget {
1953 UncommittedChanges,
1955
1956 #[serde(rename_all = "camelCase")]
1958 #[ts(rename_all = "camelCase")]
1959 BaseBranch { branch: String },
1960
1961 #[serde(rename_all = "camelCase")]
1963 #[ts(rename_all = "camelCase")]
1964 Commit {
1965 sha: String,
1966 title: Option<String>,
1968 },
1969
1970 #[serde(rename_all = "camelCase")]
1972 #[ts(rename_all = "camelCase")]
1973 Custom { instructions: String },
1974}
1975
1976#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1977pub struct ReviewRequest {
1979 pub target: ReviewTarget,
1980 #[serde(skip_serializing_if = "Option::is_none")]
1981 #[ts(optional)]
1982 pub user_facing_hint: Option<String>,
1983 #[serde(default)]
1986 pub prompt: String,
1987}
1988
1989#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
1990pub struct ReviewSnapshotInfo {
1991 #[serde(skip_serializing_if = "Option::is_none")]
1992 pub snapshot_commit: Option<String>,
1993 #[serde(skip_serializing_if = "Option::is_none")]
1994 pub branch: Option<String>,
1995 #[serde(skip_serializing_if = "Option::is_none")]
1996 pub worktree_path: Option<PathBuf>,
1997 #[serde(skip_serializing_if = "Option::is_none")]
1998 pub repo_root: Option<PathBuf>,
1999}
2000
2001#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2002pub struct ReviewContextMetadata {
2003 #[serde(skip_serializing_if = "Option::is_none")]
2004 pub snapshot: Option<ReviewSnapshotInfo>,
2005}
2006
2007#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2009pub struct ReviewOutputEvent {
2010 pub findings: Vec<ReviewFinding>,
2011 pub overall_correctness: String,
2012 pub overall_explanation: String,
2013 pub overall_confidence_score: f32,
2014}
2015
2016impl Default for ReviewOutputEvent {
2017 fn default() -> Self {
2018 Self {
2019 findings: Vec::new(),
2020 overall_correctness: String::default(),
2021 overall_explanation: String::default(),
2022 overall_confidence_score: 0.0,
2023 }
2024 }
2025}
2026
2027#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2029pub struct ReviewFinding {
2030 pub title: String,
2031 pub body: String,
2032 pub confidence_score: f32,
2033 pub priority: i32,
2034 pub code_location: ReviewCodeLocation,
2035}
2036
2037#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2039pub struct ReviewCodeLocation {
2040 pub absolute_file_path: PathBuf,
2041 pub line_range: ReviewLineRange,
2042}
2043
2044#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2046pub struct ReviewLineRange {
2047 pub start: u32,
2048 pub end: u32,
2049}
2050
2051#[derive(
2052 Debug, Clone, Copy, Display, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS, Default,
2053)]
2054#[serde(rename_all = "snake_case")]
2055pub enum ExecCommandSource {
2056 #[default]
2057 Agent,
2058 UserShell,
2059 UnifiedExecStartup,
2060 UnifiedExecInteraction,
2061}
2062
2063#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2064pub struct ExecCommandBeginEvent {
2065 pub call_id: String,
2067 #[serde(default, skip_serializing_if = "Option::is_none")]
2069 #[ts(optional)]
2070 pub process_id: Option<String>,
2071 pub turn_id: String,
2073 pub command: Vec<String>,
2075 pub cwd: PathBuf,
2077 pub parsed_cmd: Vec<ParsedCommand>,
2078 #[serde(default)]
2080 pub source: ExecCommandSource,
2081 #[serde(default, skip_serializing_if = "Option::is_none")]
2083 #[ts(optional)]
2084 pub interaction_input: Option<String>,
2085}
2086
2087#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2088pub struct ExecCommandEndEvent {
2089 pub call_id: String,
2091 #[serde(default, skip_serializing_if = "Option::is_none")]
2093 #[ts(optional)]
2094 pub process_id: Option<String>,
2095 pub turn_id: String,
2097 pub command: Vec<String>,
2099 pub cwd: PathBuf,
2101 pub parsed_cmd: Vec<ParsedCommand>,
2102 #[serde(default)]
2104 pub source: ExecCommandSource,
2105 #[serde(default, skip_serializing_if = "Option::is_none")]
2107 #[ts(optional)]
2108 pub interaction_input: Option<String>,
2109
2110 pub stdout: String,
2112 pub stderr: String,
2114 #[serde(default)]
2116 pub aggregated_output: String,
2117 pub exit_code: i32,
2119 #[ts(type = "string")]
2121 pub duration: Duration,
2122 pub formatted_output: String,
2124}
2125
2126#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2127pub struct ViewImageToolCallEvent {
2128 pub call_id: String,
2130 pub path: PathBuf,
2132}
2133
2134#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2135#[serde(rename_all = "snake_case")]
2136pub enum ExecOutputStream {
2137 Stdout,
2138 Stderr,
2139}
2140
2141#[serde_as]
2142#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2143pub struct ExecCommandOutputDeltaEvent {
2144 pub call_id: String,
2146 pub stream: ExecOutputStream,
2148 #[serde_as(as = "serde_with::base64::Base64")]
2150 #[schemars(with = "String")]
2151 #[ts(type = "string")]
2152 pub chunk: Vec<u8>,
2153}
2154
2155#[serde_as]
2156#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2157pub struct TerminalInteractionEvent {
2158 pub call_id: String,
2160 pub process_id: String,
2162 pub stdin: String,
2164}
2165
2166#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2167pub struct BackgroundEventEvent {
2168 pub message: String,
2169}
2170
2171#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2172pub struct DeprecationNoticeEvent {
2173 pub summary: String,
2175 #[serde(skip_serializing_if = "Option::is_none")]
2177 pub details: Option<String>,
2178}
2179
2180#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2181pub struct UndoStartedEvent {
2182 #[serde(skip_serializing_if = "Option::is_none")]
2183 pub message: Option<String>,
2184}
2185
2186#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2187pub struct UndoCompletedEvent {
2188 pub success: bool,
2189 #[serde(skip_serializing_if = "Option::is_none")]
2190 pub message: Option<String>,
2191}
2192
2193#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2194pub struct ThreadRolledBackEvent {
2195 pub num_turns: u32,
2197}
2198
2199#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2200pub struct StreamErrorEvent {
2201 pub message: String,
2202 #[serde(default)]
2203 pub codex_error_info: Option<CodexErrorInfo>,
2204 #[serde(default)]
2208 pub additional_details: Option<String>,
2209}
2210
2211#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2212pub struct StreamInfoEvent {
2213 pub message: String,
2214}
2215
2216#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2217pub struct PatchApplyBeginEvent {
2218 pub call_id: String,
2220 #[serde(default)]
2223 pub turn_id: String,
2224 pub auto_approved: bool,
2226 pub changes: HashMap<PathBuf, FileChange>,
2228}
2229
2230#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2231pub struct PatchApplyEndEvent {
2232 pub call_id: String,
2234 #[serde(default)]
2237 pub turn_id: String,
2238 pub stdout: String,
2240 pub stderr: String,
2242 pub success: bool,
2244 #[serde(default)]
2246 pub changes: HashMap<PathBuf, FileChange>,
2247}
2248
2249#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2250pub struct TurnDiffEvent {
2251 pub unified_diff: String,
2252}
2253
2254#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2255pub struct GetHistoryEntryResponseEvent {
2256 pub offset: usize,
2257 pub log_id: u64,
2258 #[serde(skip_serializing_if = "Option::is_none")]
2260 pub entry: Option<HistoryEntry>,
2261}
2262
2263#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2264pub struct McpListToolsResponseEvent {
2265 pub tools: std::collections::HashMap<String, McpTool>,
2267 #[serde(default, skip_serializing_if = "Option::is_none")]
2269 pub server_tools: Option<std::collections::HashMap<String, Vec<String>>>,
2270 #[serde(default, skip_serializing_if = "Option::is_none")]
2272 pub server_failures: Option<std::collections::HashMap<String, McpServerFailure>>,
2273 #[serde(default)]
2275 pub resources: std::collections::HashMap<String, Vec<McpResource>>,
2276 #[serde(default)]
2278 pub resource_templates: std::collections::HashMap<String, Vec<McpResourceTemplate>>,
2279 #[serde(default)]
2281 pub auth_statuses: std::collections::HashMap<String, McpAuthStatus>,
2282}
2283
2284#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
2285#[serde(rename_all = "snake_case")]
2286#[ts(rename_all = "snake_case")]
2287pub enum McpServerFailurePhase {
2288 Start,
2289 ListTools,
2290}
2291
2292#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)]
2293pub struct McpServerFailure {
2294 pub phase: McpServerFailurePhase,
2295 pub message: String,
2296}
2297
2298#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2299pub struct McpStartupUpdateEvent {
2300 pub server: String,
2302 pub status: McpStartupStatus,
2304}
2305
2306#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2307#[serde(rename_all = "snake_case", tag = "state")]
2308#[ts(rename_all = "snake_case", tag = "state")]
2309pub enum McpStartupStatus {
2310 Starting,
2311 Ready,
2312 Failed { error: String },
2313 Cancelled,
2314}
2315
2316#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, Default)]
2317pub struct McpStartupCompleteEvent {
2318 pub ready: Vec<String>,
2319 pub failed: Vec<McpStartupFailure>,
2320 pub cancelled: Vec<String>,
2321}
2322
2323#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2324pub struct McpStartupFailure {
2325 pub server: String,
2326 pub error: String,
2327}
2328
2329#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
2330#[serde(rename_all = "snake_case")]
2331#[ts(rename_all = "snake_case")]
2332pub enum McpAuthStatus {
2333 Unsupported,
2334 NotLoggedIn,
2335 BearerToken,
2336 OAuth,
2337}
2338
2339impl fmt::Display for McpAuthStatus {
2340 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2341 let text = match self {
2342 McpAuthStatus::Unsupported => "Unsupported",
2343 McpAuthStatus::NotLoggedIn => "Not logged in",
2344 McpAuthStatus::BearerToken => "Bearer token",
2345 McpAuthStatus::OAuth => "OAuth",
2346 };
2347 f.write_str(text)
2348 }
2349}
2350
2351#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2353pub struct ListCustomPromptsResponseEvent {
2354 pub custom_prompts: Vec<CustomPrompt>,
2355}
2356
2357#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2359pub struct ListSkillsResponseEvent {
2360 pub skills: Vec<SkillsListEntry>,
2361}
2362
2363#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2364pub struct RemoteSkillSummary {
2365 pub id: String,
2366 pub name: String,
2367 pub description: String,
2368}
2369
2370#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2372pub struct ListRemoteSkillsResponseEvent {
2373 pub skills: Vec<RemoteSkillSummary>,
2374}
2375
2376#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2378pub struct RemoteSkillDownloadedEvent {
2379 pub id: String,
2380 pub name: String,
2381 pub path: PathBuf,
2382}
2383
2384#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
2385#[serde(rename_all = "snake_case")]
2386#[ts(rename_all = "snake_case")]
2387pub enum SkillScope {
2388 User,
2389 Repo,
2390 System,
2391 Admin,
2392}
2393
2394#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2395pub struct SkillMetadata {
2396 pub name: String,
2397 pub description: String,
2398 #[serde(default, skip_serializing_if = "Option::is_none")]
2399 #[ts(optional)]
2400 pub short_description: Option<String>,
2402 #[serde(default, skip_serializing_if = "Option::is_none")]
2403 #[ts(optional)]
2404 pub interface: Option<SkillInterface>,
2405 #[serde(default, skip_serializing_if = "Option::is_none")]
2406 #[ts(optional)]
2407 pub dependencies: Option<SkillDependencies>,
2408 pub path: PathBuf,
2409 pub scope: SkillScope,
2410 pub enabled: bool,
2411}
2412
2413#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)]
2414pub struct SkillInterface {
2415 #[ts(optional)]
2416 pub display_name: Option<String>,
2417 #[ts(optional)]
2418 pub short_description: Option<String>,
2419 #[ts(optional)]
2420 pub icon_small: Option<PathBuf>,
2421 #[ts(optional)]
2422 pub icon_large: Option<PathBuf>,
2423 #[ts(optional)]
2424 pub brand_color: Option<String>,
2425 #[ts(optional)]
2426 pub default_prompt: Option<String>,
2427}
2428
2429#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)]
2430pub struct SkillDependencies {
2431 pub tools: Vec<SkillToolDependency>,
2432}
2433
2434#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)]
2435pub struct SkillToolDependency {
2436 #[serde(rename = "type")]
2437 #[ts(rename = "type")]
2438 pub r#type: String,
2439 pub value: String,
2440 #[serde(default, skip_serializing_if = "Option::is_none")]
2441 #[ts(optional)]
2442 pub description: Option<String>,
2443 #[serde(default, skip_serializing_if = "Option::is_none")]
2444 #[ts(optional)]
2445 pub transport: Option<String>,
2446 #[serde(default, skip_serializing_if = "Option::is_none")]
2447 #[ts(optional)]
2448 pub command: Option<String>,
2449 #[serde(default, skip_serializing_if = "Option::is_none")]
2450 #[ts(optional)]
2451 pub url: Option<String>,
2452}
2453
2454#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2455pub struct SkillErrorInfo {
2456 pub path: PathBuf,
2457 pub message: String,
2458}
2459
2460#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2461pub struct SkillsListEntry {
2462 pub cwd: PathBuf,
2463 pub skills: Vec<SkillMetadata>,
2464 pub errors: Vec<SkillErrorInfo>,
2465}
2466
2467#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2468pub struct SessionConfiguredEvent {
2469 pub session_id: ThreadId,
2470 #[serde(skip_serializing_if = "Option::is_none")]
2471 pub forked_from_id: Option<ThreadId>,
2472
2473 #[serde(default, skip_serializing_if = "Option::is_none")]
2475 #[ts(optional)]
2476 pub thread_name: Option<String>,
2477
2478 pub model: String,
2480
2481 pub model_provider_id: String,
2482
2483 pub approval_policy: AskForApproval,
2485
2486 pub sandbox_policy: SandboxPolicy,
2488
2489 pub cwd: PathBuf,
2492
2493 #[serde(skip_serializing_if = "Option::is_none")]
2495 pub reasoning_effort: Option<ReasoningEffortConfig>,
2496
2497 pub history_log_id: u64,
2499
2500 pub history_entry_count: usize,
2502
2503 #[serde(skip_serializing_if = "Option::is_none")]
2506 pub initial_messages: Option<Vec<EventMsg>>,
2507
2508 #[serde(skip_serializing_if = "Option::is_none")]
2510 pub rollout_path: Option<PathBuf>,
2511}
2512
2513#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2514pub struct ThreadNameUpdatedEvent {
2515 pub thread_id: ThreadId,
2516 #[serde(default, skip_serializing_if = "Option::is_none")]
2517 #[ts(optional)]
2518 pub thread_name: Option<String>,
2519}
2520
2521#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, Display, JsonSchema, TS)]
2523#[serde(rename_all = "snake_case")]
2524pub enum ReviewDecision {
2525 Approved,
2527
2528 ApprovedExecpolicyAmendment {
2531 proposed_execpolicy_amendment: ExecPolicyAmendment,
2532 },
2533
2534 ApprovedForSession,
2538
2539 #[default]
2542 Denied,
2543
2544 Abort,
2547}
2548
2549impl ReviewDecision {
2550 pub fn to_opaque_string(&self) -> &'static str {
2553 match self {
2554 ReviewDecision::Approved => "approved",
2555 ReviewDecision::ApprovedExecpolicyAmendment { .. } => "approved_with_amendment",
2556 ReviewDecision::ApprovedForSession => "approved_for_session",
2557 ReviewDecision::Denied => "denied",
2558 ReviewDecision::Abort => "abort",
2559 }
2560 }
2561}
2562
2563#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2564#[serde(tag = "type", rename_all = "snake_case")]
2565#[ts(tag = "type")]
2566pub enum FileChange {
2567 Add {
2568 content: String,
2569 },
2570 Delete {
2571 content: String,
2572 },
2573 Update {
2574 unified_diff: String,
2575 move_path: Option<PathBuf>,
2576 },
2577}
2578
2579#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2580pub struct Chunk {
2581 pub orig_index: u32,
2583 pub deleted_lines: Vec<String>,
2584 pub inserted_lines: Vec<String>,
2585}
2586
2587#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
2588pub struct TurnAbortedEvent {
2589 pub reason: TurnAbortReason,
2590}
2591
2592#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2593#[serde(rename_all = "snake_case")]
2594pub enum TurnAbortReason {
2595 Interrupted,
2596 Replaced,
2597 ReviewEnded,
2598}
2599
2600#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2601pub struct CollabAgentSpawnBeginEvent {
2602 pub call_id: String,
2604 pub sender_thread_id: ThreadId,
2606 pub prompt: String,
2609}
2610
2611#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2612pub struct CollabAgentSpawnEndEvent {
2613 pub call_id: String,
2615 pub sender_thread_id: ThreadId,
2617 pub new_thread_id: Option<ThreadId>,
2619 pub prompt: String,
2622 pub status: AgentStatus,
2624}
2625
2626#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2627pub struct CollabAgentInteractionBeginEvent {
2628 pub call_id: String,
2630 pub sender_thread_id: ThreadId,
2632 pub receiver_thread_id: ThreadId,
2634 pub prompt: String,
2637}
2638
2639#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2640pub struct CollabAgentInteractionEndEvent {
2641 pub call_id: String,
2643 pub sender_thread_id: ThreadId,
2645 pub receiver_thread_id: ThreadId,
2647 pub prompt: String,
2650 pub status: AgentStatus,
2652}
2653
2654#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2655pub struct CollabWaitingBeginEvent {
2656 pub sender_thread_id: ThreadId,
2658 pub receiver_thread_ids: Vec<ThreadId>,
2660 pub call_id: String,
2662}
2663
2664#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2665pub struct CollabWaitingEndEvent {
2666 pub sender_thread_id: ThreadId,
2668 pub call_id: String,
2670 pub statuses: HashMap<ThreadId, AgentStatus>,
2672}
2673
2674#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2675pub struct CollabCloseBeginEvent {
2676 pub call_id: String,
2678 pub sender_thread_id: ThreadId,
2680 pub receiver_thread_id: ThreadId,
2682}
2683
2684#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2685pub struct CollabCloseEndEvent {
2686 pub call_id: String,
2688 pub sender_thread_id: ThreadId,
2690 pub receiver_thread_id: ThreadId,
2692 pub status: AgentStatus,
2695}
2696
2697#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2698pub struct CollabResumeBeginEvent {
2699 pub call_id: String,
2701 pub sender_thread_id: ThreadId,
2703 pub receiver_thread_id: ThreadId,
2705}
2706
2707#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
2708pub struct CollabResumeEndEvent {
2709 pub call_id: String,
2711 pub sender_thread_id: ThreadId,
2713 pub receiver_thread_id: ThreadId,
2715 pub status: AgentStatus,
2718}
2719
2720#[cfg(test)]
2721mod tests {
2722 use super::*;
2723 use crate::items::UserMessageItem;
2724 use crate::items::WebSearchItem;
2725 use anyhow::Result;
2726 use pretty_assertions::assert_eq;
2727 use serde_json::json;
2728 use tempfile::NamedTempFile;
2729
2730 #[test]
2731 fn external_sandbox_reports_full_access_flags() {
2732 let restricted = SandboxPolicy::ExternalSandbox {
2733 network_access: NetworkAccess::Restricted,
2734 };
2735 assert!(restricted.has_full_disk_write_access());
2736 assert!(!restricted.has_full_network_access());
2737
2738 let enabled = SandboxPolicy::ExternalSandbox {
2739 network_access: NetworkAccess::Enabled,
2740 };
2741 assert!(enabled.has_full_disk_write_access());
2742 assert!(enabled.has_full_network_access());
2743 }
2744
2745 #[test]
2746 fn item_started_event_from_web_search_emits_begin_event() {
2747 let event = ItemStartedEvent {
2748 thread_id: ThreadId::new(),
2749 turn_id: "turn-1".into(),
2750 item: TurnItem::WebSearch(WebSearchItem {
2751 id: "search-1".into(),
2752 query: "find docs".into(),
2753 action: WebSearchAction::Search {
2754 query: Some("find docs".into()),
2755 queries: None,
2756 },
2757 }),
2758 };
2759
2760 let legacy_events = event.as_legacy_events(false);
2761 assert_eq!(legacy_events.len(), 1);
2762 match &legacy_events[0] {
2763 EventMsg::WebSearchBegin(event) => assert_eq!(event.call_id, "search-1"),
2764 _ => panic!("expected WebSearchBegin event"),
2765 }
2766 }
2767
2768 #[test]
2769 fn item_started_event_from_non_web_search_emits_no_legacy_events() {
2770 let event = ItemStartedEvent {
2771 thread_id: ThreadId::new(),
2772 turn_id: "turn-1".into(),
2773 item: TurnItem::UserMessage(UserMessageItem::new(&[])),
2774 };
2775
2776 assert!(event.as_legacy_events(false).is_empty());
2777 }
2778
2779 #[test]
2780 fn user_input_serialization_omits_final_output_json_schema_when_none() -> Result<()> {
2781 let op = Op::UserInput {
2782 items: Vec::new(),
2783 final_output_json_schema: None,
2784 };
2785
2786 let json_op = serde_json::to_value(op)?;
2787 assert_eq!(json_op, json!({ "type": "user_input", "items": [] }));
2788
2789 Ok(())
2790 }
2791
2792 #[test]
2793 fn user_input_deserializes_without_final_output_json_schema_field() -> Result<()> {
2794 let op: Op = serde_json::from_value(json!({ "type": "user_input", "items": [] }))?;
2795
2796 assert_eq!(
2797 op,
2798 Op::UserInput {
2799 items: Vec::new(),
2800 final_output_json_schema: None,
2801 }
2802 );
2803
2804 Ok(())
2805 }
2806
2807 #[test]
2808 fn user_input_serialization_includes_final_output_json_schema_when_some() -> Result<()> {
2809 let schema = json!({
2810 "type": "object",
2811 "properties": {
2812 "answer": { "type": "string" }
2813 },
2814 "required": ["answer"],
2815 "additionalProperties": false
2816 });
2817 let op = Op::UserInput {
2818 items: Vec::new(),
2819 final_output_json_schema: Some(schema.clone()),
2820 };
2821
2822 let json_op = serde_json::to_value(op)?;
2823 assert_eq!(
2824 json_op,
2825 json!({
2826 "type": "user_input",
2827 "items": [],
2828 "final_output_json_schema": schema,
2829 })
2830 );
2831
2832 Ok(())
2833 }
2834
2835 #[test]
2836 fn user_input_text_serializes_empty_text_elements() -> Result<()> {
2837 let input = UserInput::Text {
2838 text: "hello".to_string(),
2839 text_elements: Vec::new(),
2840 };
2841
2842 let json_input = serde_json::to_value(input)?;
2843 assert_eq!(
2844 json_input,
2845 json!({
2846 "type": "text",
2847 "text": "hello",
2848 "text_elements": [],
2849 })
2850 );
2851
2852 Ok(())
2853 }
2854
2855 #[test]
2856 fn user_message_event_serializes_empty_metadata_vectors() -> Result<()> {
2857 let event = UserMessageEvent {
2858 message: "hello".to_string(),
2859 images: None,
2860 local_images: Vec::new(),
2861 text_elements: Vec::new(),
2862 };
2863
2864 let json_event = serde_json::to_value(event)?;
2865 assert_eq!(
2866 json_event,
2867 json!({
2868 "message": "hello",
2869 "local_images": [],
2870 "text_elements": [],
2871 })
2872 );
2873
2874 Ok(())
2875 }
2876
2877 #[test]
2880 fn serialize_event() -> Result<()> {
2881 let conversation_id = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;
2882 let rollout_file = NamedTempFile::new()?;
2883 let event = Event {
2884 id: "1234".to_string(),
2885 msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
2886 session_id: conversation_id,
2887 forked_from_id: None,
2888 thread_name: None,
2889 model: "codex-mini-latest".to_string(),
2890 model_provider_id: "openai".to_string(),
2891 approval_policy: AskForApproval::Never,
2892 sandbox_policy: SandboxPolicy::ReadOnly,
2893 cwd: PathBuf::from("/home/user/project"),
2894 reasoning_effort: Some(ReasoningEffortConfig::default()),
2895 history_log_id: 0,
2896 history_entry_count: 0,
2897 initial_messages: None,
2898 rollout_path: Some(rollout_file.path().to_path_buf()),
2899 }),
2900 };
2901
2902 let expected = json!({
2903 "id": "1234",
2904 "msg": {
2905 "type": "session_configured",
2906 "session_id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
2907 "model": "codex-mini-latest",
2908 "model_provider_id": "openai",
2909 "approval_policy": "never",
2910 "sandbox_policy": {
2911 "type": "read-only"
2912 },
2913 "cwd": "/home/user/project",
2914 "reasoning_effort": "medium",
2915 "history_log_id": 0,
2916 "history_entry_count": 0,
2917 "rollout_path": format!("{}", rollout_file.path().display()),
2918 }
2919 });
2920 assert_eq!(expected, serde_json::to_value(&event)?);
2921 Ok(())
2922 }
2923
2924 #[test]
2925 fn vec_u8_as_base64_serialization_and_deserialization() -> Result<()> {
2926 let event = ExecCommandOutputDeltaEvent {
2927 call_id: "call21".to_string(),
2928 stream: ExecOutputStream::Stdout,
2929 chunk: vec![1, 2, 3, 4, 5],
2930 };
2931 let serialized = serde_json::to_string(&event)?;
2932 assert_eq!(
2933 r#"{"call_id":"call21","stream":"stdout","chunk":"AQIDBAU="}"#,
2934 serialized,
2935 );
2936
2937 let deserialized: ExecCommandOutputDeltaEvent = serde_json::from_str(&serialized)?;
2938 assert_eq!(deserialized, event);
2939 Ok(())
2940 }
2941
2942 #[test]
2943 fn serialize_mcp_startup_update_event() -> Result<()> {
2944 let event = Event {
2945 id: "init".to_string(),
2946 msg: EventMsg::McpStartupUpdate(McpStartupUpdateEvent {
2947 server: "srv".to_string(),
2948 status: McpStartupStatus::Failed {
2949 error: "boom".to_string(),
2950 },
2951 }),
2952 };
2953
2954 let value = serde_json::to_value(&event)?;
2955 assert_eq!(value["msg"]["type"], "mcp_startup_update");
2956 assert_eq!(value["msg"]["server"], "srv");
2957 assert_eq!(value["msg"]["status"]["state"], "failed");
2958 assert_eq!(value["msg"]["status"]["error"], "boom");
2959 Ok(())
2960 }
2961
2962 #[test]
2963 fn serialize_mcp_startup_complete_event() -> Result<()> {
2964 let event = Event {
2965 id: "init".to_string(),
2966 msg: EventMsg::McpStartupComplete(McpStartupCompleteEvent {
2967 ready: vec!["a".to_string()],
2968 failed: vec![McpStartupFailure {
2969 server: "b".to_string(),
2970 error: "bad".to_string(),
2971 }],
2972 cancelled: vec!["c".to_string()],
2973 }),
2974 };
2975
2976 let value = serde_json::to_value(&event)?;
2977 assert_eq!(value["msg"]["type"], "mcp_startup_complete");
2978 assert_eq!(value["msg"]["ready"][0], "a");
2979 assert_eq!(value["msg"]["failed"][0]["server"], "b");
2980 assert_eq!(value["msg"]["failed"][0]["error"], "bad");
2981 assert_eq!(value["msg"]["cancelled"][0], "c");
2982 Ok(())
2983 }
2984}