1use crate::io::items::ThreadItem;
34use serde::{Deserialize, Serialize};
35use serde_json::Value;
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
53#[serde(tag = "type", rename_all = "camelCase")]
54pub enum UserInput {
55 Text { text: String },
57 Image { data: String },
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct ClientInfo {
71 pub name: String,
73 pub version: String,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub title: Option<String>,
78}
79
80#[derive(Debug, Clone, Default, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct InitializeCapabilities {
84 #[serde(default)]
86 pub experimental_api: bool,
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub opt_out_notification_methods: Option<Vec<String>>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
96#[serde(rename_all = "camelCase")]
97pub struct InitializeParams {
98 pub client_info: ClientInfo,
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub capabilities: Option<InitializeCapabilities>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub struct InitializeResponse {
109 pub user_agent: String,
111}
112
113#[derive(Debug, Clone, Default, Serialize, Deserialize)]
121#[serde(rename_all = "camelCase")]
122pub struct ThreadStartParams {
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub instructions: Option<String>,
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub tools: Option<Vec<Value>>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133#[serde(rename_all = "camelCase")]
134pub struct ThreadInfo {
135 pub id: String,
137 #[serde(flatten)]
139 pub extra: Value,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct ThreadStartResponse {
146 pub thread: ThreadInfo,
148 #[serde(default)]
150 pub model: Option<String>,
151 #[serde(flatten)]
153 pub extra: Value,
154}
155
156impl ThreadStartResponse {
157 pub fn thread_id(&self) -> &str {
159 &self.thread.id
160 }
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
165#[serde(rename_all = "camelCase")]
166pub struct ThreadArchiveParams {
167 pub thread_id: String,
168}
169
170#[derive(Debug, Clone, Default, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct ThreadArchiveResponse {}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
184#[serde(rename_all = "camelCase")]
185pub struct TurnStartParams {
186 pub thread_id: String,
188 pub input: Vec<UserInput>,
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub model: Option<String>,
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub reasoning_effort: Option<String>,
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub sandbox_policy: Option<Value>,
199}
200
201#[derive(Debug, Clone, Default, Serialize, Deserialize)]
203#[serde(rename_all = "camelCase")]
204pub struct TurnStartResponse {}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
208#[serde(rename_all = "camelCase")]
209pub struct TurnInterruptParams {
210 pub thread_id: String,
211}
212
213#[derive(Debug, Clone, Default, Serialize, Deserialize)]
215#[serde(rename_all = "camelCase")]
216pub struct TurnInterruptResponse {}
217
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
224#[serde(rename_all = "camelCase")]
225pub enum TurnStatus {
226 Completed,
228 Interrupted,
230 Failed,
232 InProgress,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238#[serde(rename_all = "camelCase")]
239pub struct TurnError {
240 pub message: String,
241 #[serde(skip_serializing_if = "Option::is_none")]
242 pub codex_error_info: Option<Value>,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
249#[serde(rename_all = "camelCase")]
250pub struct Turn {
251 pub id: String,
253 #[serde(default)]
255 pub items: Vec<ThreadItem>,
256 pub status: TurnStatus,
258 #[serde(skip_serializing_if = "Option::is_none")]
260 pub error: Option<TurnError>,
261}
262
263#[derive(Debug, Clone, Default, Serialize, Deserialize)]
270#[serde(rename_all = "camelCase")]
271pub struct TokenCounts {
272 #[serde(default)]
274 pub input_tokens: u64,
275 #[serde(default)]
277 pub output_tokens: u64,
278 #[serde(default)]
280 pub cached_input_tokens: u64,
281 #[serde(default)]
283 pub reasoning_output_tokens: u64,
284 #[serde(default)]
286 pub total_tokens: u64,
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
295#[serde(rename_all = "camelCase")]
296pub struct TokenUsage {
297 pub last: TokenCounts,
299 pub total: TokenCounts,
301 pub model_context_window: u64,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
314#[serde(tag = "type", rename_all = "camelCase")]
315pub enum ThreadStatus {
316 NotLoaded,
318 Idle,
320 Active {
322 #[serde(default, skip_serializing_if = "Vec::is_empty")]
325 active_flags: Vec<Value>,
326 },
327 SystemError,
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
340#[serde(rename_all = "camelCase")]
341pub struct ThreadStartedNotification {
342 pub thread: ThreadInfo,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
347#[serde(rename_all = "camelCase")]
348pub struct ThreadStatusChangedNotification {
349 pub thread_id: String,
350 pub status: ThreadStatus,
351}
352
353#[derive(Debug, Clone, Serialize, Deserialize)]
357#[serde(rename_all = "camelCase")]
358pub struct TurnStartedNotification {
359 pub thread_id: String,
360 pub turn: Turn,
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize)]
367#[serde(rename_all = "camelCase")]
368pub struct TurnCompletedNotification {
369 pub thread_id: String,
370 pub turn: Turn,
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize)]
375#[serde(rename_all = "camelCase")]
376pub struct ItemStartedNotification {
377 pub thread_id: String,
378 pub turn_id: String,
379 #[serde(default, skip_serializing_if = "Option::is_none")]
382 pub started_at_ms: Option<i64>,
383 pub item: ThreadItem,
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize)]
388#[serde(rename_all = "camelCase")]
389pub struct ItemCompletedNotification {
390 pub thread_id: String,
391 pub turn_id: String,
392 #[serde(default, skip_serializing_if = "Option::is_none")]
395 pub completed_at_ms: Option<i64>,
396 pub item: ThreadItem,
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize)]
401#[serde(rename_all = "camelCase")]
402pub struct AgentMessageDeltaNotification {
403 pub thread_id: String,
404 pub item_id: String,
405 pub delta: String,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
410#[serde(rename_all = "camelCase")]
411pub struct CmdOutputDeltaNotification {
412 pub thread_id: String,
413 pub item_id: String,
414 pub delta: String,
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize)]
419#[serde(rename_all = "camelCase")]
420pub struct FileChangeOutputDeltaNotification {
421 pub thread_id: String,
422 pub item_id: String,
423 pub delta: String,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize)]
428#[serde(rename_all = "camelCase")]
429pub struct ReasoningDeltaNotification {
430 pub thread_id: String,
431 pub item_id: String,
432 pub delta: String,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize)]
437#[serde(rename_all = "camelCase")]
438pub struct ErrorNotification {
439 pub error: String,
440 #[serde(skip_serializing_if = "Option::is_none")]
441 pub thread_id: Option<String>,
442 #[serde(skip_serializing_if = "Option::is_none")]
443 pub turn_id: Option<String>,
444 #[serde(default)]
445 pub will_retry: bool,
446}
447
448#[derive(Debug, Clone, Serialize, Deserialize)]
452#[serde(rename_all = "camelCase")]
453pub struct ThreadTokenUsageUpdatedNotification {
454 pub thread_id: String,
455 #[serde(default, skip_serializing_if = "Option::is_none")]
458 pub turn_id: Option<String>,
459 pub token_usage: TokenUsage,
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize)]
464#[serde(rename_all = "camelCase")]
465pub struct RateLimitWindow {
466 pub resets_at: i64,
468 pub used_percent: i32,
470 pub window_duration_mins: i64,
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize)]
476#[serde(rename_all = "camelCase")]
477pub struct RateLimits {
478 #[serde(default, skip_serializing_if = "Option::is_none")]
481 pub credits: Option<Value>,
482 pub limit_id: String,
484 #[serde(default, skip_serializing_if = "Option::is_none")]
486 pub limit_name: Option<String>,
487 pub plan_type: String,
489 #[serde(default, skip_serializing_if = "Option::is_none")]
491 pub primary: Option<RateLimitWindow>,
492 #[serde(default, skip_serializing_if = "Option::is_none")]
494 pub secondary: Option<RateLimitWindow>,
495 #[serde(default, skip_serializing_if = "Option::is_none")]
497 pub rate_limit_reached_type: Option<String>,
498}
499
500#[derive(Debug, Clone, Serialize, Deserialize)]
502#[serde(rename_all = "camelCase")]
503pub struct AccountRateLimitsUpdatedNotification {
504 pub rate_limits: RateLimits,
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize)]
512#[serde(rename_all = "camelCase")]
513pub struct McpServerStartupStatusUpdatedNotification {
514 pub name: String,
516 pub status: String,
519 #[serde(default, skip_serializing_if = "Option::is_none")]
521 pub error: Option<String>,
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
526#[serde(rename_all = "camelCase")]
527pub struct RemoteControlStatusChangedNotification {
528 pub status: String,
530 #[serde(default, skip_serializing_if = "Option::is_none")]
532 pub environment_id: Option<String>,
533}
534
535#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
544#[serde(rename_all = "camelCase")]
545pub enum CommandApprovalDecision {
546 Accept,
548 AcceptForSession,
550 Decline,
552 Cancel,
554}
555
556#[derive(Debug, Clone, Serialize, Deserialize)]
562#[serde(rename_all = "camelCase")]
563pub struct CommandExecutionApprovalParams {
564 pub thread_id: String,
565 pub turn_id: String,
566 pub call_id: String,
568 pub command: String,
570 pub cwd: String,
572 #[serde(skip_serializing_if = "Option::is_none")]
574 pub reason: Option<String>,
575}
576
577#[derive(Debug, Clone, Serialize, Deserialize)]
579#[serde(rename_all = "camelCase")]
580pub struct CommandExecutionApprovalResponse {
581 pub decision: CommandApprovalDecision,
582}
583
584#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
589#[serde(rename_all = "camelCase")]
590pub enum FileChangeApprovalDecision {
591 Accept,
593 AcceptForSession,
595 Decline,
597 Cancel,
599}
600
601#[derive(Debug, Clone, Serialize, Deserialize)]
607#[serde(rename_all = "camelCase")]
608pub struct FileChangeApprovalParams {
609 pub thread_id: String,
610 pub turn_id: String,
611 pub call_id: String,
613 pub changes: Value,
615 #[serde(skip_serializing_if = "Option::is_none")]
617 pub reason: Option<String>,
618}
619
620#[derive(Debug, Clone, Serialize, Deserialize)]
622#[serde(rename_all = "camelCase")]
623pub struct FileChangeApprovalResponse {
624 pub decision: FileChangeApprovalDecision,
625}
626
627pub mod methods {
640 pub const INITIALIZE: &str = "initialize";
642 pub const INITIALIZED: &str = "initialized";
643 pub const THREAD_START: &str = "thread/start";
644 pub const THREAD_ARCHIVE: &str = "thread/archive";
645 pub const TURN_START: &str = "turn/start";
646 pub const TURN_INTERRUPT: &str = "turn/interrupt";
647 pub const TURN_STEER: &str = "turn/steer";
648
649 pub const THREAD_STARTED: &str = "thread/started";
651 pub const THREAD_STATUS_CHANGED: &str = "thread/status/changed";
652 pub const THREAD_TOKEN_USAGE_UPDATED: &str = "thread/tokenUsage/updated";
653 pub const TURN_STARTED: &str = "turn/started";
654 pub const TURN_COMPLETED: &str = "turn/completed";
655 pub const ITEM_STARTED: &str = "item/started";
656 pub const ITEM_COMPLETED: &str = "item/completed";
657 pub const AGENT_MESSAGE_DELTA: &str = "item/agentMessage/delta";
658 pub const CMD_OUTPUT_DELTA: &str = "item/commandExecution/outputDelta";
659 pub const FILE_CHANGE_OUTPUT_DELTA: &str = "item/fileChange/outputDelta";
660 pub const REASONING_DELTA: &str = "item/reasoning/summaryTextDelta";
661 pub const ERROR: &str = "error";
662 pub const ACCOUNT_RATE_LIMITS_UPDATED: &str = "account/rateLimits/updated";
663 pub const MCP_SERVER_STARTUP_STATUS_UPDATED: &str = "mcpServer/startupStatus/updated";
664 pub const REMOTE_CONTROL_STATUS_CHANGED: &str = "remoteControl/status/changed";
665
666 pub const CMD_EXEC_APPROVAL: &str = "item/commandExecution/requestApproval";
668 pub const FILE_CHANGE_APPROVAL: &str = "item/fileChange/requestApproval";
669}
670
671#[cfg(test)]
672mod tests {
673 use super::*;
674
675 #[test]
676 fn test_initialize_params() {
677 let params = InitializeParams {
678 client_info: ClientInfo {
679 name: "my-app".to_string(),
680 version: "1.0.0".to_string(),
681 title: Some("My App".to_string()),
682 },
683 capabilities: None,
684 };
685 let json = serde_json::to_string(¶ms).unwrap();
686 assert!(json.contains("clientInfo"));
687 assert!(json.contains("my-app"));
688 assert!(!json.contains("capabilities"));
689 }
690
691 #[test]
692 fn test_initialize_response() {
693 let json = r#"{"userAgent":"codex-cli/0.104.0"}"#;
694 let resp: InitializeResponse = serde_json::from_str(json).unwrap();
695 assert_eq!(resp.user_agent, "codex-cli/0.104.0");
696 }
697
698 #[test]
699 fn test_initialize_capabilities() {
700 let params = InitializeParams {
701 client_info: ClientInfo {
702 name: "test".to_string(),
703 version: "0.1.0".to_string(),
704 title: None,
705 },
706 capabilities: Some(InitializeCapabilities {
707 experimental_api: true,
708 opt_out_notification_methods: Some(vec!["thread/started".to_string()]),
709 }),
710 };
711 let json = serde_json::to_string(¶ms).unwrap();
712 assert!(json.contains("experimentalApi"));
713 assert!(json.contains("optOutNotificationMethods"));
714 }
715
716 #[test]
717 fn test_user_input_text() {
718 let input = UserInput::Text {
719 text: "Hello".to_string(),
720 };
721 let json = serde_json::to_string(&input).unwrap();
722 assert!(json.contains(r#""type":"text""#));
723 let parsed: UserInput = serde_json::from_str(&json).unwrap();
724 assert!(matches!(parsed, UserInput::Text { text } if text == "Hello"));
725 }
726
727 #[test]
728 fn test_thread_start_params() {
729 let params = ThreadStartParams {
730 instructions: Some("Be helpful".to_string()),
731 tools: None,
732 };
733 let json = serde_json::to_string(¶ms).unwrap();
734 assert!(json.contains("instructions"));
735 assert!(!json.contains("tools"));
736 }
737
738 #[test]
739 fn test_thread_start_response() {
740 let json = r#"{"thread":{"id":"th_abc123"},"model":"gpt-4","approvalPolicy":"never","cwd":"/tmp","modelProvider":"openai","sandbox":{}}"#;
741 let resp: ThreadStartResponse = serde_json::from_str(json).unwrap();
742 assert_eq!(resp.thread_id(), "th_abc123");
743 assert_eq!(resp.model.as_deref(), Some("gpt-4"));
744 }
745
746 #[test]
747 fn test_turn_start_params() {
748 let params = TurnStartParams {
749 thread_id: "th_1".to_string(),
750 input: vec![UserInput::Text {
751 text: "What is 2+2?".to_string(),
752 }],
753 model: None,
754 reasoning_effort: None,
755 sandbox_policy: None,
756 };
757 let json = serde_json::to_string(¶ms).unwrap();
758 assert!(json.contains("threadId"));
759 assert!(json.contains("input"));
760 }
761
762 #[test]
763 fn test_turn_status() {
764 let json = r#""completed""#;
765 let status: TurnStatus = serde_json::from_str(json).unwrap();
766 assert_eq!(status, TurnStatus::Completed);
767 }
768
769 #[test]
770 fn test_turn_completed_notification() {
771 let json = r#"{
772 "threadId": "th_1",
773 "turnId": "t_1",
774 "turn": {
775 "id": "t_1",
776 "items": [],
777 "status": "completed"
778 }
779 }"#;
780 let notif: TurnCompletedNotification = serde_json::from_str(json).unwrap();
781 assert_eq!(notif.thread_id, "th_1");
782 assert_eq!(notif.turn.status, TurnStatus::Completed);
783 }
784
785 #[test]
786 fn test_agent_message_delta() {
787 let json = r#"{"threadId":"th_1","itemId":"msg_1","delta":"Hello "}"#;
788 let notif: AgentMessageDeltaNotification = serde_json::from_str(json).unwrap();
789 assert_eq!(notif.delta, "Hello ");
790 }
791
792 #[test]
793 fn test_command_approval_decision() {
794 let json = r#""accept""#;
795 let decision: CommandApprovalDecision = serde_json::from_str(json).unwrap();
796 assert_eq!(decision, CommandApprovalDecision::Accept);
797
798 let json = r#""acceptForSession""#;
799 let decision: CommandApprovalDecision = serde_json::from_str(json).unwrap();
800 assert_eq!(decision, CommandApprovalDecision::AcceptForSession);
801 }
802
803 #[test]
804 fn test_command_approval_params() {
805 let json = r#"{
806 "threadId": "th_1",
807 "turnId": "t_1",
808 "callId": "call_1",
809 "command": "rm -rf /tmp/test",
810 "cwd": "/home/user"
811 }"#;
812 let params: CommandExecutionApprovalParams = serde_json::from_str(json).unwrap();
813 assert_eq!(params.command, "rm -rf /tmp/test");
814 }
815
816 #[test]
817 fn test_error_notification() {
818 let json = r#"{"error":"something failed","willRetry":true}"#;
819 let notif: ErrorNotification = serde_json::from_str(json).unwrap();
820 assert_eq!(notif.error, "something failed");
821 assert!(notif.will_retry);
822 }
823
824 #[test]
825 fn test_thread_status_idle() {
826 let json = r#"{"type":"idle"}"#;
827 let status: ThreadStatus = serde_json::from_str(json).unwrap();
828 assert!(matches!(status, ThreadStatus::Idle));
829 }
830
831 #[test]
832 fn test_thread_status_active_with_flags() {
833 let json = r#"{"type":"active","activeFlags":[]}"#;
834 let status: ThreadStatus = serde_json::from_str(json).unwrap();
835 match status {
836 ThreadStatus::Active { active_flags } => assert!(active_flags.is_empty()),
837 other => panic!("expected Active, got {:?}", other),
838 }
839 }
840
841 #[test]
842 fn test_token_usage() {
843 let json = r#"{
844 "last":{"inputTokens":100,"outputTokens":200,"cachedInputTokens":50,"reasoningOutputTokens":0,"totalTokens":300},
845 "total":{"inputTokens":1000,"outputTokens":2000,"cachedInputTokens":500,"reasoningOutputTokens":10,"totalTokens":3000},
846 "modelContextWindow":200000
847 }"#;
848 let usage: TokenUsage = serde_json::from_str(json).unwrap();
849 assert_eq!(usage.last.input_tokens, 100);
850 assert_eq!(usage.last.output_tokens, 200);
851 assert_eq!(usage.last.cached_input_tokens, 50);
852 assert_eq!(usage.total.input_tokens, 1000);
853 assert_eq!(usage.model_context_window, 200000);
854 }
855}