use serde::{Deserialize, Serialize};
use super::content::ContentBlock;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SessionId(pub String);
impl SessionId {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl core::fmt::Display for SessionId {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
core::fmt::Display::fmt(&self.0, formatter)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StopReason {
EndTurn,
MaxTokens,
MaxTurnRequests,
Refusal,
Cancelled,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NewSessionRequest {
pub cwd: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub additional_directories: Option<Vec<String>>,
pub mcp_servers: Vec<McpServer>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NewSessionResponse {
pub session_id: SessionId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub modes: Option<SessionModeState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config_options: Option<Vec<SessionConfigOption>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LoadSessionRequest {
pub session_id: SessionId,
pub cwd: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub additional_directories: Option<Vec<String>>,
pub mcp_servers: Vec<McpServer>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LoadSessionResponse {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub modes: Option<SessionModeState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config_options: Option<Vec<SessionConfigOption>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListSessionsRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListSessionsResponse {
pub sessions: Vec<SessionInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionInfo {
pub session_id: SessionId,
pub cwd: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub additional_directories: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub updated_at: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteSessionRequest {
pub session_id: SessionId,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteSessionResponse {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResumeSessionRequest {
pub session_id: SessionId,
pub cwd: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub additional_directories: Option<Vec<String>>,
pub mcp_servers: Vec<McpServer>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResumeSessionResponse {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub modes: Option<SessionModeState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config_options: Option<Vec<SessionConfigOption>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CloseSessionRequest {
pub session_id: SessionId,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CloseSessionResponse {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptRequest {
pub session_id: SessionId,
pub prompt: Vec<ContentBlock>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptResponse {
pub stop_reason: StopReason,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CancelNotification {
pub session_id: SessionId,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LogoutRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<SessionId>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LogoutResponse {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum McpServer {
Stdio(McpServerStdio),
Http(McpServerHttp),
Sse(McpServerSse),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpServerStdio {
pub name: String,
pub command: String,
pub args: Vec<String>,
pub env: Vec<EnvVariable>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpServerHttp {
pub name: String,
pub url: String,
pub headers: Vec<HttpHeader>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpServerSse {
pub name: String,
pub url: String,
pub headers: Vec<HttpHeader>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EnvVariable {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HttpHeader {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionModeState {
pub available_modes: Vec<SessionMode>,
pub current_mode_id: SessionModeId,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionMode {
pub id: SessionModeId,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SessionModeId(pub String);
impl SessionModeId {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
}
impl core::fmt::Display for SessionModeId {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
core::fmt::Display::fmt(&self.0, formatter)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionConfigOption {
pub id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub current_value: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub options: Option<Vec<String>>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::collections::BTreeMap;
#[test]
fn a_prompt_request_round_trips() -> Result<(), serde_json::Error> {
let request = PromptRequest {
session_id: SessionId::new("session-1"),
prompt: vec![ContentBlock::Text(super::super::content::TextContent {
text: "hi".to_owned(),
annotations: None,
meta: BTreeMap::new(),
})],
};
let value = serde_json::to_value(&request)?;
assert_eq!(value.get("sessionId"), Some(&json!("session-1")));
let prompt = value
.get("prompt")
.and_then(serde_json::Value::as_array)
.ok_or_else(|| {
serde_json::Error::io(std::io::Error::other("prompt is not an array"))
})?;
assert_eq!(
prompt.first().and_then(|b| b.get("type")),
Some(&json!("text"))
);
let back: PromptRequest = serde_json::from_value(value)?;
assert_eq!(back, request);
Ok(())
}
#[test]
fn a_session_id_newtype_round_trips() -> Result<(), serde_json::Error> {
let id = SessionId::new("abc-123");
let value = serde_json::to_value(&id)?;
assert_eq!(value, json!("abc-123"));
let back: SessionId = serde_json::from_value(value)?;
assert_eq!(back, id);
Ok(())
}
#[test]
fn mcp_server_stdio_round_trips() -> Result<(), serde_json::Error> {
let server = McpServer::Stdio(McpServerStdio {
name: "stdio-server".to_owned(),
command: "/usr/local/bin/mcp-server".to_owned(),
args: vec!["--quiet".to_owned()],
env: vec![EnvVariable {
name: "LOG_LEVEL".to_owned(),
value: "info".to_owned(),
}],
});
let value = serde_json::to_value(&server)?;
assert_eq!(value.get("type"), Some(&json!("stdio")));
assert_eq!(value.get("name"), Some(&json!("stdio-server")));
let back: McpServer = serde_json::from_value(value)?;
assert_eq!(back, server);
Ok(())
}
#[test]
fn a_new_session_request_round_trips() -> Result<(), serde_json::Error> {
let request = NewSessionRequest {
cwd: "/home/user/project".to_owned(),
additional_directories: Some(vec!["/home/user/shared".to_owned()]),
mcp_servers: vec![],
};
let value = serde_json::to_value(&request)?;
assert_eq!(value.get("cwd"), Some(&json!("/home/user/project")));
assert_eq!(
value.get("additionalDirectories"),
Some(&json!(["/home/user/shared"]))
);
assert_eq!(value.get("mcpServers"), Some(&json!([])));
let back: NewSessionRequest = serde_json::from_value(value)?;
assert_eq!(back, request);
Ok(())
}
#[test]
fn a_list_sessions_request_default_round_trips() -> Result<(), serde_json::Error> {
let request = ListSessionsRequest::default();
let value = serde_json::to_value(&request)?;
assert_eq!(value, json!({}));
let back: ListSessionsRequest = serde_json::from_value(value)?;
assert_eq!(back, request);
Ok(())
}
#[test]
fn a_cancel_notification_round_trips() -> Result<(), serde_json::Error> {
let notification = CancelNotification {
session_id: SessionId::new("session-1"),
};
let value = serde_json::to_value(¬ification)?;
assert_eq!(value.get("sessionId"), Some(&json!("session-1")));
let back: CancelNotification = serde_json::from_value(value)?;
assert_eq!(back, notification);
Ok(())
}
#[test]
fn stop_reason_string_round_trips() -> Result<(), serde_json::Error> {
for (variant, wire) in [
(StopReason::EndTurn, "end_turn"),
(StopReason::MaxTokens, "max_tokens"),
(StopReason::MaxTurnRequests, "max_turn_requests"),
(StopReason::Refusal, "refusal"),
(StopReason::Cancelled, "cancelled"),
] {
let value = serde_json::to_value(variant)?;
assert_eq!(value, json!(wire));
let back: StopReason = serde_json::from_value(value)?;
assert_eq!(back, variant);
}
Ok(())
}
#[test]
fn a_new_session_response_round_trips() -> Result<(), serde_json::Error> {
let response = NewSessionResponse {
session_id: SessionId::new("session-1"),
modes: Some(SessionModeState {
available_modes: vec![SessionMode {
id: SessionModeId::new("default"),
name: "Default".to_owned(),
description: None,
}],
current_mode_id: SessionModeId::new("default"),
}),
config_options: None,
};
let value = serde_json::to_value(&response)?;
assert_eq!(value.get("sessionId"), Some(&json!("session-1")));
let modes = value
.get("modes")
.ok_or_else(|| serde_json::Error::io(std::io::Error::other("missing `modes` field")))?;
assert_eq!(modes.get("currentModeId"), Some(&json!("default")));
let back: NewSessionResponse = serde_json::from_value(value)?;
assert_eq!(back, response);
Ok(())
}
#[test]
fn a_load_session_request_round_trips() -> Result<(), serde_json::Error> {
let request = LoadSessionRequest {
session_id: SessionId::new("session-1"),
cwd: "/home/user/project".to_owned(),
additional_directories: None,
mcp_servers: vec![],
};
let value = serde_json::to_value(&request)?;
assert_eq!(value.get("sessionId"), Some(&json!("session-1")));
assert_eq!(value.get("cwd"), Some(&json!("/home/user/project")));
let back: LoadSessionRequest = serde_json::from_value(value)?;
assert_eq!(back, request);
Ok(())
}
#[test]
fn a_load_session_response_round_trips() -> Result<(), serde_json::Error> {
let response = LoadSessionResponse {
modes: None,
config_options: None,
};
let value = serde_json::to_value(&response)?;
assert_eq!(value, json!({}));
let back: LoadSessionResponse = serde_json::from_value(value)?;
assert_eq!(back, response);
Ok(())
}
#[test]
fn a_list_sessions_response_round_trips() -> Result<(), serde_json::Error> {
let response = ListSessionsResponse {
sessions: vec![SessionInfo {
session_id: SessionId::new("session-1"),
cwd: "/home/user/project".to_owned(),
additional_directories: None,
title: Some("My project".to_owned()),
updated_at: Some("2026-10-15T00:00:00Z".to_owned()),
}],
next_cursor: None,
};
let value = serde_json::to_value(&response)?;
let sessions = value
.get("sessions")
.and_then(serde_json::Value::as_array)
.ok_or_else(|| serde_json::Error::io(std::io::Error::other("missing `sessions`")))?;
assert_eq!(sessions.len(), 1);
assert_eq!(
sessions
.first()
.and_then(|s| s.get("sessionId"))
.and_then(serde_json::Value::as_str),
Some("session-1"),
);
let back: ListSessionsResponse = serde_json::from_value(value)?;
assert_eq!(back, response);
Ok(())
}
#[test]
fn a_delete_session_request_round_trips() -> Result<(), serde_json::Error> {
let request = DeleteSessionRequest {
session_id: SessionId::new("session-1"),
};
let value = serde_json::to_value(&request)?;
assert_eq!(value, json!({"sessionId": "session-1"}));
let back: DeleteSessionRequest = serde_json::from_value(value)?;
assert_eq!(back, request);
Ok(())
}
#[test]
fn a_delete_session_response_round_trips() -> Result<(), serde_json::Error> {
let response = DeleteSessionResponse::default();
let value = serde_json::to_value(&response)?;
assert_eq!(value, json!({}));
let back: DeleteSessionResponse = serde_json::from_value(value)?;
assert_eq!(back, response);
Ok(())
}
#[test]
fn a_resume_session_request_round_trips() -> Result<(), serde_json::Error> {
let request = ResumeSessionRequest {
session_id: SessionId::new("session-1"),
cwd: "/home/user/project".to_owned(),
additional_directories: None,
mcp_servers: vec![],
};
let value = serde_json::to_value(&request)?;
assert_eq!(value.get("sessionId"), Some(&json!("session-1")));
assert_eq!(value.get("cwd"), Some(&json!("/home/user/project")));
let back: ResumeSessionRequest = serde_json::from_value(value)?;
assert_eq!(back, request);
Ok(())
}
#[test]
fn a_resume_session_response_round_trips() -> Result<(), serde_json::Error> {
let response = ResumeSessionResponse {
modes: None,
config_options: None,
};
let value = serde_json::to_value(&response)?;
assert_eq!(value, json!({}));
let back: ResumeSessionResponse = serde_json::from_value(value)?;
assert_eq!(back, response);
Ok(())
}
#[test]
fn a_close_session_request_round_trips() -> Result<(), serde_json::Error> {
let request = CloseSessionRequest {
session_id: SessionId::new("session-1"),
};
let value = serde_json::to_value(&request)?;
assert_eq!(value, json!({"sessionId": "session-1"}));
let back: CloseSessionRequest = serde_json::from_value(value)?;
assert_eq!(back, request);
Ok(())
}
#[test]
fn a_close_session_response_round_trips() -> Result<(), serde_json::Error> {
let response = CloseSessionResponse::default();
let value = serde_json::to_value(&response)?;
assert_eq!(value, json!({}));
let back: CloseSessionResponse = serde_json::from_value(value)?;
assert_eq!(back, response);
Ok(())
}
#[test]
fn a_prompt_response_round_trips() -> Result<(), serde_json::Error> {
let response = PromptResponse {
stop_reason: StopReason::EndTurn,
};
let value = serde_json::to_value(&response)?;
assert_eq!(value, json!({"stopReason": "end_turn"}));
let back: PromptResponse = serde_json::from_value(value)?;
assert_eq!(back, response);
Ok(())
}
#[test]
fn a_logout_request_default_round_trips() -> Result<(), serde_json::Error> {
let request = LogoutRequest::default();
let value = serde_json::to_value(&request)?;
assert_eq!(value, json!({}));
let back: LogoutRequest = serde_json::from_value(value)?;
assert_eq!(back, request);
Ok(())
}
#[test]
fn a_logout_request_with_session_id_round_trips() -> Result<(), serde_json::Error> {
let request = LogoutRequest {
session_id: Some(SessionId::new("session-1")),
};
let value = serde_json::to_value(&request)?;
assert_eq!(value, json!({"sessionId": "session-1"}));
let back: LogoutRequest = serde_json::from_value(value)?;
assert_eq!(back, request);
Ok(())
}
#[test]
fn a_logout_response_round_trips() -> Result<(), serde_json::Error> {
let response = LogoutResponse::default();
let value = serde_json::to_value(&response)?;
assert_eq!(value, json!({}));
let back: LogoutResponse = serde_json::from_value(value)?;
assert_eq!(back, response);
Ok(())
}
#[test]
fn mcp_server_http_round_trips() -> Result<(), serde_json::Error> {
let server = McpServer::Http(McpServerHttp {
name: "http-server".to_owned(),
url: "https://mcp.example.com/sse".to_owned(),
headers: vec![HttpHeader {
name: "Authorization".to_owned(),
value: "Bearer token".to_owned(),
}],
});
let value = serde_json::to_value(&server)?;
assert_eq!(value.get("type"), Some(&json!("http")));
assert_eq!(
value.get("url"),
Some(&json!("https://mcp.example.com/sse"))
);
let back: McpServer = serde_json::from_value(value)?;
assert_eq!(back, server);
Ok(())
}
#[test]
fn mcp_server_sse_round_trips() -> Result<(), serde_json::Error> {
let server = McpServer::Sse(McpServerSse {
name: "sse-server".to_owned(),
url: "https://mcp.example.com/sse".to_owned(),
headers: vec![],
});
let value = serde_json::to_value(&server)?;
assert_eq!(value.get("type"), Some(&json!("sse")));
let back: McpServer = serde_json::from_value(value)?;
assert_eq!(back, server);
Ok(())
}
#[test]
fn a_session_config_option_round_trips() -> Result<(), serde_json::Error> {
let option = SessionConfigOption {
id: "model".to_owned(),
name: "Model".to_owned(),
description: Some("The LLM to use".to_owned()),
current_value: "claude-sonnet-4-5".to_owned(),
options: Some(vec![
"claude-sonnet-4-5".to_owned(),
"claude-opus-4-7".to_owned(),
]),
};
let value = serde_json::to_value(&option)?;
assert_eq!(value.get("id"), Some(&json!("model")));
assert_eq!(value.get("currentValue"), Some(&json!("claude-sonnet-4-5")));
let back: SessionConfigOption = serde_json::from_value(value)?;
assert_eq!(back, option);
Ok(())
}
}