1use serde::{Deserialize, Serialize};
44
45use super::content::ContentBlock;
46
47#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
55#[serde(transparent)]
56pub struct SessionId(pub String);
57
58impl SessionId {
59 pub fn new(value: impl Into<String>) -> Self {
64 Self(value.into())
65 }
66
67 pub fn as_str(&self) -> &str {
69 &self.0
70 }
71}
72
73impl core::fmt::Display for SessionId {
74 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
75 core::fmt::Display::fmt(&self.0, formatter)
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
87#[serde(rename_all = "snake_case")]
88pub enum StopReason {
89 EndTurn,
91 MaxTokens,
94 MaxTurnRequests,
98 Refusal,
103 Cancelled,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "camelCase")]
113pub struct NewSessionRequest {
114 pub cwd: String,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub additional_directories: Option<Vec<String>>,
123 pub mcp_servers: Vec<McpServer>,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct NewSessionResponse {
133 pub session_id: SessionId,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub modes: Option<SessionModeState>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub config_options: Option<Vec<SessionConfigOption>>,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152#[serde(rename_all = "camelCase")]
153pub struct LoadSessionRequest {
154 pub session_id: SessionId,
156 pub cwd: String,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub additional_directories: Option<Vec<String>>,
164 pub mcp_servers: Vec<McpServer>,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub struct LoadSessionResponse {
173 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub modes: Option<SessionModeState>,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub config_options: Option<Vec<SessionConfigOption>>,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct ListSessionsRequest {
188 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub cwd: Option<String>,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub cursor: Option<String>,
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
201#[serde(rename_all = "camelCase")]
202pub struct ListSessionsResponse {
203 pub sessions: Vec<SessionInfo>,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub next_cursor: Option<String>,
212}
213
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
217#[serde(rename_all = "camelCase")]
218pub struct SessionInfo {
219 pub session_id: SessionId,
221 pub cwd: String,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub additional_directories: Option<Vec<String>>,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub title: Option<String>,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub updated_at: Option<String>,
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
241#[serde(rename_all = "camelCase")]
242pub struct DeleteSessionRequest {
243 pub session_id: SessionId,
245}
246
247#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
251#[serde(rename_all = "camelCase")]
252pub struct DeleteSessionResponse {}
253
254#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
259#[serde(rename_all = "camelCase")]
260pub struct ResumeSessionRequest {
261 pub session_id: SessionId,
263 pub cwd: String,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub additional_directories: Option<Vec<String>>,
268 pub mcp_servers: Vec<McpServer>,
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
274#[serde(rename_all = "camelCase")]
275pub struct ResumeSessionResponse {
276 #[serde(default, skip_serializing_if = "Option::is_none")]
280 pub modes: Option<SessionModeState>,
281 #[serde(default, skip_serializing_if = "Option::is_none")]
283 pub config_options: Option<Vec<SessionConfigOption>>,
284}
285
286#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
291#[serde(rename_all = "camelCase")]
292pub struct CloseSessionRequest {
293 pub session_id: SessionId,
295}
296
297#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
301#[serde(rename_all = "camelCase")]
302pub struct CloseSessionResponse {}
303
304#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
309#[serde(rename_all = "camelCase")]
310pub struct PromptRequest {
311 pub session_id: SessionId,
313 pub prompt: Vec<ContentBlock>,
319}
320
321#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
324#[serde(rename_all = "camelCase")]
325pub struct PromptResponse {
326 pub stop_reason: StopReason,
328}
329
330#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
336#[serde(rename_all = "camelCase")]
337pub struct CancelNotification {
338 pub session_id: SessionId,
341}
342
343#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
347#[serde(rename_all = "camelCase")]
348pub struct LogoutRequest {
349 #[serde(default, skip_serializing_if = "Option::is_none")]
352 pub session_id: Option<SessionId>,
353}
354
355#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
359#[serde(rename_all = "camelCase")]
360pub struct LogoutResponse {}
361
362#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
370#[serde(tag = "type", rename_all = "lowercase")]
371pub enum McpServer {
372 Stdio(McpServerStdio),
374 Http(McpServerHttp),
377 Sse(McpServerSse),
380}
381
382#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
385#[serde(rename_all = "camelCase")]
386pub struct McpServerStdio {
387 pub name: String,
389 pub command: String,
391 pub args: Vec<String>,
393 pub env: Vec<EnvVariable>,
396}
397
398#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
400#[serde(rename_all = "camelCase")]
401pub struct McpServerHttp {
402 pub name: String,
404 pub url: String,
406 pub headers: Vec<HttpHeader>,
409}
410
411#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
413#[serde(rename_all = "camelCase")]
414pub struct McpServerSse {
415 pub name: String,
417 pub url: String,
419 pub headers: Vec<HttpHeader>,
422}
423
424#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
428#[serde(rename_all = "camelCase")]
429pub struct EnvVariable {
430 pub name: String,
432 pub value: String,
434}
435
436#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
440#[serde(rename_all = "camelCase")]
441pub struct HttpHeader {
442 pub name: String,
444 pub value: String,
446}
447
448#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
453#[serde(rename_all = "camelCase")]
454pub struct SessionModeState {
455 pub available_modes: Vec<SessionMode>,
457 pub current_mode_id: SessionModeId,
460}
461
462#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
466#[serde(rename_all = "camelCase")]
467pub struct SessionMode {
468 pub id: SessionModeId,
473 pub name: String,
475 #[serde(default, skip_serializing_if = "Option::is_none")]
477 pub description: Option<String>,
478}
479
480#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
487#[serde(transparent)]
488pub struct SessionModeId(pub String);
489
490impl SessionModeId {
491 pub fn new(value: impl Into<String>) -> Self {
494 Self(value.into())
495 }
496}
497
498impl core::fmt::Display for SessionModeId {
499 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
500 core::fmt::Display::fmt(&self.0, formatter)
501 }
502}
503
504#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
508#[serde(rename_all = "camelCase")]
509pub struct SessionConfigOption {
510 pub id: String,
514 pub name: String,
516 #[serde(default, skip_serializing_if = "Option::is_none")]
518 pub description: Option<String>,
519 pub current_value: String,
521 #[serde(default, skip_serializing_if = "Option::is_none")]
524 pub options: Option<Vec<String>>,
525}
526
527#[cfg(test)]
528mod tests {
529 use super::*;
530 use serde_json::json;
531 use std::collections::BTreeMap;
532
533 #[test]
538 fn a_prompt_request_round_trips() -> Result<(), serde_json::Error> {
539 let request = PromptRequest {
540 session_id: SessionId::new("session-1"),
541 prompt: vec![ContentBlock::Text(super::super::content::TextContent {
542 text: "hi".to_owned(),
543 annotations: None,
544 meta: BTreeMap::new(),
545 })],
546 };
547 let value = serde_json::to_value(&request)?;
548 assert_eq!(value.get("sessionId"), Some(&json!("session-1")));
549 let prompt = value
550 .get("prompt")
551 .and_then(serde_json::Value::as_array)
552 .ok_or_else(|| {
553 serde_json::Error::io(std::io::Error::other("prompt is not an array"))
554 })?;
555 assert_eq!(
556 prompt.first().and_then(|b| b.get("type")),
557 Some(&json!("text"))
558 );
559 let back: PromptRequest = serde_json::from_value(value)?;
560 assert_eq!(back, request);
561 Ok(())
562 }
563
564 #[test]
568 fn a_session_id_newtype_round_trips() -> Result<(), serde_json::Error> {
569 let id = SessionId::new("abc-123");
570 let value = serde_json::to_value(&id)?;
571 assert_eq!(value, json!("abc-123"));
572 let back: SessionId = serde_json::from_value(value)?;
573 assert_eq!(back, id);
574 Ok(())
575 }
576
577 #[test]
581 fn mcp_server_stdio_round_trips() -> Result<(), serde_json::Error> {
582 let server = McpServer::Stdio(McpServerStdio {
583 name: "stdio-server".to_owned(),
584 command: "/usr/local/bin/mcp-server".to_owned(),
585 args: vec!["--quiet".to_owned()],
586 env: vec![EnvVariable {
587 name: "LOG_LEVEL".to_owned(),
588 value: "info".to_owned(),
589 }],
590 });
591 let value = serde_json::to_value(&server)?;
592 assert_eq!(value.get("type"), Some(&json!("stdio")));
593 assert_eq!(value.get("name"), Some(&json!("stdio-server")));
594 let back: McpServer = serde_json::from_value(value)?;
595 assert_eq!(back, server);
596 Ok(())
597 }
598
599 #[test]
603 fn a_new_session_request_round_trips() -> Result<(), serde_json::Error> {
604 let request = NewSessionRequest {
605 cwd: "/home/user/project".to_owned(),
606 additional_directories: Some(vec!["/home/user/shared".to_owned()]),
607 mcp_servers: vec![],
608 };
609 let value = serde_json::to_value(&request)?;
610 assert_eq!(value.get("cwd"), Some(&json!("/home/user/project")));
611 assert_eq!(
612 value.get("additionalDirectories"),
613 Some(&json!(["/home/user/shared"]))
614 );
615 assert_eq!(value.get("mcpServers"), Some(&json!([])));
616 let back: NewSessionRequest = serde_json::from_value(value)?;
617 assert_eq!(back, request);
618 Ok(())
619 }
620
621 #[test]
625 fn a_list_sessions_request_default_round_trips() -> Result<(), serde_json::Error> {
626 let request = ListSessionsRequest::default();
627 let value = serde_json::to_value(&request)?;
628 assert_eq!(value, json!({}));
629 let back: ListSessionsRequest = serde_json::from_value(value)?;
630 assert_eq!(back, request);
631 Ok(())
632 }
633
634 #[test]
639 fn a_cancel_notification_round_trips() -> Result<(), serde_json::Error> {
640 let notification = CancelNotification {
641 session_id: SessionId::new("session-1"),
642 };
643 let value = serde_json::to_value(¬ification)?;
644 assert_eq!(value.get("sessionId"), Some(&json!("session-1")));
645 let back: CancelNotification = serde_json::from_value(value)?;
646 assert_eq!(back, notification);
647 Ok(())
648 }
649
650 #[test]
654 fn stop_reason_string_round_trips() -> Result<(), serde_json::Error> {
655 for (variant, wire) in [
656 (StopReason::EndTurn, "end_turn"),
657 (StopReason::MaxTokens, "max_tokens"),
658 (StopReason::MaxTurnRequests, "max_turn_requests"),
659 (StopReason::Refusal, "refusal"),
660 (StopReason::Cancelled, "cancelled"),
661 ] {
662 let value = serde_json::to_value(variant)?;
663 assert_eq!(value, json!(wire));
664 let back: StopReason = serde_json::from_value(value)?;
665 assert_eq!(back, variant);
666 }
667 Ok(())
668 }
669
670 #[test]
675 fn a_new_session_response_round_trips() -> Result<(), serde_json::Error> {
676 let response = NewSessionResponse {
677 session_id: SessionId::new("session-1"),
678 modes: Some(SessionModeState {
679 available_modes: vec![SessionMode {
680 id: SessionModeId::new("default"),
681 name: "Default".to_owned(),
682 description: None,
683 }],
684 current_mode_id: SessionModeId::new("default"),
685 }),
686 config_options: None,
687 };
688 let value = serde_json::to_value(&response)?;
689 assert_eq!(value.get("sessionId"), Some(&json!("session-1")));
690 let modes = value
691 .get("modes")
692 .ok_or_else(|| serde_json::Error::io(std::io::Error::other("missing `modes` field")))?;
693 assert_eq!(modes.get("currentModeId"), Some(&json!("default")));
694 let back: NewSessionResponse = serde_json::from_value(value)?;
695 assert_eq!(back, response);
696 Ok(())
697 }
698
699 #[test]
703 fn a_load_session_request_round_trips() -> Result<(), serde_json::Error> {
704 let request = LoadSessionRequest {
705 session_id: SessionId::new("session-1"),
706 cwd: "/home/user/project".to_owned(),
707 additional_directories: None,
708 mcp_servers: vec![],
709 };
710 let value = serde_json::to_value(&request)?;
711 assert_eq!(value.get("sessionId"), Some(&json!("session-1")));
712 assert_eq!(value.get("cwd"), Some(&json!("/home/user/project")));
713 let back: LoadSessionRequest = serde_json::from_value(value)?;
714 assert_eq!(back, request);
715 Ok(())
716 }
717
718 #[test]
722 fn a_load_session_response_round_trips() -> Result<(), serde_json::Error> {
723 let response = LoadSessionResponse {
724 modes: None,
725 config_options: None,
726 };
727 let value = serde_json::to_value(&response)?;
728 assert_eq!(value, json!({}));
729 let back: LoadSessionResponse = serde_json::from_value(value)?;
730 assert_eq!(back, response);
731 Ok(())
732 }
733
734 #[test]
739 fn a_list_sessions_response_round_trips() -> Result<(), serde_json::Error> {
740 let response = ListSessionsResponse {
741 sessions: vec![SessionInfo {
742 session_id: SessionId::new("session-1"),
743 cwd: "/home/user/project".to_owned(),
744 additional_directories: None,
745 title: Some("My project".to_owned()),
746 updated_at: Some("2026-10-15T00:00:00Z".to_owned()),
747 }],
748 next_cursor: None,
749 };
750 let value = serde_json::to_value(&response)?;
751 let sessions = value
752 .get("sessions")
753 .and_then(serde_json::Value::as_array)
754 .ok_or_else(|| serde_json::Error::io(std::io::Error::other("missing `sessions`")))?;
755 assert_eq!(sessions.len(), 1);
756 assert_eq!(
757 sessions
758 .first()
759 .and_then(|s| s.get("sessionId"))
760 .and_then(serde_json::Value::as_str),
761 Some("session-1"),
762 );
763 let back: ListSessionsResponse = serde_json::from_value(value)?;
764 assert_eq!(back, response);
765 Ok(())
766 }
767
768 #[test]
772 fn a_delete_session_request_round_trips() -> Result<(), serde_json::Error> {
773 let request = DeleteSessionRequest {
774 session_id: SessionId::new("session-1"),
775 };
776 let value = serde_json::to_value(&request)?;
777 assert_eq!(value, json!({"sessionId": "session-1"}));
778 let back: DeleteSessionRequest = serde_json::from_value(value)?;
779 assert_eq!(back, request);
780 Ok(())
781 }
782
783 #[test]
787 fn a_delete_session_response_round_trips() -> Result<(), serde_json::Error> {
788 let response = DeleteSessionResponse::default();
789 let value = serde_json::to_value(&response)?;
790 assert_eq!(value, json!({}));
791 let back: DeleteSessionResponse = serde_json::from_value(value)?;
792 assert_eq!(back, response);
793 Ok(())
794 }
795
796 #[test]
801 fn a_resume_session_request_round_trips() -> Result<(), serde_json::Error> {
802 let request = ResumeSessionRequest {
803 session_id: SessionId::new("session-1"),
804 cwd: "/home/user/project".to_owned(),
805 additional_directories: None,
806 mcp_servers: vec![],
807 };
808 let value = serde_json::to_value(&request)?;
809 assert_eq!(value.get("sessionId"), Some(&json!("session-1")));
810 assert_eq!(value.get("cwd"), Some(&json!("/home/user/project")));
811 let back: ResumeSessionRequest = serde_json::from_value(value)?;
812 assert_eq!(back, request);
813 Ok(())
814 }
815
816 #[test]
820 fn a_resume_session_response_round_trips() -> Result<(), serde_json::Error> {
821 let response = ResumeSessionResponse {
822 modes: None,
823 config_options: None,
824 };
825 let value = serde_json::to_value(&response)?;
826 assert_eq!(value, json!({}));
827 let back: ResumeSessionResponse = serde_json::from_value(value)?;
828 assert_eq!(back, response);
829 Ok(())
830 }
831
832 #[test]
836 fn a_close_session_request_round_trips() -> Result<(), serde_json::Error> {
837 let request = CloseSessionRequest {
838 session_id: SessionId::new("session-1"),
839 };
840 let value = serde_json::to_value(&request)?;
841 assert_eq!(value, json!({"sessionId": "session-1"}));
842 let back: CloseSessionRequest = serde_json::from_value(value)?;
843 assert_eq!(back, request);
844 Ok(())
845 }
846
847 #[test]
851 fn a_close_session_response_round_trips() -> Result<(), serde_json::Error> {
852 let response = CloseSessionResponse::default();
853 let value = serde_json::to_value(&response)?;
854 assert_eq!(value, json!({}));
855 let back: CloseSessionResponse = serde_json::from_value(value)?;
856 assert_eq!(back, response);
857 Ok(())
858 }
859
860 #[test]
864 fn a_prompt_response_round_trips() -> Result<(), serde_json::Error> {
865 let response = PromptResponse {
866 stop_reason: StopReason::EndTurn,
867 };
868 let value = serde_json::to_value(&response)?;
869 assert_eq!(value, json!({"stopReason": "end_turn"}));
870 let back: PromptResponse = serde_json::from_value(value)?;
871 assert_eq!(back, response);
872 Ok(())
873 }
874
875 #[test]
879 fn a_logout_request_default_round_trips() -> Result<(), serde_json::Error> {
880 let request = LogoutRequest::default();
881 let value = serde_json::to_value(&request)?;
882 assert_eq!(value, json!({}));
883 let back: LogoutRequest = serde_json::from_value(value)?;
884 assert_eq!(back, request);
885 Ok(())
886 }
887
888 #[test]
891 fn a_logout_request_with_session_id_round_trips() -> Result<(), serde_json::Error> {
892 let request = LogoutRequest {
893 session_id: Some(SessionId::new("session-1")),
894 };
895 let value = serde_json::to_value(&request)?;
896 assert_eq!(value, json!({"sessionId": "session-1"}));
897 let back: LogoutRequest = serde_json::from_value(value)?;
898 assert_eq!(back, request);
899 Ok(())
900 }
901
902 #[test]
906 fn a_logout_response_round_trips() -> Result<(), serde_json::Error> {
907 let response = LogoutResponse::default();
908 let value = serde_json::to_value(&response)?;
909 assert_eq!(value, json!({}));
910 let back: LogoutResponse = serde_json::from_value(value)?;
911 assert_eq!(back, response);
912 Ok(())
913 }
914
915 #[test]
919 fn mcp_server_http_round_trips() -> Result<(), serde_json::Error> {
920 let server = McpServer::Http(McpServerHttp {
921 name: "http-server".to_owned(),
922 url: "https://mcp.example.com/sse".to_owned(),
923 headers: vec![HttpHeader {
924 name: "Authorization".to_owned(),
925 value: "Bearer token".to_owned(),
926 }],
927 });
928 let value = serde_json::to_value(&server)?;
929 assert_eq!(value.get("type"), Some(&json!("http")));
930 assert_eq!(
931 value.get("url"),
932 Some(&json!("https://mcp.example.com/sse"))
933 );
934 let back: McpServer = serde_json::from_value(value)?;
935 assert_eq!(back, server);
936 Ok(())
937 }
938
939 #[test]
943 fn mcp_server_sse_round_trips() -> Result<(), serde_json::Error> {
944 let server = McpServer::Sse(McpServerSse {
945 name: "sse-server".to_owned(),
946 url: "https://mcp.example.com/sse".to_owned(),
947 headers: vec![],
948 });
949 let value = serde_json::to_value(&server)?;
950 assert_eq!(value.get("type"), Some(&json!("sse")));
951 let back: McpServer = serde_json::from_value(value)?;
952 assert_eq!(back, server);
953 Ok(())
954 }
955
956 #[test]
960 fn a_session_config_option_round_trips() -> Result<(), serde_json::Error> {
961 let option = SessionConfigOption {
962 id: "model".to_owned(),
963 name: "Model".to_owned(),
964 description: Some("The LLM to use".to_owned()),
965 current_value: "claude-sonnet-4-5".to_owned(),
966 options: Some(vec![
967 "claude-sonnet-4-5".to_owned(),
968 "claude-opus-4-7".to_owned(),
969 ]),
970 };
971 let value = serde_json::to_value(&option)?;
972 assert_eq!(value.get("id"), Some(&json!("model")));
973 assert_eq!(value.get("currentValue"), Some(&json!("claude-sonnet-4-5")));
974 let back: SessionConfigOption = serde_json::from_value(value)?;
975 assert_eq!(back, option);
976 Ok(())
977 }
978}