1use crate::io::items::ThreadItem;
36use crate::jsonrpc::RequestId;
37use serde::{Deserialize, Serialize};
38use serde_json::Value;
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(tag = "type", rename_all = "camelCase")]
57pub enum UserInput {
58 Text { text: String },
60 Image { data: String },
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct ClientInfo {
74 pub name: String,
76 pub version: String,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub title: Option<String>,
81}
82
83#[derive(Debug, Clone, Default, Serialize, Deserialize)]
85#[serde(rename_all = "camelCase")]
86pub struct InitializeCapabilities {
87 #[serde(default)]
89 pub experimental_api: bool,
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub opt_out_notification_methods: Option<Vec<String>>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(rename_all = "camelCase")]
100pub struct InitializeParams {
101 pub client_info: ClientInfo,
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub capabilities: Option<InitializeCapabilities>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct InitializeResponse {
112 pub user_agent: String,
114}
115
116#[derive(Debug, Clone, Default, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct ThreadStartParams {
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub instructions: Option<String>,
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub tools: Option<Vec<Value>>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136#[serde(rename_all = "camelCase")]
137pub struct ThreadInfo {
138 pub id: String,
140 #[serde(flatten)]
142 pub extra: Value,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct ThreadStartResponse {
149 pub thread: ThreadInfo,
151 #[serde(default)]
153 pub model: Option<String>,
154 #[serde(flatten)]
156 pub extra: Value,
157}
158
159impl ThreadStartResponse {
160 pub fn thread_id(&self) -> &str {
162 &self.thread.id
163 }
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168#[serde(rename_all = "camelCase")]
169pub struct ThreadArchiveParams {
170 pub thread_id: String,
171}
172
173#[derive(Debug, Clone, Default, Serialize, Deserialize)]
175#[serde(rename_all = "camelCase")]
176pub struct ThreadArchiveResponse {}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
187#[serde(rename_all = "camelCase")]
188pub struct TurnStartParams {
189 pub thread_id: String,
191 pub input: Vec<UserInput>,
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub model: Option<String>,
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub reasoning_effort: Option<String>,
199 #[serde(skip_serializing_if = "Option::is_none")]
201 pub sandbox_policy: Option<Value>,
202}
203
204#[derive(Debug, Clone, Default, Serialize, Deserialize)]
206#[serde(rename_all = "camelCase")]
207pub struct TurnStartResponse {}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211#[serde(rename_all = "camelCase")]
212pub struct TurnInterruptParams {
213 pub thread_id: String,
214}
215
216#[derive(Debug, Clone, Default, Serialize, Deserialize)]
218#[serde(rename_all = "camelCase")]
219pub struct TurnInterruptResponse {}
220
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
227#[serde(rename_all = "camelCase")]
228pub enum TurnStatus {
229 Completed,
231 Interrupted,
233 Failed,
235 InProgress,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
241#[serde(rename_all = "camelCase")]
242pub struct TurnError {
243 pub message: String,
244 #[serde(skip_serializing_if = "Option::is_none")]
245 pub codex_error_info: Option<Value>,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
252#[serde(rename_all = "camelCase")]
253pub struct Turn {
254 pub id: String,
256 #[serde(default)]
258 pub items: Vec<ThreadItem>,
259 pub status: TurnStatus,
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub error: Option<TurnError>,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
274#[serde(rename_all = "camelCase")]
275pub struct TokenUsage {
276 pub input_tokens: u64,
278 pub output_tokens: u64,
280 #[serde(default)]
282 pub cached_input_tokens: u64,
283}
284
285#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
291#[serde(rename_all = "camelCase")]
292pub enum ThreadStatus {
293 NotLoaded,
295 Idle,
297 Active,
299 SystemError,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
309#[serde(rename_all = "camelCase")]
310pub struct ThreadStartedNotification {
311 pub thread_id: String,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316#[serde(rename_all = "camelCase")]
317pub struct ThreadStatusChangedNotification {
318 pub thread_id: String,
319 pub status: ThreadStatus,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
324#[serde(rename_all = "camelCase")]
325pub struct TurnStartedNotification {
326 pub thread_id: String,
327 pub turn_id: String,
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize)]
332#[serde(rename_all = "camelCase")]
333pub struct TurnCompletedNotification {
334 pub thread_id: String,
335 pub turn_id: String,
336 pub turn: Turn,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
341#[serde(rename_all = "camelCase")]
342pub struct ItemStartedNotification {
343 pub thread_id: String,
344 pub turn_id: String,
345 pub item: ThreadItem,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
350#[serde(rename_all = "camelCase")]
351pub struct ItemCompletedNotification {
352 pub thread_id: String,
353 pub turn_id: String,
354 pub item: ThreadItem,
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize)]
359#[serde(rename_all = "camelCase")]
360pub struct AgentMessageDeltaNotification {
361 pub thread_id: String,
362 pub item_id: String,
363 pub delta: String,
364}
365
366#[derive(Debug, Clone, Serialize, Deserialize)]
368#[serde(rename_all = "camelCase")]
369pub struct CmdOutputDeltaNotification {
370 pub thread_id: String,
371 pub item_id: String,
372 pub delta: String,
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize)]
377#[serde(rename_all = "camelCase")]
378pub struct FileChangeOutputDeltaNotification {
379 pub thread_id: String,
380 pub item_id: String,
381 pub delta: String,
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize)]
386#[serde(rename_all = "camelCase")]
387pub struct ReasoningDeltaNotification {
388 pub thread_id: String,
389 pub item_id: String,
390 pub delta: String,
391}
392
393#[derive(Debug, Clone, Serialize, Deserialize)]
395#[serde(rename_all = "camelCase")]
396pub struct ErrorNotification {
397 pub error: String,
398 #[serde(skip_serializing_if = "Option::is_none")]
399 pub thread_id: Option<String>,
400 #[serde(skip_serializing_if = "Option::is_none")]
401 pub turn_id: Option<String>,
402 #[serde(default)]
403 pub will_retry: bool,
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
408#[serde(rename_all = "camelCase")]
409pub struct ThreadTokenUsageUpdatedNotification {
410 pub thread_id: String,
411 pub usage: TokenUsage,
412}
413
414#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
423#[serde(rename_all = "camelCase")]
424pub enum CommandApprovalDecision {
425 Accept,
427 AcceptForSession,
429 Decline,
431 Cancel,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize)]
441#[serde(rename_all = "camelCase")]
442pub struct CommandExecutionApprovalParams {
443 pub thread_id: String,
444 pub turn_id: String,
445 pub call_id: String,
447 pub command: String,
449 pub cwd: String,
451 #[serde(skip_serializing_if = "Option::is_none")]
453 pub reason: Option<String>,
454}
455
456#[derive(Debug, Clone, Serialize, Deserialize)]
458#[serde(rename_all = "camelCase")]
459pub struct CommandExecutionApprovalResponse {
460 pub decision: CommandApprovalDecision,
461}
462
463#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
468#[serde(rename_all = "camelCase")]
469pub enum FileChangeApprovalDecision {
470 Accept,
472 AcceptForSession,
474 Decline,
476 Cancel,
478}
479
480#[derive(Debug, Clone, Serialize, Deserialize)]
486#[serde(rename_all = "camelCase")]
487pub struct FileChangeApprovalParams {
488 pub thread_id: String,
489 pub turn_id: String,
490 pub call_id: String,
492 pub changes: Value,
494 #[serde(skip_serializing_if = "Option::is_none")]
496 pub reason: Option<String>,
497}
498
499#[derive(Debug, Clone, Serialize, Deserialize)]
501#[serde(rename_all = "camelCase")]
502pub struct FileChangeApprovalResponse {
503 pub decision: FileChangeApprovalDecision,
504}
505
506#[derive(Debug, Clone)]
523pub enum ServerMessage {
524 Notification {
526 method: String,
527 params: Option<Value>,
528 },
529 Request {
532 id: RequestId,
533 method: String,
534 params: Option<Value>,
535 },
536}
537
538pub mod methods {
547 pub const INITIALIZE: &str = "initialize";
549 pub const INITIALIZED: &str = "initialized";
550 pub const THREAD_START: &str = "thread/start";
551 pub const THREAD_ARCHIVE: &str = "thread/archive";
552 pub const TURN_START: &str = "turn/start";
553 pub const TURN_INTERRUPT: &str = "turn/interrupt";
554 pub const TURN_STEER: &str = "turn/steer";
555
556 pub const THREAD_STARTED: &str = "thread/started";
558 pub const THREAD_STATUS_CHANGED: &str = "thread/status/changed";
559 pub const THREAD_TOKEN_USAGE_UPDATED: &str = "thread/tokenUsage/updated";
560 pub const TURN_STARTED: &str = "turn/started";
561 pub const TURN_COMPLETED: &str = "turn/completed";
562 pub const ITEM_STARTED: &str = "item/started";
563 pub const ITEM_COMPLETED: &str = "item/completed";
564 pub const AGENT_MESSAGE_DELTA: &str = "item/agentMessage/delta";
565 pub const CMD_OUTPUT_DELTA: &str = "item/commandExecution/outputDelta";
566 pub const FILE_CHANGE_OUTPUT_DELTA: &str = "item/fileChange/outputDelta";
567 pub const REASONING_DELTA: &str = "item/reasoning/summaryTextDelta";
568 pub const ERROR: &str = "error";
569
570 pub const CMD_EXEC_APPROVAL: &str = "item/commandExecution/requestApproval";
572 pub const FILE_CHANGE_APPROVAL: &str = "item/fileChange/requestApproval";
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578
579 #[test]
580 fn test_initialize_params() {
581 let params = InitializeParams {
582 client_info: ClientInfo {
583 name: "my-app".to_string(),
584 version: "1.0.0".to_string(),
585 title: Some("My App".to_string()),
586 },
587 capabilities: None,
588 };
589 let json = serde_json::to_string(¶ms).unwrap();
590 assert!(json.contains("clientInfo"));
591 assert!(json.contains("my-app"));
592 assert!(!json.contains("capabilities"));
593 }
594
595 #[test]
596 fn test_initialize_response() {
597 let json = r#"{"userAgent":"codex-cli/0.104.0"}"#;
598 let resp: InitializeResponse = serde_json::from_str(json).unwrap();
599 assert_eq!(resp.user_agent, "codex-cli/0.104.0");
600 }
601
602 #[test]
603 fn test_initialize_capabilities() {
604 let params = InitializeParams {
605 client_info: ClientInfo {
606 name: "test".to_string(),
607 version: "0.1.0".to_string(),
608 title: None,
609 },
610 capabilities: Some(InitializeCapabilities {
611 experimental_api: true,
612 opt_out_notification_methods: Some(vec!["thread/started".to_string()]),
613 }),
614 };
615 let json = serde_json::to_string(¶ms).unwrap();
616 assert!(json.contains("experimentalApi"));
617 assert!(json.contains("optOutNotificationMethods"));
618 }
619
620 #[test]
621 fn test_user_input_text() {
622 let input = UserInput::Text {
623 text: "Hello".to_string(),
624 };
625 let json = serde_json::to_string(&input).unwrap();
626 assert!(json.contains(r#""type":"text""#));
627 let parsed: UserInput = serde_json::from_str(&json).unwrap();
628 assert!(matches!(parsed, UserInput::Text { text } if text == "Hello"));
629 }
630
631 #[test]
632 fn test_thread_start_params() {
633 let params = ThreadStartParams {
634 instructions: Some("Be helpful".to_string()),
635 tools: None,
636 };
637 let json = serde_json::to_string(¶ms).unwrap();
638 assert!(json.contains("instructions"));
639 assert!(!json.contains("tools"));
640 }
641
642 #[test]
643 fn test_thread_start_response() {
644 let json = r#"{"thread":{"id":"th_abc123"},"model":"gpt-4","approvalPolicy":"never","cwd":"/tmp","modelProvider":"openai","sandbox":{}}"#;
645 let resp: ThreadStartResponse = serde_json::from_str(json).unwrap();
646 assert_eq!(resp.thread_id(), "th_abc123");
647 assert_eq!(resp.model.as_deref(), Some("gpt-4"));
648 }
649
650 #[test]
651 fn test_turn_start_params() {
652 let params = TurnStartParams {
653 thread_id: "th_1".to_string(),
654 input: vec![UserInput::Text {
655 text: "What is 2+2?".to_string(),
656 }],
657 model: None,
658 reasoning_effort: None,
659 sandbox_policy: None,
660 };
661 let json = serde_json::to_string(¶ms).unwrap();
662 assert!(json.contains("threadId"));
663 assert!(json.contains("input"));
664 }
665
666 #[test]
667 fn test_turn_status() {
668 let json = r#""completed""#;
669 let status: TurnStatus = serde_json::from_str(json).unwrap();
670 assert_eq!(status, TurnStatus::Completed);
671 }
672
673 #[test]
674 fn test_turn_completed_notification() {
675 let json = r#"{
676 "threadId": "th_1",
677 "turnId": "t_1",
678 "turn": {
679 "id": "t_1",
680 "items": [],
681 "status": "completed"
682 }
683 }"#;
684 let notif: TurnCompletedNotification = serde_json::from_str(json).unwrap();
685 assert_eq!(notif.thread_id, "th_1");
686 assert_eq!(notif.turn.status, TurnStatus::Completed);
687 }
688
689 #[test]
690 fn test_agent_message_delta() {
691 let json = r#"{"threadId":"th_1","itemId":"msg_1","delta":"Hello "}"#;
692 let notif: AgentMessageDeltaNotification = serde_json::from_str(json).unwrap();
693 assert_eq!(notif.delta, "Hello ");
694 }
695
696 #[test]
697 fn test_command_approval_decision() {
698 let json = r#""accept""#;
699 let decision: CommandApprovalDecision = serde_json::from_str(json).unwrap();
700 assert_eq!(decision, CommandApprovalDecision::Accept);
701
702 let json = r#""acceptForSession""#;
703 let decision: CommandApprovalDecision = serde_json::from_str(json).unwrap();
704 assert_eq!(decision, CommandApprovalDecision::AcceptForSession);
705 }
706
707 #[test]
708 fn test_command_approval_params() {
709 let json = r#"{
710 "threadId": "th_1",
711 "turnId": "t_1",
712 "callId": "call_1",
713 "command": "rm -rf /tmp/test",
714 "cwd": "/home/user"
715 }"#;
716 let params: CommandExecutionApprovalParams = serde_json::from_str(json).unwrap();
717 assert_eq!(params.command, "rm -rf /tmp/test");
718 }
719
720 #[test]
721 fn test_error_notification() {
722 let json = r#"{"error":"something failed","willRetry":true}"#;
723 let notif: ErrorNotification = serde_json::from_str(json).unwrap();
724 assert_eq!(notif.error, "something failed");
725 assert!(notif.will_retry);
726 }
727
728 #[test]
729 fn test_thread_status() {
730 let json = r#""idle""#;
731 let status: ThreadStatus = serde_json::from_str(json).unwrap();
732 assert_eq!(status, ThreadStatus::Idle);
733 }
734
735 #[test]
736 fn test_token_usage() {
737 let json = r#"{"inputTokens":100,"outputTokens":200,"cachedInputTokens":50}"#;
738 let usage: TokenUsage = serde_json::from_str(json).unwrap();
739 assert_eq!(usage.input_tokens, 100);
740 assert_eq!(usage.output_tokens, 200);
741 assert_eq!(usage.cached_input_tokens, 50);
742 }
743}